Stirling-Tools/Stirling-PDF · error · StripeFunctionError

accept-payg-bundle-quote failed

Error message

accept-payg-bundle-quote failed

What it means

Fallback thrown by acceptBundleStripeQuote when accept-payg-bundle-quote returns success === false OR omits invoice_id. Accepting a quote must raise a Stripe invoice, so a missing invoice_id is a hard failure regardless of the success flag.

Source

Thrown at frontend/editor/src/portal/billing/stripe.ts:390

  error?: string;
}

/**
 * Accept the Stripe quote for a persisted quote row, via {@code accept-payg-bundle-quote}. Acceptance
 * generates the net-terms invoice as a DRAFT (auto_advance off) and returns the hosted URL; the
 * payment step ({@link finalizeBundleInvoice}) stamps the recipient + PO and finalizes it. Payable by
 * card on the hosted page, or by bank transfer / PO. Capacity is credited only on invoice.paid.
 */
export async function acceptBundleStripeQuote(req: {
  teamId: number;
  quoteId: number;
}): Promise<BundleInvoice> {
  const res = await invoke<BundleInvoiceResponse>("accept-payg-bundle-quote", {
    team_id: req.teamId,
    quote_id: req.quoteId,
  });
  if (!res.success || !res.invoice_id) {
    throw new StripeFunctionError(
      res.error ?? "accept-payg-bundle-quote failed",
    );
  }
  return {
    invoiceId: res.invoice_id,
    hostedInvoiceUrl: res.hosted_invoice_url ?? null,
    invoicePdf: res.invoice_pdf ?? null,
    status: res.status ?? null,
  };
}

/**
 * Finalize the accepted bundle invoice (stamping an optional PO), via {@code finalize-payg-bundle-invoice}.
 * Returns the hosted checkout URL + PDF. Called by both Download-invoice and Pay-online; idempotent
 * server-side (an already-finalized invoice comes back as-is, PO locked).
 */
export async function finalizeBundleInvoice(req: {
  teamId: number;

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Refresh the quote state (getLatestBundleQuote) and only show Accept when status is open/valid.
  2. Make accept idempotent on the client: disable the button while the request is in flight and after success.
  3. Inspect res.error / function logs for the real cause (e.g. quote_already_accepted).
  4. Ensure the edge function sets invoice_id on every success path and a meaningful error otherwise.

Example fix

// edge function — before
if (alreadyAccepted) return json({ success: true });

// after
if (alreadyAccepted) {
  return json({ success: false, error: 'quote_already_accepted' });
}
return json({ success: true, invoice_id: invoice.id });
Defensive patterns

Strategy: try-catch

Validate before calling

function canAcceptQuote(quote: LatestBundleQuote | null): boolean {
  return quote != null && quote.status === "open";
}

if (!canAcceptQuote(latestQuote)) {
  notifications.show({ message: "Quote cannot be accepted in its current state.", color: "orange" });
  return;
}

Type guard

function isBundleInvoiceResponse(res: BundleInvoiceResponse | null): res is BundleInvoiceResponse & { success: true; invoice_id: string } {
  return res != null && res.success === true && typeof res.invoice_id === "string";
}

Try / catch

try {
  await acceptBundleStripeQuote({ teamId, quoteId });
} catch (e) {
  if (e instanceof StripeFunctionError && /already.*accept/i.test(e.message)) {
    notifications.show({ message: "This quote was already accepted.", color: "orange" });
    refreshQuote();
  } else throw e;
}

Prevention

When it happens

Trigger: accept-payg-bundle-quote responds success:false (with res.error, or this fallback if error omitted), or success:true without invoice_id. Common when the quote was already accepted/cancelled/expired, or Stripe invoice creation failed inside the function.

Common situations: Double-accept of the same quote (concurrent clicks / page reload); quote in a non-open status; Stripe error creating the invoice; function bug returning success without the invoice handle.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/69959c487026404b. Report an issue: GitHub.