Skip to content

feat(billing): provider-proxied payment-method + invoices read endpoints (#17)#26

Merged
pedromvgomes merged 1 commit into
mainfrom
feature/billing-read-endpoints
Jun 29, 2026
Merged

feat(billing): provider-proxied payment-method + invoices read endpoints (#17)#26
pedromvgomes merged 1 commit into
mainfrom
feature/billing-read-endpoints

Conversation

@pedromvgomes

Copy link
Copy Markdown
Contributor

Summary

Adds two read-only, provider-proxied billing endpoints on the USER plane so the My Account SPA can render the payment-method card and invoice-history table with real data — while staying PCI SAQ-A (only brand/last4/exp + invoice metadata cross the boundary; never PAN/CVC, and card entry stays on hosted Checkout/Portal).

Closes #17. PR2 of the My Account initiative; builds on the PR1 billing split (#25).

  • GET /v1/tenants/{id}/billing/payment-methodPaymentMethodView or null
  • GET /v1/tenants/{id}/billing/invoices[InvoiceView] (newest first)

Both are owner-checked (require_owner), idempotent reads — no DB writes, no webhook involvement.

Design

Follows the existing two-layer port pattern:

  • StripeGateway (gateway.rs) gains default_payment_method + list_invoices, normalizing Stripe wire format into contract DTOs. A new get_json helper shares a single decode_response tail with post_form, so the PII-safe error contract (never log the raw body — invariant chore(ci): bump actions/checkout from 6.0.3 to 7.0.0 #9) lives in one place. payment-method uses a single expanded customer retrieve; invoices maps the list endpoint (totalamount_cents, createdYYYY-MM-DD, status → agnostic enum), warns on unrecognized statuses rather than silently dropping, and caps the page at 24.
  • BillingPort (ports.rs) gains payment_method + invoices; BillingService is a thin pass-through that returns null/[] for a tenant with no provider customer (never 5xx) without calling Stripe.
  • USER-plane handlers reuse require_owner + state.billing(); utoipa-annotated so they appear in the generated OpenAPI doc.

Wire DTOs (PaymentMethodView, InvoiceView, InvoiceStatus) live in wardnet_common::contract per invariant #21.

Note on the invoice amount shape

The endpoint returns structured amount_cents + currency rather than a pre-formatted "$8.00" string (the issue's literal example), keeping the API locale/provider-agnostic — the SPA formats for display.

Acceptance criteria

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

Tests

  • Gateway (wiremock): expanded-card mapping, no-default → None, invoice list mapping + limit assertion + newest-first, draft skipped, PII-safe error surfacing.
  • HTTP end-to-end: with/without provider customer for both endpoints, and owner-scope 403 for a different tenant's token.

cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace all green.

https://claude.ai/code/session_01DX7SWGnBBRxga9L74hycXB

…nts (#17)

Add two read-only, provider-proxied billing endpoints on the USER plane so the
My Account SPA can render the payment-method card and invoice-history table with
real data, while staying PCI SAQ-A (only brand/last4/exp + invoice metadata cross
the boundary — never PAN/CVC; card entry stays on hosted Checkout/Portal).

- GET /v1/tenants/{id}/billing/payment-method -> PaymentMethodView | null
- GET /v1/tenants/{id}/billing/invoices       -> [InvoiceView] (newest first)

Follows the existing two-layer port pattern:
- StripeGateway (gateway.rs) gains default_payment_method + list_invoices, which
  normalize Stripe wire format into contract DTOs. New get_json helper shares a
  single decode_response tail with post_form so the PII-safe error contract
  (invariant #9) lives in one place. payment-method uses a single expanded
  customer retrieve; invoices maps the list endpoint (total -> amount_cents,
  created -> YYYY-MM-DD, status -> agnostic enum), warns on unrecognized statuses
  rather than silently dropping, and caps the page at 24.
- BillingPort (ports.rs) gains payment_method + invoices; BillingService is a thin
  pass-through that returns null/[] for a tenant with no provider customer (never
  5xx) without calling Stripe.
- USER-plane handlers reuse require_owner + state.billing(); utoipa-annotated.
- Amount is structured (amount_cents + currency), not a pre-formatted string, so
  the API stays locale/provider-agnostic and the SPA formats.

Wire DTOs (PaymentMethodView, InvoiceView, InvoiceStatus) live in
wardnet_common::contract per invariant #21.

Tests: wiremock gateway coverage (expanded card, no-default, invoice mapping +
limit, PII-safe error); HTTP end-to-end for with/without provider customer and
owner-scope 403. fmt + clippy -D warnings + workspace tests green.

Claude-Session: https://claude.ai/code/session_01DX7SWGnBBRxga9L74hycXB
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.94595% with 3 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
source/crates/billing/src/gateway.rs 94.23% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@pedromvgomes pedromvgomes merged commit dbe403c into main Jun 29, 2026
15 checks passed
@pedromvgomes pedromvgomes deleted the feature/billing-read-endpoints branch June 29, 2026 04:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant