{"record":{"id":"cfa14d92e3bb8fbb","repo":"Stirling-Tools/Stirling-PDF","slug":"finalize-payg-bundle-invoice-failed","errorCode":null,"errorMessage":"finalize-payg-bundle-invoice failed","messagePattern":"finalize-payg-bundle-invoice failed","errorType":"exception","errorClass":"StripeFunctionError","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/portal/billing/stripe.ts","lineNumber":427,"sourceCode":"  quoteId: number;\n  poNumber?: string;\n  /** Optional company — becomes the invoice bill-to name (no length cap). */\n  companyName?: string;\n  /** Required account-holder name — the bill-to when there's no company, else an \"Account holder\" field. */\n  accountName?: string;\n}): Promise<BundleInvoice> {\n  const res = await invoke<BundleInvoiceResponse>(\n    \"finalize-payg-bundle-invoice\",\n    {\n      team_id: req.teamId,\n      quote_id: req.quoteId,\n      ...(req.poNumber ? { po_number: req.poNumber } : {}),\n      ...(req.companyName ? { company_name: req.companyName } : {}),\n      ...(req.accountName ? { account_name: req.accountName } : {}),\n    },\n  );\n  if (!res.success || !res.invoice_id) {\n    throw new StripeFunctionError(\n      res.error ?? \"finalize-payg-bundle-invoice failed\",\n    );\n  }\n  return {\n    invoiceId: res.invoice_id,\n    hostedInvoiceUrl: res.hosted_invoice_url ?? null,\n    invoicePdf: res.invoice_pdf ?? null,\n    status: res.status ?? null,\n  };\n}\n\n/**\n * Cancel an unpaid prepaid-bundle purchase via {@code cancel-payg-bundle-quote}: the edge fn voids the\n * invoice (delete if draft, void if finalized), best-effort cancels the Stripe quote, and voids the quote\n * row so the buyer can start over. Nothing was charged (capacity is credited on invoice.paid), so there's\n * no refund. Throws a StripeFunctionError on failure (e.g. {@code invoice_already_paid}).\n */\nexport async function cancelBundleQuote(req: {","sourceCodeStart":409,"sourceCodeEnd":445,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/portal/billing/stripe.ts#L409-L445","documentation":"Fallback thrown by finalizeBundleInvoice when finalize-payg-bundle-invoice returns success === false OR omits invoice_id. Finalize stamps the recipient + PO and finalizes the Stripe invoice; a missing invoice_id means there is nothing the caller can hand to the hosted payment page.","triggerScenarios":"finalize-payg-bundle-invoice responds success:false (with or without res.error), or success:true without invoice_id. Fires when the invoice cannot be finalized — e.g. it was already paid/finalized, or required PO/company fields were rejected.","commonSituations":"Re-finalizing an already-finalized or already-paid invoice; Stripe rejecting the finalize call; a function guard rejecting the PO/company input and returning success without invoice_id; concurrent finalize requests.","solutions":["Guard against double-finalize: track invoice status and disable finalize once status === open/paid.","Validate poNumber/companyName/accountName before calling if the function requires them.","Read res.error / function logs to distinguish already-paid from a genuine Stripe failure.","Ensure the function returns a clear error code (e.g. invoice_already_paid) on the failure branch."],"exampleFix":"// edge function — before\nif (invoicePaid) return json({ success: true });\n\n// after\nif (invoicePaid) {\n  return json({ success: false, error: 'invoice_already_paid' });\n}\nawait stripe.invoices.finalizeInvoice(invoiceId);\nreturn json({ success: true, invoice_id: invoiceId });","handlingStrategy":"try-catch","validationCode":"function canFinalizeInvoice(invoice: BundleInvoice | null): boolean {\n  return invoice != null && invoice.status !== \"paid\" && invoice.status !== \"open\";\n}\n\nif (!canFinalizeInvoice(currentInvoice)) {\n  notifications.show({ message: \"Invoice cannot be finalized in its current state.\", color: \"orange\" });\n  return;\n}","typeGuard":"function isFinalizeResponse(res: BundleInvoiceResponse | null): res is BundleInvoiceResponse & { success: true; invoice_id: string } {\n  return res != null && res.success === true && typeof res.invoice_id === \"string\";\n}","tryCatchPattern":"try {\n  await finalizeBundleInvoice({ teamId, quoteId, poNumber });\n} catch (e) {\n  if (e instanceof StripeFunctionError && /already.*paid/i.test(e.message)) {\n    notifications.show({ message: \"This invoice is already paid.\", color: \"orange\" });\n    refreshInvoice();\n  } else throw e;\n}","preventionTips":["Track invoice status and disable finalize once it is open/paid.","Validate poNumber/companyName before calling if the function requires them.","Distinguish already-paid from genuine Stripe errors via the returned error code."],"tags":["billing","stripe","supabase","edge-function","invoice","contract"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}