feat(billing): provider-proxied payment-method + invoices read endpoints (#17)#26
Merged
Merged
Conversation
…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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-method→PaymentMethodViewornullGET /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) gainsdefault_payment_method+list_invoices, normalizing Stripe wire format into contract DTOs. A newget_jsonhelper shares a singledecode_responsetail withpost_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 (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) gainspayment_method+invoices;BillingServiceis a thin pass-through that returnsnull/[]for a tenant with no provider customer (never 5xx) without calling Stripe.require_owner+state.billing(); utoipa-annotated so they appear in the generated OpenAPI doc.Wire DTOs (
PaymentMethodView,InvoiceView,InvoiceStatus) live inwardnet_common::contractper invariant #21.Note on the invoice amount shape
The endpoint returns structured
amount_cents+currencyrather 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
payment-method→nullandinvoices→[]for a trial tenant with no provider customer (no 5xx)PaymentProviderport gains the two reads; Stripe adapter implemented; no PAN/CVC ever requested, returned, logged, or stored (SAQ-A preserved)Tests
None, invoice list mapping +limitassertion + newest-first, draft skipped, PII-safe error surfacing.cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings, andcargo test --workspaceall green.https://claude.ai/code/session_01DX7SWGnBBRxga9L74hycXB