177 lines
6.2 KiB
TypeScript
177 lines
6.2 KiB
TypeScript
"use server";
|
|
|
|
import { getAdminUser } from "@/lib/admin-permissions";
|
|
import { pool } from "@/lib/db";
|
|
import { getSession } from "@/lib/auth";
|
|
|
|
// Stripe API version type - using const assertion for type safety
|
|
type StripeApiVersion = "2026-06-24.dahlia";
|
|
|
|
// ── Price ID config ────────────────────────────────────────────────────────────
|
|
// Maps plan/addon keys to Stripe price IDs via environment variables
|
|
|
|
const PRICE_KEYS: Record<string, string | undefined> = Object.freeze({
|
|
starter: process.env.STRIPE_PRICE_STARTER,
|
|
farm: process.env.STRIPE_PRICE_FARM,
|
|
enterprise: process.env.STRIPE_PRICE_ENTERPRISE,
|
|
harvest_reach: process.env.STRIPE_PRICE_HARVEST_REACH,
|
|
wholesale_portal: process.env.STRIPE_PRICE_WHOLESALE_PORTAL,
|
|
water_log: process.env.STRIPE_PRICE_WATER_LOG,
|
|
ai_tools: process.env.STRIPE_PRICE_AI_TOOLS,
|
|
square_sync: process.env.STRIPE_PRICE_SQUARE_SYNC,
|
|
sms_campaigns: process.env.STRIPE_PRICE_SMS_CAMPAIGNS,
|
|
});
|
|
|
|
function getPriceId(key: string): string | null {
|
|
return PRICE_KEYS[key] ?? null;
|
|
}
|
|
|
|
// ── Checkout session creation ─────────────────────────────────────────────────
|
|
|
|
async function createStripeCheckoutSession(
|
|
brandId: string,
|
|
priceKey: string,
|
|
successPath: string,
|
|
cancelPath: string,
|
|
annual = false
|
|
): Promise<{ success: boolean; url?: string; error?: string }> {
|
|
|
|
await getSession();
|
|
const adminUser = await getAdminUser();
|
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
|
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
|
return { success: false, error: "Not authorized" };
|
|
}
|
|
|
|
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
|
if (!stripeKey) return { success: false, error: "Stripe not configured" };
|
|
|
|
const priceId = getPriceId(priceKey);
|
|
if (!priceId) return { success: false, error: `No Stripe price configured for "${priceKey}". Set ${priceKey.toUpperCase()}_PRICE in your environment.` };
|
|
|
|
// Get brand's Stripe customer ID
|
|
const custRes = await pool.query<{ stripe_customer_id: string | null; name: string }>(
|
|
"SELECT stripe_customer_id, name FROM brands WHERE id = $1 LIMIT 1",
|
|
[brandId]
|
|
);
|
|
const brand = custRes.rows[0];
|
|
if (!brand?.stripe_customer_id) {
|
|
return { success: false, error: "No Stripe customer found. Complete Stripe setup in Payments settings first." };
|
|
}
|
|
|
|
const Stripe = (await import("stripe")).default;
|
|
const stripe = new Stripe(stripeKey, { apiVersion: "2026-06-24.dahlia" as StripeApiVersion });
|
|
|
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
|
|
|
|
// Build recurring interval
|
|
const recurring = annual ? { interval: "year" } : { interval: "month" };
|
|
|
|
const session = await stripe.checkout.sessions.create({
|
|
customer: brand.stripe_customer_id!,
|
|
mode: "subscription",
|
|
line_items: [{ price: priceId, quantity: 1 }],
|
|
success_url: `${siteUrl}${successPath}?session_id={CHECKOUT_SESSION_ID}&status=success`,
|
|
cancel_url: `${siteUrl}${cancelPath}?status=cancelled`,
|
|
metadata: { brand_id: brandId, price_key: priceKey, billing: annual ? "annual" : "monthly" },
|
|
subscription_data: {
|
|
metadata: { brand_id: brandId, price_key: priceKey },
|
|
},
|
|
});
|
|
|
|
return { success: true, url: session.url ?? undefined };
|
|
}
|
|
|
|
export async function createPlanUpgradeCheckout(
|
|
brandId: string,
|
|
planTier: string,
|
|
billingPeriod?: "monthly" | "annual"
|
|
): Promise<{ success: boolean; url?: string; error?: string }> {
|
|
|
|
if (!["starter", "farm", "enterprise"].includes(planTier)) {
|
|
return { success: false, error: "Invalid plan tier" };
|
|
}
|
|
await getSession();
|
|
const annual = billingPeriod === "annual";
|
|
return createStripeCheckoutSession(
|
|
brandId,
|
|
planTier,
|
|
"/admin/settings/billing",
|
|
"/admin/settings/billing",
|
|
annual
|
|
);
|
|
}
|
|
|
|
export async function createAddonCheckoutSession(
|
|
brandId: string,
|
|
addonKey: string
|
|
): Promise<{ success: boolean; url?: string; error?: string }> {
|
|
|
|
await getSession(); return createStripeCheckoutSession(
|
|
brandId,
|
|
addonKey,
|
|
"/admin/settings/billing",
|
|
"/admin/settings/billing"
|
|
);
|
|
}
|
|
|
|
export async function cancelAddonSubscription(
|
|
brandId: string,
|
|
addonKey: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
|
|
await getSession();
|
|
const adminUser = await getAdminUser();
|
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
|
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
|
return { success: false, error: "Not authorized" };
|
|
}
|
|
|
|
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
|
if (!stripeKey) return { success: false, error: "Stripe not configured" };
|
|
|
|
// Get active subscription for this brand
|
|
const subRes = await pool.query<{ stripe_subscription_id: string | null }>(
|
|
"SELECT stripe_subscription_id FROM subscriptions WHERE brand_id = $1 LIMIT 1",
|
|
[brandId]
|
|
);
|
|
if (subRes.rows.length === 0) {
|
|
return { success: false, error: "No active subscription found" };
|
|
}
|
|
const subData = subRes.rows[0];
|
|
if (!subData?.stripe_subscription_id) {
|
|
return { success: false, error: "No active subscription found" };
|
|
}
|
|
|
|
const Stripe = (await import("stripe")).default;
|
|
const stripe = new Stripe(stripeKey, { apiVersion: "2026-06-24.dahlia" as StripeApiVersion });
|
|
|
|
// Retrieve subscription and find the item for this add-on
|
|
const subscription = await stripe.subscriptions.retrieve(subData.stripe_subscription_id);
|
|
const targetPriceId = getPriceId(addonKey);
|
|
|
|
const itemToDelete = subscription.items.data.find(
|
|
(item) => item.price.id === targetPriceId
|
|
);
|
|
|
|
if (!itemToDelete) {
|
|
return { success: false, error: "Add-on subscription item not found in Stripe" };
|
|
}
|
|
|
|
await stripe.subscriptions.update(subData.stripe_subscription_id, {
|
|
items: [{ id: itemToDelete.id, deleted: true }],
|
|
proration_behavior: "none",
|
|
});
|
|
|
|
// Disable the feature flag locally
|
|
try {
|
|
await pool.query(
|
|
"SELECT set_brand_feature($1, $2, $3)",
|
|
[brandId, addonKey, false]
|
|
);
|
|
} catch {
|
|
// best-effort: webhook reconciliation will eventually fix the flag
|
|
}
|
|
|
|
return { success: true };
|
|
} |