Invoice generation and retrieval is implemented
Why it matters
Users who cannot retrieve their own invoices open support tickets for every tax filing, expense report, and accounting reconciliation, burning engineering and support cycles on a solved problem. For B2B customers, missing invoice access blocks procurement workflows — finance teams will not pay invoices they cannot download as PDFs with proper tax metadata. This maps directly to user-experience taxons and creates churn risk when customers cannot close their books at month-end.
Severity rationale
Medium because it causes sustained support load and B2B procurement friction without directly exposing data or blocking sign-ups.
Remediation
Add a Stripe Customer Portal session endpoint and link it from the account settings page — the portal handles invoice listing, PDF downloads, and receipts with zero custom UI. Create app/api/billing/portal/route.ts that calls stripe.billingPortal.sessions.create({ customer, return_url }) and redirect the user to portalSession.url.
Detection
-
ID:
invoice-generation -
Severity:
medium -
What to look for: Determine whether users can access their billing history and download invoices. Look for a billing history page, a customer portal integration, or an API endpoint that retrieves invoices from Stripe. Check for Stripe Customer Portal integration (
stripe.billingPortal.sessions.create()), or a custom invoice listing that callsstripe.invoices.list({ customer: customerId }). Also look for whether invoices are stored locally or fetched from Stripe on demand. -
Pass criteria: Count every invoice access mechanism. Users can access their invoice history through at least 1 of: Stripe Customer Portal (which handles this automatically), a custom billing page that fetches invoices from Stripe's API, or a webhook-triggered local store of invoice data. Invoices must be accessible to the user without manual intervention.
-
Fail criteria: No invoice access mechanism exists — users have no way to see or download their billing history. Invoice data is stored locally but never populated (no webhook handler for
invoice.paid). -
Skip (N/A) when: One-time purchases only (no subscription billing where ongoing invoices are generated). Signal: subscription-related dependencies and schema fields are absent.
-
Detail on fail:
"No billing history page or Stripe Customer Portal integration — users cannot access invoices"or"Invoice page exists but fetches from local DB that is never populated — no invoice.paid webhook handler" -
Remediation: The simplest approach is Stripe Customer Portal, which handles invoices, payment method updates, and cancellation without custom code:
// app/api/billing/portal/route.ts export async function POST(req: Request) { const session = await getServerSession() const user = await db.user.findUnique({ where: { id: session.userId } }) const portalSession = await stripe.billingPortal.sessions.create({ customer: user.stripe_customer_id, return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`, }) return Response.json({ url: portalSession.url }) }
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-billing·automated