Invoice detail pages omit payments, cards and the subscription #150

Open
opened 2026-09-28 03:52:56 +00:00 by cgalo5758 · 0 comments
Owner

The console stores each invoice's lines, the subscription it bills, and one payment row per Stripe charge with the card used. The two invoice detail pages show little of this, and the member page cannot reach Stripe's own invoice.

What the pages lack

  • Operator invoice detail (operator_billing_invoice_detail.html, OperatorInvoiceDetailData in operator_billing.go) ends at the line items. It shows no payments, no card and no subscription. The operator-billing-views spec already requires Dashboard links for payments and subscriptions, and no template renders them.
  • Member invoice detail (member_invoice_detail.html, MemberPaymentViewModel in member_invoices.go) shows each payment's date, amount and status only. It has no card and no subscription, and the Stripe invoice number ("Stripe invoice ABCD1234-0007") is plain text.
  • Void date. Neither page shows when an invoice was voided, although voided_at is stored.
  • Period. Both detail pages and the member list show the invoice's period_start–period_end. Stripe documents that these are not the service period ("Use the line item period to get the service period", invoice object). A first invoice or an upgrade therefore reads "Sep 27 – Sep 27". Each line's own period is stored; the operator page shows it and the member page does not. The member-invoice-history spec asks for a billing period, so the fix changes that spec too.
  • Mobile. At 390 px the operator line-item table scrolls sideways and hides Amount and Period.

Parity with Stripe's invoice

Stripe issues every invoice today, so its hosted page and PDF are the invoice of record and the console page is a view of it. The pages should link to Stripe's document rather than rebuild it, and show what a member or operator acts on.

  • Member link. Make the Stripe invoice number a link to a console route. The route checks that the invoice belongs to the member (the same gate as the detail page), reads the invoice from Stripe and redirects to its hosted_invoice_url.
    • The URL changes on every read and expires 30 days after the due date, never later than 120 days, so it is read on demand. It is never stored or logged: anyone holding it can view and pay the invoice.
    • A paid invoice's hosted page offers the invoice and the receipt for download. An open invoice's page takes payment, which today is the member's only way to pay an unpaid invoice (#148).
    • There is no link when the invoice's recorded environment differs from the key's (#134).
    • Relates to #67 (downloading an invoice) and #13 (the name heading Stripe's pages).
  • Operator link. "View in Stripe" exists but builds its Dashboard URL from the key's mode, so a test invoice shown under a live key links to the live Dashboard. Build it from the invoice's recorded environment, and make the number itself the link.
  • Issue date. Show it on both pages. It is Stripe's effective_at, the PDF's "Date of issue".

Failed attempts

In Stripe's model the payment is the payment intent, and each charge is one try at it (one InvoicePayment per payment intent in later API versions). A declined try belongs to the invoice through its payment intent, but it never counts as paid and Stripe never shows it to the customer:

  • the hosted page lists only successful payments;
  • a voided invoice whose only try was declined reads "No payments have been made";
  • the PDF lists no payments;
  • no receipt is sent.

Chargebee, Recurly, Kill Bill and Lago work the same way: operators see every attempt; customers and "amount paid" see only successes. The member page today lists a decline as a payment and counts it ("2 payments").

  • Member. List succeeded payments with their card. An open invoice whose last try failed shows one line with that try's date and card. Auto-charged invoices have no due date and so are never marked overdue, which makes this line the only sign on the page that collection failed.
  • Operator. List every try with its date, amount, card, outcome and decline reason, and for an open invoice the next retry. The decline reason needs failure_code and failure_message on payments. design/data-model.md already has both, and the reconcile already reads the charge that carries them.
  • Elsewhere. The operator activity feed files a failed row under payment_received; relabel it. Add a status facet to the Payments list.

Out of scope here: discounts (#25), credit balance (#27), refunds and disputes (#106).

The console stores each invoice's lines, the subscription it bills, and one payment row per Stripe charge with the card used. The two invoice detail pages show little of this, and the member page cannot reach Stripe's own invoice. ## What the pages lack - **Operator invoice detail** (`operator_billing_invoice_detail.html`, `OperatorInvoiceDetailData` in `operator_billing.go`) ends at the line items. It shows no payments, no card and no subscription. The `operator-billing-views` spec already requires Dashboard links for payments and subscriptions, and no template renders them. - **Member invoice detail** (`member_invoice_detail.html`, `MemberPaymentViewModel` in `member_invoices.go`) shows each payment's date, amount and status only. It has no card and no subscription, and the Stripe invoice number ("Stripe invoice ABCD1234-0007") is plain text. - **Void date.** Neither page shows when an invoice was voided, although `voided_at` is stored. - **Period.** Both detail pages and the member list show the invoice's `period_start`–`period_end`. Stripe documents that these are not the service period ("Use the line item period to get the service period", invoice object). A first invoice or an upgrade therefore reads "Sep 27 – Sep 27". Each line's own period is stored; the operator page shows it and the member page does not. The `member-invoice-history` spec asks for a billing period, so the fix changes that spec too. - **Mobile.** At 390 px the operator line-item table scrolls sideways and hides Amount and Period. ## Parity with Stripe's invoice Stripe issues every invoice today, so its hosted page and PDF are the invoice of record and the console page is a view of it. The pages should link to Stripe's document rather than rebuild it, and show what a member or operator acts on. - **Member link.** Make the Stripe invoice number a link to a console route. The route checks that the invoice belongs to the member (the same gate as the detail page), reads the invoice from Stripe and redirects to its `hosted_invoice_url`. - The URL changes on every read and expires 30 days after the due date, never later than 120 days, so it is read on demand. It is never stored or logged: anyone holding it can view and pay the invoice. - A paid invoice's hosted page offers the invoice and the receipt for download. An open invoice's page takes payment, which today is the member's only way to pay an unpaid invoice (#148). - There is no link when the invoice's recorded environment differs from the key's (#134). - Relates to #67 (downloading an invoice) and #13 (the name heading Stripe's pages). - **Operator link.** "View in Stripe" exists but builds its Dashboard URL from the key's mode, so a test invoice shown under a live key links to the live Dashboard. Build it from the invoice's recorded environment, and make the number itself the link. - **Issue date.** Show it on both pages. It is Stripe's `effective_at`, the PDF's "Date of issue". ## Failed attempts In Stripe's model the payment is the payment intent, and each charge is one try at it (one `InvoicePayment` per payment intent in later API versions). A declined try belongs to the invoice through its payment intent, but it never counts as paid and Stripe never shows it to the customer: - the hosted page lists only successful payments; - a voided invoice whose only try was declined reads "No payments have been made"; - the PDF lists no payments; - no receipt is sent. Chargebee, Recurly, Kill Bill and Lago work the same way: operators see every attempt; customers and "amount paid" see only successes. The member page today lists a decline as a payment and counts it ("2 payments"). - **Member.** List succeeded payments with their card. An open invoice whose last try failed shows one line with that try's date and card. Auto-charged invoices have no due date and so are never marked overdue, which makes this line the only sign on the page that collection failed. - **Operator.** List every try with its date, amount, card, outcome and decline reason, and for an open invoice the next retry. The decline reason needs `failure_code` and `failure_message` on payments. `design/data-model.md` already has both, and the reconcile already reads the charge that carries them. - **Elsewhere.** The operator activity feed files a failed row under `payment_received`; relabel it. Add a status facet to the Payments list. Out of scope here: discounts (#25), credit balance (#27), refunds and disputes (#106).
cgalo5758 added the
kind
bug
area/billingarea/operator-uiarea/member-ui
labels 2026-09-28 03:52:56 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wiki-cafe/member-console#150