Skip to content

PR2 — Provider-proxied billing read endpoints (payment-method, invoices) #17

Description

@pedromvgomes

Summary

Add two read-only, provider-proxied billing endpoints so the SPA can render the prototype's payment-method card and invoice history table with real data — while staying PCI SAQ-A (we never touch PAN/CVC; card entry/update remains hosted Stripe Checkout/Portal redirects).

This is PR2 of the My Account initiative. Depends on PR1 (the billing crate + PaymentProvider port must exist).

Why

The prototype shows a payment-method card (Visa •••• 4242, exp 08/27) and an invoice table. last4, brand, exp_month/year, and invoice records are not PAN/CVC, so reading them back from the provider is outside PCI scope. Today there is no API for them, so the SPA can't render those sections for real. Card entry still goes only through hosted Checkout/Portal — so we add reads, not writes.

Scope

Add to the billing crate, exposed on the USER plane (owner-checked, same auth as the other /v1/tenants/{id}/... routes):

GET /v1/tenants/{id}/billing/payment-method

  • Returns the default payment method summary or null (e.g. trial with no card):
    { "brand": "visa", "last4": "4242", "exp_month": 8, "exp_year": 2027 }
  • Backed by a new method on the PaymentProvider port (Stripe adapter: retrieve the customer's default payment method). Returns null when the tenant has no provider customer yet.

GET /v1/tenants/{id}/billing/invoices

  • Returns recent invoices, newest first:
    [ { "date": "2026-06-01", "amount": "$8.00", "status": "paid", "hosted_url": "https://..." } ]
  • status ∈ provider-agnostic set (paid | open | void | uncollectible — map provider values). hosted_url is the provider-hosted invoice/receipt link (the download action). Empty array when no customer/invoices.
  • Backed by a new PaymentProvider method (Stripe adapter: list invoices for the customer).

Notes

  • Extend the PaymentProvider port (provider-agnostic DTOs in billing), with the Stripe adapter implementing the two new reads via the existing hand-rolled reqwest client pattern (no new SDK).
  • Both endpoints are idempotent reads; no DB writes, no webhook involvement.
  • Keep responses SPA-shaped (provider-agnostic), not raw Stripe objects.

Acceptance criteria

  • Both endpoints exist on the USER plane, owner-checked; return the shapes above.
  • payment-method returns null and invoices returns [] for a trial tenant with no provider customer (no 5xx).
  • PaymentProvider port gains the two reads; Stripe adapter implemented; no PAN/CVC is ever requested, returned, logged, or stored (SAQ-A preserved).
  • Tests cover both happy-path and no-customer cases, with the provider mocked (wiremock, consistent with the existing Stripe-gateway tests).
  • utoipa annotations added so the endpoints appear in the generated OpenAPI doc.

References

  • PaymentProvider port + Stripe adapter (from PR1, formerly crates/tenants/src/stripe.rs).
  • Prototype billing sections: paymentMethodCard() and billingHistory() in the design HTML.
  • Part of the My Account initiative (refines WS-H: My Account SPA (site/) + account API #1). Blocked by PR1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestrustPull requests that update rust code

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions