Stirling-Tools/Stirling-PDF · error · StripeFunctionError

cancel-payg-bundle-quote failed

Error message

cancel-payg-bundle-quote failed

What it means

Fallback thrown by cancelBundleQuote when cancel-payg-bundle-quote returns success === false. Cancellation reverts a quote to an open/editable row and charges nothing (capacity is only credited on invoice.paid), so a failed cancel is surfaced but is often a recoverable race rather than data loss. The comment explicitly names invoice_already_paid as a typical cause.

Source

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

  };
}

/**
 * Cancel an unpaid prepaid-bundle purchase via {@code cancel-payg-bundle-quote}: the edge fn voids the
 * invoice (delete if draft, void if finalized), best-effort cancels the Stripe quote, and voids the quote
 * row so the buyer can start over. Nothing was charged (capacity is credited on invoice.paid), so there's
 * no refund. Throws a StripeFunctionError on failure (e.g. {@code invoice_already_paid}).
 */
export async function cancelBundleQuote(req: {
  teamId: number;
  quoteId: number;
}): Promise<void> {
  const res = await invoke<{ success?: boolean; error?: string }>(
    "cancel-payg-bundle-quote",
    { team_id: req.teamId, quote_id: req.quoteId },
  );
  if (!res.success) {
    throw new StripeFunctionError(
      res.error ?? "cancel-payg-bundle-quote failed",
    );
  }
}

/**
 * Fetch the Stripe-rendered quote PDF for a persisted quote, via the {@code create-payg-bundle-quote}
 * GET route (streams application/pdf). Returns a Blob the caller can object-URL for download.
 */
export async function fetchBundleQuotePdf(quoteId: number): Promise<Blob> {
  ensureSaasSupabase();
  const supabase = getSupabaseClient();
  if (!supabase) {
    throw new StripeFunctionError(
      "SaaS Supabase not configured — set VITE_SUPABASE_URL.",
      "unconfigured",
    );
  }

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Treat cancel failure as expected: catch StripeFunctionError and inspect the code (e.g. invoice_already_paid) before toasting.
  2. Refresh the quote state after a failed cancel — it may already be paid/cancelled.
  3. Disable the Cancel control once the quote is in a non-cancellable status.
  4. Ensure the function returns a stable error code so the UI can branch gracefully.

Example fix

// before
await cancelBundleQuote({ teamId, quoteId });

// after
try {
  await cancelBundleQuote({ teamId, quoteId });
} catch (e) {
  if (e instanceof StripeFunctionError && /already.*paid/i.test(e.message)) {
    notifications.show({ message: "Invoice already paid — cannot cancel.", color: "orange" });
    refreshQuote();
    return;
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

function canCancelQuote(quote: LatestBundleQuote | null): boolean {
  return quote != null && quote.status !== "cancelled" && quote.status !== "paid";
}

if (!canCancelQuote(latestQuote)) {
  notifications.show({ message: "Quote cannot be cancelled.", color: "orange" });
  return;
}

Type guard

function isCancelOk(res: { success?: boolean; error?: string }): boolean {
  return res.success === true;
}

Try / catch

try {
  await cancelBundleQuote({ teamId, quoteId });
} catch (e) {
  if (e instanceof StripeFunctionError && /already.*paid/i.test(e.message)) {
    notifications.show({ message: "Invoice already paid — cannot cancel.", color: "orange" });
    refreshQuote();
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: cancel-payg-bundle-quote responds success:false — most often because the quote's invoice was already paid (you cannot cancel a paid/finalized invoice), or the quote was already cancelled, or the function hit a Stripe error.

Common situations: User clicks Cancel after the invoice was paid in another tab; cancel on a quote whose accept+finalize already completed; concurrent cancel/accept; Stripe-side rejection inside the function.

Related errors


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