72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { getSessionUser } from "@/lib/session";
|
|
import { getOrgBilling } from "@/lib/db";
|
|
import {
|
|
createSetupCheckoutSession,
|
|
ensureStripeCustomerForOrg,
|
|
} from "@/lib/stripe";
|
|
import { safeError } from "@/lib/errors";
|
|
|
|
/**
|
|
* POST /api/billing/setup-card
|
|
*
|
|
* Phase 9. Customer-initiated "Set up auto-pay" / "Update card"
|
|
* flow. Creates a Checkout session in setup mode and returns its
|
|
* URL — the caller redirects the browser. On completion, the
|
|
* webhook handler saves the resulting PaymentMethod's display
|
|
* fields against this org's billing config.
|
|
*
|
|
* Auth: any signed-in member of the org. We don't owner-gate this
|
|
* because non-owners might legitimately need to update payment
|
|
* (e.g., for a team they administer). The actual card data is
|
|
* collected by Stripe, not us — there's nothing to leak from
|
|
* misuse here.
|
|
*
|
|
* Requires an existing billing snapshot (org_billing row). If
|
|
* absent, returns 400 — the customer hasn't set their billing
|
|
* address yet, and Stripe needs the address for the customer
|
|
* object.
|
|
*/
|
|
export async function POST(request: Request) {
|
|
const user = await getSessionUser();
|
|
if (!user) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
const orgBilling = await getOrgBilling(user.orgId);
|
|
if (!orgBilling) {
|
|
return NextResponse.json(
|
|
{ error: "Billing address required before saving a card." },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
try {
|
|
// Ensure the Stripe customer exists. Idempotent — if we
|
|
// already created one for this org (e.g. from a prior
|
|
// "Pay by Card" Checkout), it's reused.
|
|
const customerId = await ensureStripeCustomerForOrg({
|
|
zitadelOrgId: user.orgId,
|
|
companyName: orgBilling.companyName,
|
|
billingEmail: orgBilling.billingEmail,
|
|
address: {
|
|
line1: orgBilling.streetAddress,
|
|
postalCode: orgBilling.postalCode,
|
|
city: orgBilling.city,
|
|
country: orgBilling.country,
|
|
},
|
|
});
|
|
// Pick the base URL from the request's origin so redirects
|
|
// work in dev (localhost), staging, and prod without env vars.
|
|
const origin = new URL(request.url).origin;
|
|
const session = await createSetupCheckoutSession({
|
|
customerId,
|
|
baseUrl: origin,
|
|
});
|
|
return NextResponse.json({ url: session.url });
|
|
} catch (e) {
|
|
return NextResponse.json(
|
|
{ error: safeError(e, "Failed to start card setup") },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|