Phase8: Auto bill credit card
All checks were successful
Build and Push / build (push) Successful in 1m48s
All checks were successful
Build and Push / build (push) Successful in 1m48s
This commit is contained in:
@@ -27,7 +27,7 @@ import {
|
||||
createSetupFeeCheckoutSession,
|
||||
ensureStripeCustomerForOrg,
|
||||
} from "@/lib/stripe";
|
||||
import { createTenantSetupFeeInvoice } from "@/lib/billing";
|
||||
import { createTenantSetupFeeInvoice, voidInvoice } from "@/lib/billing";
|
||||
import { deriveTenantName } from "@/lib/tenant-naming";
|
||||
import type {
|
||||
InvoiceBillingSnapshot,
|
||||
@@ -417,21 +417,23 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 9b: enforce auto-pay before accepting an order. If the
|
||||
// org has no saved card OR has explicitly disabled auto-charge,
|
||||
// the order can't proceed — return 402 with a link to the
|
||||
// settings page where they can set up auto-pay. The wizard
|
||||
// surfaces this as a friendly redirect rather than an error.
|
||||
// Phase 9b (revised): a saved card on file IS the consent to
|
||||
// auto-bill. There is no customer-facing "disable auto-pay"
|
||||
// switch — ordering requires a card, full stop. The
|
||||
// auto_charge_enabled flag is now an admin-only pause (used
|
||||
// during disputes) and does NOT block a customer from ordering:
|
||||
// if admin has paused recurring charges, that's a separate
|
||||
// concern handled on the invoice side, not here. So the gate is
|
||||
// simply: do they have a card on file?
|
||||
const cfg = await getOrgBillingConfig(user.orgId);
|
||||
const hasSavedCard = !!cfg.stripeDefaultPaymentMethodId;
|
||||
const autoChargeOn = cfg.autoChargeEnabled !== false;
|
||||
if (!hasSavedCard || !autoChargeOn) {
|
||||
if (!hasSavedCard) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
"Auto-pay must be set up before ordering a new instance. " +
|
||||
"Please save a card and ensure auto-pay is enabled on /settings/billing.",
|
||||
code: "auto_pay_required",
|
||||
"A payment card is required before ordering a new instance. " +
|
||||
"Please save a card on /settings/billing, then submit again.",
|
||||
code: "card_required",
|
||||
redirectTo: "/settings/billing",
|
||||
},
|
||||
{ status: 402 }
|
||||
@@ -611,7 +613,24 @@ export async function POST(request: Request) {
|
||||
checkoutUrl = url;
|
||||
} catch (e) {
|
||||
console.error("Failed to create setup-fee Checkout session:", e);
|
||||
// Roll back the pending_payment row.
|
||||
// Roll back BOTH the pending_payment row and the setup invoice
|
||||
// we already created. The invoice was issued in 'open' status
|
||||
// but no payment will ever arrive (Checkout never started), so
|
||||
// void it to keep the ledger clean — an open invoice with no
|
||||
// route to payment would otherwise linger and show up in
|
||||
// arrears reports. Void (not delete) preserves the audit trail
|
||||
// and the void reason. Best-effort: a void failure is logged
|
||||
// but doesn't change the 500 we return.
|
||||
await voidInvoice({
|
||||
invoiceId: setupInvoice.id,
|
||||
reason: "Order abandoned before payment (Checkout could not be started)",
|
||||
voidedBy: user.id,
|
||||
}).catch((ve) =>
|
||||
console.error(
|
||||
`Failed to void orphaned setup invoice ${setupInvoice.id}:`,
|
||||
ve
|
||||
)
|
||||
);
|
||||
await deletePendingPaymentRequest(tenantRequest.id).catch(() => undefined);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to start payment. Please try again." },
|
||||
|
||||
Reference in New Issue
Block a user