From 1af369b385e76f3e59f22d130aca19338613e097 Mon Sep 17 00:00:00 2001 From: midagedev Date: Sat, 29 Aug 2026 15:58:51 +0900 Subject: [PATCH 1/5] Refresh Stripe coverage baselines to 175/587 Re-measured the OpenAPI inventory against the pinned 2026-04-22.dahlia snapshot (stripe/openapi v2261): 175/587 operations, 29.8% L1+. Updates README, the 90% target family table, tracking docs, and gate evidence; documents the tax-rate/tax-ID and invoice-depth surface in COMPATIBILITY.md; and corrects stale subscription-item ID risk text in the claim registry. --- README.md | 6 ++--- docs/API_VALIDATION_AND_ERROR_SIMULATION.md | 2 +- docs/COMPATIBILITY.md | 14 +++++----- docs/COMPATIBILITY_TRACKING.md | 8 ++++-- docs/GATE_STATUS.md | 22 ++++++++------- docs/PUBLIC_RELEASE_READINESS.md | 6 ++--- docs/SIMULATION_CAPACITY_BACKLOG.md | 8 +++--- docs/STRIPE_API_COMPATIBILITY_ROADMAP.md | 14 +++++----- docs/STRIPE_COMPATIBILITY_90_TARGET.md | 30 ++++++++++++++------- internal/stripecompat/registry.go | 4 +-- 10 files changed, 66 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 67723a2..f04482a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ behavior outside the published contract. | Surface | What it is for | | --- | --- | -| Stripe-like API | Local customers, products, prices, coupons, promotion codes, checkout sessions, subscriptions, schedules, invoices, payment intents, cash balance, refunds, credit notes, disputes, test clocks, webhook endpoints, and events for supported billing flows. | +| Stripe-like API | Local customers, products, prices, coupons, promotion codes, tax rates, checkout sessions, subscriptions, schedules, invoices, payment intents, cash balance, refunds, credit notes, disputes, test clocks, webhook endpoints, and events for supported billing flows. | | Hosted checkout | Browser-visible sandbox checkout for exercising app integration and deterministic payment outcomes. | | Billing portal | Local customer portal for plan changes, seats, cancellation, resume, and payment-method update flows. | | Developer dashboard | Billing objects, timeline, webhook delivery attempts, app responses, and debug bundle export in one place. | @@ -327,13 +327,13 @@ curl -fsS "http://localhost:8080/api/diagnostics?limit=100" \ | --- | --- | --- | | Runtime | Go server with SQLite local default | In-memory storage exists for tests | | Frontend | React checkout, portal, and dashboard apps | Built with Vite into `dist/app` | -| Stripe-like API | Practical local subset | Customers, catalog, checkout, portal sessions, subscriptions, schedules, invoices, payment intents, cash balance, refunds, credit notes, disputes, test clocks, webhook endpoints, events, search/list projections used by tests | +| Stripe-like API | Practical local subset | Customers, catalog, checkout, portal sessions, subscriptions, schedules, invoices, payment intents, cash balance, refunds, credit notes, disputes, tax rates, test clocks, webhook endpoints, events, search/list projections used by tests | | Webhooks | Signed delivery with reliability controls | Retry, duplicate, delay, out-of-order, grouped replay, endpoint attempts, delivery evidence, redaction | | Scenarios | YAML runner | Local clock, app assertions, JSON/Markdown reports, exit-code policy | | Fixtures | Apply/snapshot/assert APIs | JSON/YAML input, fixture metadata isolation, structured pass/fail reports | | SaaS profile | Generic workspace billing profile | Plans, seats, members, export quota, extra export, payment history, support bundle, platform/connect-style webhook evidence | | Release state | Source plus GHCR image | Local Docker image builds and GHCR image workflow; no package/Homebrew/signed binary yet | -| Stripe API inventory | `160 / 587` operations, `27.3%` L1+ | OpenAPI route inventory is schema-visible for all `587` operations; implemented coverage is tracked in `docs/STRIPE_COMPATIBILITY_90_TARGET.md` | +| Stripe API inventory | `175 / 587` operations, `29.8%` L1+ | OpenAPI route inventory is schema-visible for all `587` operations; implemented coverage is tracked in `docs/STRIPE_COMPATIBILITY_90_TARGET.md` | Detailed compatibility matrix: `docs/COMPATIBILITY.md`. diff --git a/docs/API_VALIDATION_AND_ERROR_SIMULATION.md b/docs/API_VALIDATION_AND_ERROR_SIMULATION.md index 0702445..8e25961 100644 --- a/docs/API_VALIDATION_AND_ERROR_SIMULATION.md +++ b/docs/API_VALIDATION_AND_ERROR_SIMULATION.md @@ -208,7 +208,7 @@ Current lane: - `go run ./cmd/billtap compatibility scorecard --output-dir dist/compatibility` runs the offline Billtap-owned corpus and writes JSON, Markdown, and replay bundle artifacts without requiring external Stripe services. -- `l3-public-readiness-v7` currently has 49 release-blocking cases covering the +- `l3-public-readiness-v7` currently has 50 release-blocking cases covering the public subset's request validation, protocol parameter acceptance, OpenAPI-backed fallback validation, idempotency mismatch, deterministic checkout payment-error aliases, and direct PaymentIntent/SetupIntent state diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 31784e2..e49a119 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -102,21 +102,23 @@ traffic can keep using the unprefixed service URL. | Prices | `POST /v1/prices`, `GET /v1/prices`, `GET /v1/prices/{id}`, `POST /v1/prices/{id}`, `GET /v1/prices/search` | Supported | Create, list, retrieve, update, and search prices. Supports `product`, `currency`, `unit_amount`, `lookup_key`, recurring interval fields, `active`, and metadata. List supports `product`, `active`, `type=recurring`, and `type=one_time`. Search supports a measured Stripe Search Query Language subset for `active`, `type`, `lookup_key`, and metadata equality clauses joined by `AND`, including one-time price lookup by `lookup_key`. | | Coupons | `POST /v1/coupons`, `GET /v1/coupons`, `GET /v1/coupons/{id}`, `POST /v1/coupons/{id}`, `DELETE /v1/coupons/{id}` | Partial | Create/list/retrieve local coupon evidence for SDK and fixture smoke paths. Amount-off, percent-off, duration, metadata, and deletion markers are modeled. A bounded single-discount subset applies coupons to customer defaults, checkout sessions, subscriptions, invoice preview math, and renewal invoices; redemption limits and expiration analytics are not modeled. | | Promotion codes | `POST /v1/promotion_codes`, `GET /v1/promotion_codes`, `GET /v1/promotion_codes/{id}`, `POST /v1/promotion_codes/{id}` | Partial | Create/list/retrieve local promotion-code evidence linked to a local coupon. List supports `code`, `coupon`, `customer`, and `active` filters. Promotion codes can be applied through `discounts[0][promotion_code]`; minimum amounts, expiration rules, and promotion analytics are not modeled. | -| Checkout sessions | `POST /v1/checkout/sessions`, `GET /v1/checkout/sessions`, `GET /v1/checkout/sessions/{id}` | Supported | Creates subscription-mode sandbox checkout sessions from request line items and hosted Billtap URLs. The Stripe-style session response leaves `line_items` unexpanded. Accepts Stripe SDK form params `allow_promotion_codes`, `discounts[0][coupon]`, `discounts[0][promotion_code]`, `subscription_data[trial_period_days]`, and `subscription_data[default_tax_rates]` (applied to session totals; exclusive/inclusive snapshots; mutually exclusive with `automatic_tax`); trial checkout creates local `trialing` subscription evidence. Hosted URLs use the request host by default, or `BILLTAP_PUBLIC_BASE_URL` when configured for container-to-host browser flows. `PUBLIC_BASE_PATH`, `BILLTAP_PUBLIC_BASE_PATH`, and `X-Forwarded-Prefix` are reflected in hosted browser URLs. | +| Tax rates | `GET/POST /v1/tax_rates`, `GET/POST /v1/tax_rates/{id}` | Partial | Create/list/retrieve/update local tax-rate evidence. Checkout `subscription_data[default_tax_rates][]` and subscription `default_tax_rates` resolve `txr_*` IDs and snapshot inclusive/exclusive rates onto sessions, subscriptions, invoices, renewal/proration math, and previews, applied after discounts; `automatic_tax` is mutually exclusive. Fixture packs can seed tax rates with explicit IDs. Jurisdiction and address-based calculation are not modeled; `automatic_tax` remains a customer-metadata `tax_percent` simulation. | +| Customer tax IDs | `GET/POST /v1/customers/{id}/tax_ids`, `GET/DELETE /v1/customers/{id}/tax_ids/{id}` | Partial | Local customer tax-ID evidence store backing checkout `tax_id_collection[enabled]` flows. Provider verification is not modeled. | +| Checkout sessions | `POST /v1/checkout/sessions`, `GET /v1/checkout/sessions`, `GET /v1/checkout/sessions/{id}`, `POST /v1/checkout/sessions/{id}/expire` | Supported | Creates subscription- and payment-mode sandbox checkout sessions from request line items and hosted Billtap URLs; `setup` mode is rejected. The Stripe-style session response leaves `line_items` unexpanded. Accepts Stripe SDK form params `allow_promotion_codes`, `discounts[0][coupon]`, `discounts[0][promotion_code]`, `subscription_data[trial_period_days]`, and `subscription_data[default_tax_rates]` (applied to session totals; exclusive/inclusive snapshots; mutually exclusive with `automatic_tax`); trial checkout creates local `trialing` subscription evidence. Payment-mode sessions accept inline `price_data` line items (creating local product/price evidence), `payment_intent_data[...]`, `client_reference_id`, session-level `metadata[...]`, `automatic_tax[enabled]`, and `tax_id_collection[enabled]` (tax snapshot from customer metadata `tax_percent`), and complete into a one-time PaymentIntent without subscription or invoice evidence. `expire` moves an open session to `expired` and emits `checkout.session.expired`; non-open sessions return `invalid_request_error`. Hosted URLs use the request host by default, or `BILLTAP_PUBLIC_BASE_URL` when configured for container-to-host browser flows. `PUBLIC_BASE_PATH`, `BILLTAP_PUBLIC_BASE_PATH`, and `X-Forwarded-Prefix` are reflected in hosted browser URLs. | | Checkout completion | `POST /v1/checkout/sessions/{id}/complete`, `POST /api/checkout/sessions/{id}/complete`, `POST/DELETE /api/checkout/sessions/{id}/promotion_code` | Billtap-specific | Completes a sandbox checkout and creates subscription, invoice, payment intent, timeline, and checkout webhook evidence. Supports success plus deterministic failure aliases such as `card_declined`, `insufficient_funds`, `expired_card`, `incorrect_cvc`, `processing_error`, `authentication_required`, `payment_pending`, `canceled`, and documented Stripe test PaymentMethod IDs such as `pm_card_visa_chargeDeclined`. Hosted checkout can apply or remove a promotion code on an open `allow_promotion_codes` session via `POST/DELETE /api/checkout/sessions/{id}/promotion_code` (form `promotion_code=`), refreshing session totals before completion. | | Billing portal sessions | `POST /v1/billing_portal/sessions` | Partial | Returns a Stripe-like `billing_portal.session` object and Billtap portal URL for a known customer, including `flow`, `locale`, `on_behalf_of`, `return_url`, and `livemode` fields. Accepts `customer`, `return_url`, optional `configuration`, `locale`, `on_behalf_of`, and `flow_data`, with enum and required nested-field validation for the modeled portal flows. The hosted portal can simulate saved payment-method updates and subscription cancellation, then redirect to `return_url`; full Stripe-hosted portal configuration rendering is not modeled. | | Subscriptions | `POST /v1/subscriptions`, `GET /v1/subscriptions`, `GET /v1/subscriptions/search`, `GET /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}`, `DELETE /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}/resume`, nested `GET/POST/DELETE /v1/customers/{id}/subscriptions...`, `GET/DELETE /v1/subscriptions/{id}/discount` | Partial | Create/list/retrieve subscriptions through the local checkout-completion state path. Update supports item replacement, metadata merge, single coupon or promotion-code discount application, `test_clock`, `cancel_at_period_end`, `pause_collection`, and accepted billing/proration evidence including `proration_date`, `proration_behavior`, `payment_behavior`, `billing_cycle_anchor`, and `trial_end`; resume clears local pause evidence and emits `customer.subscription.updated`. Top-level and nested customer lists support status and metadata equality filters such as `metadata[billtap_fixture_ref]`. Search supports `id`, `customer`, `status`, and metadata equality clauses joined by `AND`. Delete performs immediate sandbox cancellation. Test-clock and scenario clock advances can activate due trials, renew active periods with bounded discount math, fail configured renewals from subscription or customer metadata, and cancel period-end subscriptions in the local billing graph. | | Subscription schedules | `POST /v1/subscription_schedules`, `GET /v1/subscription_schedules`, `GET /v1/subscription_schedules/{id}`, `POST /v1/subscription_schedules/{id}`, `POST /v1/subscription_schedules/{id}/cancel`, `POST /v1/subscription_schedules/{id}/release` | Partial | Local schedule evidence for one due phase on an existing subscription. Test-clock advance applies a due phase by replacing subscription items and emits `customer.subscription.updated`; complex multi-phase billing, proration, invoices, and schedule lifecycle parity are not modeled. | -| Subscription items | `POST /v1/subscription_items`, `DELETE /v1/subscription_items/{id}` | Partial | Add or remove local subscription items for integration smoke paths. Billing proration and invoice recalculation are not modeled. | -| Invoices | `POST /v1/invoices`, `GET /v1/invoices`, `GET /v1/invoices/search`, `GET /v1/invoices/{id}`, `POST /v1/invoices/{id}/finalize`, `POST /v1/invoices/{id}/pay`, `GET /v1/invoices/{id}/lines`, `GET /v1/invoices/{id}/payments`, `POST /v1/invoices/create_preview`, `GET/POST /v1/invoices/upcoming` | Partial | List/retrieve/search invoices created by checkout and create a bounded manual one-time invoice flow. Search supports `id`, `customer`, `subscription`, and `status` clauses joined by `AND`. `pay` retries open checkout invoices with deterministic sandbox `payment_method` or `source` aliases, mutating invoice, subscription, payment-intent, timeline, and webhook evidence. Manual invoices accept `customer`, `currency`, `collection_method`, `default_payment_method`, `description`, `auto_advance=false`, `pending_invoice_items_behavior`, `payment_settings[payment_method_types]`, and `metadata[...]`; `finalize` creates local PaymentIntent evidence, and `pay` applies per-intent or customer-level default outcomes including success, card decline, and `requires_action`. Invoice responses include `confirmation_secret`, local hosted/PDF paths, `payments.data.payment.payment_intent`, metadata, status transitions, automatic tax state, tax totals, payment settings, and array-shaped `discounts`. Preview endpoints calculate a Stripe-shaped subscription-update proration line from current period bounds, old/new price totals, `subscription_details[items]`, `subscription_details[proration_date]`, `subscription_details[proration_behavior]`, and `subscription_details[billing_cycle_anchor]`. A bounded single-discount subset updates `subtotal`, `total`, and `total_discount_amounts`; taxes, invoice rendering, automatic collection, and full dunning lifecycle are outside the modeled subset. | -| Invoice items | `POST /v1/invoiceitems`, `GET /v1/invoiceitems`, `GET /v1/invoiceitems/{id}` | Partial | Creates and lists local invoice item evidence for manual one-time invoices. `customer`, `invoice`, `amount`, `currency`, `description`, and `metadata[...]` are accepted. Tax, discount, pending invoice-item collection, and line mutation parity are not modeled. | +| Subscription items | `POST /v1/subscription_items`, `DELETE /v1/subscription_items/{id}` | Partial | Add or remove local subscription items. Create and delete accept `proration_behavior` (`none`/`create_prorations`/`always_invoice`) and `proration_date` through the same proration path as subscription update, applying the subscription's `default_tax_rates` after discounts: `always_invoice` issues a paid `subscription_update` invoice and repoints `latest_invoice`, and `create_prorations` defers the delta to the next renewal invoice. Deleting the last item is rejected. Item IDs are stored at creation so deletes no longer shift later IDs. Item-level `tax_rates` are evidence-only, and metered usage is not modeled. | +| Invoices | `POST /v1/invoices`, `GET /v1/invoices`, `GET /v1/invoices/search`, `GET /v1/invoices/{id}`, `POST /v1/invoices/{id}/finalize`, `POST /v1/invoices/{id}/pay`, `POST /v1/invoices/{id}/send`, `POST /v1/invoices/{id}/void`, `POST /v1/invoices/{id}/mark_uncollectible`, `GET /v1/invoices/{id}/lines`, `GET /v1/invoices/{id}/payments`, `POST /v1/invoices/create_preview`, `GET/POST /v1/invoices/upcoming` | Partial | List/retrieve/search invoices created by checkout and create a bounded manual one-time invoice flow. Search supports `id`, `customer`, `subscription`, and `status` clauses joined by `AND`. `pay` retries open checkout invoices with deterministic sandbox `payment_method` or `source` aliases, mutating invoice, subscription, payment-intent, timeline, and webhook evidence. Manual invoices accept `customer`, `currency`, `collection_method`, `default_payment_method`, `description`, `auto_advance=false`, `pending_invoice_items_behavior`, `payment_settings[payment_method_types]`, and `metadata[...]`; `pending_invoice_items_behavior=include` attaches the customer's same-currency pending invoice items once at create. `finalize` creates local PaymentIntent evidence, and `pay` applies per-intent or customer-level default outcomes including success, card decline, and `requires_action`. `send` records local email evidence and emits `invoice.sent`; `void` moves an `open` invoice to `void` and emits `invoice.voided`; `mark_uncollectible` moves an `open` invoice to `uncollectible` and emits `invoice.marked_uncollectible`; non-open statuses return `invalid_request_error`. Invoice responses include `confirmation_secret`, local hosted/PDF paths, `payments.data.payment.payment_intent`, metadata, status transitions, automatic tax state, tax totals, payment settings, array-shaped `discounts`, `lines.data` serialized like the lines endpoint, and `post_payment_credit_notes_amount` on paid invoices. Preview endpoints calculate a Stripe-shaped subscription-update proration line from current period bounds, old/new price totals, `subscription_details[items]`, `subscription_details[proration_date]`, `subscription_details[proration_behavior]`, and `subscription_details[billing_cycle_anchor]`; previews without item overrides return the subscription's next billing cycle (items, discounts, and tax with `billing_reason: upcoming`) matching the next renewal invoice. A bounded single-discount subset updates `subtotal`, `total`, and `total_discount_amounts`; invoice rendering, automatic collection, and full dunning lifecycle are outside the modeled subset. | +| Invoice items | `POST /v1/invoiceitems`, `GET /v1/invoiceitems`, `GET /v1/invoiceitems/{id}` | Partial | Creates, lists, and retrieves local invoice item evidence for manual one-time invoices. `customer`, `invoice`, `amount`, `currency`, `description`, and `metadata[...]` are accepted, or `pricing[price]` plus `quantity` instead of `amount` (line amount is the price's `unit_amount` times `quantity`, currency from the price). Omitting `invoice` stores the item as a pending customer item that `POST /v1/invoices` with `pending_invoice_items_behavior=include` attaches; `subscription` is accepted and echoed. Tax, discount, and full line mutation parity are not modeled. | | Payment intents | `POST /v1/payment_intents`, `GET /v1/payment_intents`, `GET /v1/payment_intents/search`, `GET /v1/payment_intents/{id}`, `POST /v1/payment_intents/{id}/confirm`, `POST /v1/payment_intents/{id}/capture`, `POST /v1/payment_intents/{id}/cancel` | Partial | Create/list/retrieve/search and mutate local payment intents. Search supports `id`, `customer`, `status`, and metadata equality clauses joined by `AND`. `confirm` supports deterministic sandbox PaymentMethod aliases such as `pm_card_visa`, `pm_card_visa_chargeDeclined`, `pm_card_threeDSecure2Required`, `requires_action`, and local bank-transfer processing. Manual capture moves through `requires_capture` before `capture` succeeds. Direct one-time intents emit `payment_intent.created` and a terminal event when confirmed. Deferred per-intent outcomes and customer defaults are supported; `requires_action` can return either `use_stripe_sdk` or local `redirect_to_url` next-action payloads. | | Setup intents | `POST /v1/setup_intents`, `GET /v1/setup_intents`, `GET /v1/setup_intents/{id}`, `POST /v1/setup_intents/{id}/confirm`, `POST /v1/setup_intents/{id}/cancel` | Partial | Create/list/retrieve and mutate local setup intents with deterministic success, decline, and authentication-required aliases. Mandates, bank-account verification, and full SCA behavior are not modeled. | | Payment methods | `GET/POST /v1/payment_methods`, `GET/POST /v1/payment_methods/{id}`, `POST /v1/payment_methods/{id}/attach`, `POST /v1/payment_methods/{id}/detach`, `GET /v1/customers/{id}/payment_methods?type=card`, `GET /v1/customers/{id}/payment_methods/{id}` | Partial | Returns deterministic sandbox card PaymentMethod projections for known customers, including SDK-friendly `billing_details`, `card.checks`, `country`, `fingerprint`, `funding`, `networks`, `three_d_secure_usage`, `metadata`, and `redaction` fields. Query validation covers `type`, `allow_redisplay`, `limit`, and unknown parameters; non-card valid types return an empty local list. Customer fixtures can opt into empty or explicit payment-method lists for no-card and multi-card local scenarios. Create/attach/detach mutate local customer payment-method metadata and reject real card data; general update is accepted as a compatibility no-op for projected local cards. | | Customer cash balance | `GET /v1/customers/{id}/cash_balance`, `POST /v1/customers/{id}/cash_balance`, `GET /v1/customers/{id}/cash_balance_transactions`, `GET /v1/customers/{id}/cash_balance_transactions/{id}`, `POST /v1/test_helpers/customers/{id}/fund_cash_balance` | Partial | Local cash-balance evidence for bank-transfer smoke tests. The test-helper funding endpoint records a cash-balance transaction and settles processing bank-transfer PaymentIntents for that customer. Banking rails, reconciliation, currency conversion, and customer-balance invoice application are not modeled. | | Connect platform evidence | `GET /v1/account`, `POST /v1/accounts`, `GET /v1/accounts`, `GET /v1/accounts/{id}`, `POST /v1/accounts/{id}`, `DELETE /v1/accounts/{id}`, `POST /v1/account_links`, `POST /v1/account_sessions`, account capabilities, people/persons, external accounts, transfers/reversals, payouts, application fees/refunds | Partial | Persist local connected-account profiles, capability status, person evidence, bank-account evidence, transfers, transfer reversals, payouts, and application-fee refunds. Account links, account sessions, and login links return local URLs/client secrets. Account deletion returns a local deletion marker. Request traces preserve `Stripe-Account` routing evidence, and local Connect evidence can emit `transfer.*`, `payout.*`, and `application_fee.refunded` webhooks. KYC, identity verification, bank verification, real onboarding, balance movement, account closure, and settlement behavior are not modeled. | -| Refunds | `POST /v1/refunds`, `GET /v1/refunds`, `GET/POST /v1/refunds/{id}`, `POST /v1/refunds/{id}/cancel` | Partial | Create/list/retrieve/update local refund evidence against an invoice, payment intent, or charge-like ID. Creation emits `charge.refunded` and `charge.refund.updated`; status updates and test-clock settlement emit `charge.refund.updated`. Refund fixtures can seed `pending` and settle on `settle_at`/`available_on`. Balance transactions and processor accounting are outside the modeled subset. | -| Credit notes | `POST /v1/credit_notes`, `GET /v1/credit_notes`, `GET /v1/credit_notes/{id}`, `POST /v1/credit_notes/{id}/void` | Partial | Create/list/retrieve/void local credit note evidence for an invoice and emit `credit_note.created` / `credit_note.voided`. Line-level tax, discount, PDF, and customer-balance math are outside the modeled subset. | +| Refunds | `POST /v1/refunds`, `GET /v1/refunds`, `GET/POST /v1/refunds/{id}`, `POST /v1/refunds/{id}/cancel` | Partial | Create/list/retrieve/update local refund evidence against an invoice, payment intent, or charge-like ID. Omitting `amount` refunds the full remaining refundable balance on the invoice or payment intent; requested amounts cannot exceed that balance. Creation emits `charge.refunded` and `charge.refund.updated`; status updates and test-clock settlement emit `charge.refund.updated`. Refund fixtures can seed `pending` and settle on `settle_at`/`available_on`. Balance transactions and processor accounting are outside the modeled subset. | +| Credit notes | `POST /v1/credit_notes`, `GET /v1/credit_notes`, `GET /v1/credit_notes/{id}`, `POST /v1/credit_notes/{id}/void` | Partial | Create/list/retrieve/void local credit note evidence for an invoice and emit `credit_note.created` / `credit_note.voided`. `memo`, `out_of_band_amount`, and `refund_amount` are accepted, persisted, and echoed with derived `credit_amount`; `out_of_band_amount` is external settlement and does not change customer cash balance. Line-level tax, discount, PDF, and customer-balance math are outside the modeled subset. | | Disputes | `GET /v1/disputes`, `GET /v1/disputes/{id}`, `POST /v1/disputes/{id}`, `POST /v1/disputes/{id}/close`, `GET /v1/charges/{id}/dispute`, `POST /v1/charges/{id}/dispute` | Partial | Create, retrieve, list, update evidence, and close local dispute evidence for chargeback-style tests. Fixture seeding and APIs emit `charge.dispute.created`, `charge.dispute.updated`, `charge.dispute.funds_withdrawn`, and `charge.dispute.closed` where applicable. Representment deadlines, balance movements, and processor outcomes are outside the modeled subset. | | Test clocks | `POST /v1/test_helpers/test_clocks`, `GET /v1/test_helpers/test_clocks`, `GET /v1/test_helpers/test_clocks/{id}`, `POST /v1/test_helpers/test_clocks/{id}/advance` | Partial | Create/retrieve/list/advance persisted local clocks. Customers, subscriptions, and pending refunds can be attached with `test_clock`; advancing a clock processes attached trial activation, renewals, configured renewal failures, period-end cancellation, and refund settlement. | | Webhook endpoints | `POST /v1/webhook_endpoints`, `GET /v1/webhook_endpoints`, `GET /v1/webhook_endpoints/{id}`, `POST /v1/webhook_endpoints/{id}`, `PATCH /v1/webhook_endpoints/{id}`, `DELETE /v1/webhook_endpoints/{id}`, `GET /v1/webhook_endpoints/{id}/attempts` | Supported | Manage local webhook endpoints and inspect endpoint-scoped delivery attempts. Secrets are generated when omitted and masked in API responses. `enabled_events` supports exact event names, `*`, and prefix wildcards such as `invoice.*`. `PATCH` accepts the same local mutable fields as `POST`, including the `enabled` alias for `active`. | diff --git a/docs/COMPATIBILITY_TRACKING.md b/docs/COMPATIBILITY_TRACKING.md index 0672349..6db0ae1 100644 --- a/docs/COMPATIBILITY_TRACKING.md +++ b/docs/COMPATIBILITY_TRACKING.md @@ -1,6 +1,6 @@ # Compatibility Tracking -Status date: 2026-05-12 +Status date: 2026-08-29 This document defines how Billtap tracks Stripe API compatibility as a measurable, long-running body of work. The public claim is the generated @@ -23,6 +23,10 @@ The command writes: - `stripe-api-inventory.json`: machine-readable source for agents and CI. - `stripe-api-inventory.md`: reviewable compatibility table for humans. +The current baseline was measured against Stripe OpenAPI `2026-04-22.dahlia` +(stripe/openapi tag `v2261`); use the same pinned tag when regenerating so +family deltas stay comparable across PRs. + The runtime also carries a generated known-route catalog from the same public Stripe OpenAPI lane. That catalog is diagnostic-only: matching a route there returns `unsupported_endpoint` for unimplemented operations, but it does not @@ -84,7 +88,7 @@ The long-running expansion goal is at least 90% OpenAPI operation coverage at The concrete target and chunk plan live in `docs/STRIPE_COMPATIBILITY_90_TARGET.md`. In short: -- current baseline: `160 / 587`, `27.3%` +- current baseline: `175 / 587`, `29.8%` - target: at least `529 / 587`, `90.0%` - maximum remaining `L0`: `58 / 587` - P0/P1 billing-lab flows still need deeper `L3-L6` evidence, not just broad diff --git a/docs/GATE_STATUS.md b/docs/GATE_STATUS.md index 90d73a6..491d38f 100644 --- a/docs/GATE_STATUS.md +++ b/docs/GATE_STATUS.md @@ -16,9 +16,9 @@ This is the public gate snapshot. Internal adoption evidence and raw handoff not | G9 Release Candidate | Passed locally | Dockerfile, sample app, public examples, release checklist | | G10 Fixture Integration Smoke | Passed locally | Fixture apply/snapshot/assert APIs support deterministic integration setup | | G11 Assertion Ergonomics | Passed locally | Structured pass/fail fixture assertions and fixture-scoped snapshots | -| G12 Public Release Readiness | Passed locally | Public claims are tied to tests/scorecard cases; scorecard corpus has 49 release-blocking cases; Apache-2.0 `LICENSE` and `NOTICE` are present | +| G12 Public Release Readiness | Passed locally | Public claims are tied to tests/scorecard cases; scorecard corpus has 50 release-blocking cases; Apache-2.0 `LICENSE` and `NOTICE` are present | | G13 Stripe API Expansion | In progress | Roadmap defines compatibility levels and endpoint-family priorities; OpenAPI inventory generator and optional workflow write JSON/Markdown coverage artifacts | -| G14 Stripe API 90% Program | In progress | `docs/STRIPE_COMPATIBILITY_90_TARGET.md` defines 90% L1+ target, current 160/587 baseline, family thresholds, and chunk plan | +| G14 Stripe API 90% Program | In progress | `docs/STRIPE_COMPATIBILITY_90_TARGET.md` defines 90% L1+ target, current 175/587 baseline, family thresholds, and chunk plan | ## Current Public Claim @@ -30,25 +30,27 @@ under Apache-2.0. ## Current Compatibility Evidence - Scorecard version: `l3-public-readiness-v7` -- Release-blocking scorecard cases: 49 +- Release-blocking scorecard cases: 50 - Required scorecard release result: `mismatch=0`, `error=0`, `passed=true` - Coverage focus: request validation, protocol parameter acceptance, idempotency mismatch, deterministic checkout payment-error aliases, direct intent state machines, and billing lifecycle retry/renewal mutations -- OpenAPI operation baseline: `160 / 587`, `27.3%` +- OpenAPI operation baseline: `175 / 587`, `29.8%` - Long-running OpenAPI operation target: at least `529 / 587`, `90.0%`, at `L1+` with deeper P0/P1 behavior gates ## Last Local Code Verification -Verified on 2026-05-12 from branch `simulation-capacity-backlog`: +Verified on 2026-08-29 from branch `main`: - `go test ./... -count=1` -- `go run ./cmd/billtap compatibility scorecard --output-dir /tmp/billtap-scorecard-simulation-impl` - - result: `imported=49 skipped=1 unsupported=1 mismatch=0 error=0` -- `go run ./cmd/billtap compatibility inventory --openapi /tmp/stripe-openapi-spec3.json --output-dir /tmp/billtap-inventory-simulation-impl --source stripe-openapi-local` - - result: `operations=587 implemented=160 inventory_only=427 schema_validated=587 implemented_percent=27.3` -- Current branch verification covers Go tests, compatibility scorecard, inventory generation, whitespace checks, and forbidden internal keyword scan. +- `go run ./cmd/billtap compatibility scorecard --output-dir /tmp/billtap-compatibility` + - result: `imported=50 skipped=1 unsupported=1 mismatch=0 error=0` +- `go run ./cmd/billtap compatibility inventory --openapi --output-dir /tmp/billtap-inventory --source stripe/openapi-v2261-2026-04-22.dahlia` + - result: `operations=587 implemented=175 inventory_only=412 schema_validated=587 implemented_percent=29.8` +- Current branch verification covers Go tests, compatibility scorecard, and + inventory generation; `gofmt -l .` is clean. The forbidden internal keyword + scan runs in CI with the configured keyword list. - Container/image release gates run after merge through the existing release workflow. Release verification should still be rerun on the final release branch or tag. diff --git a/docs/PUBLIC_RELEASE_READINESS.md b/docs/PUBLIC_RELEASE_READINESS.md index a603962..23b56f2 100644 --- a/docs/PUBLIC_RELEASE_READINESS.md +++ b/docs/PUBLIC_RELEASE_READINESS.md @@ -1,6 +1,6 @@ # Public Release Readiness -Status date: 2026-05-12 +Status date: 2026-08-29 Billtap has a clear community-facing strength: it is a stateful local billing lab for deterministic subscription, fixture, scenario, and webhook reliability @@ -84,10 +84,10 @@ The scorecard evidence should state: - imported/skipped/unsupported/mismatch/error counts - whether `passed` is `true` -Current scorecard evidence on 2026-05-12: +Current scorecard evidence on 2026-08-29: - Scorecard version: `l3-public-readiness-v7` -- Scorecard result: `imported=49 skipped=1 unsupported=1 mismatch=0 error=0` +- Scorecard result: `imported=50 skipped=1 unsupported=1 mismatch=0 error=0` - Apache-2.0 `LICENSE`, `NOTICE`, `package.json`, and `package-lock.json` metadata are aligned. diff --git a/docs/SIMULATION_CAPACITY_BACKLOG.md b/docs/SIMULATION_CAPACITY_BACKLOG.md index ca26ea9..4034f11 100644 --- a/docs/SIMULATION_CAPACITY_BACKLOG.md +++ b/docs/SIMULATION_CAPACITY_BACKLOG.md @@ -1,6 +1,6 @@ # Simulation Capacity Backlog -Status date: 2026-05-12 +Status date: 2026-08-29 This backlog turns production-regression learnings into public, product-neutral Billtap capability work. It is intentionally broader than the current @@ -26,8 +26,8 @@ these public capability rows in their own private docs. Billtap currently has a stateful Stripe-like local subset with: - customers, products, prices, customer/product/price/subscription/invoice/ - payment-intent search, coupons, promotion codes, checkout sessions, - subscriptions, subscription schedules, invoices, invoice preview, + payment-intent search, coupons, promotion codes, tax rates, checkout + sessions, subscriptions, subscription schedules, invoices, invoice preview, payment intents, setup intents, payment methods, refunds, credit notes, disputes, cash balance, test clocks, Connect evidence, webhook endpoints, and events @@ -36,7 +36,7 @@ Billtap currently has a stateful Stripe-like local subset with: - webhook retry, duplicate, delay, out-of-order, replay, historical replay, and delivery attempt evidence - diagnostic bundles, request traces, timeline evidence, and dashboard views -- OpenAPI inventory baseline: `160 / 587` implemented operations, `27.3%` +- OpenAPI inventory baseline: `175 / 587` implemented operations, `29.8%` ## P0 Regression-Paired Backlog diff --git a/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md b/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md index e7f92cc..96b4593 100644 --- a/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md +++ b/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md @@ -1,6 +1,6 @@ # Stripe API Compatibility Roadmap -Status date: 2026-05-12 +Status date: 2026-08-29 This roadmap extends Billtap beyond one SaaS adoption path. The goal is to make Billtap progressively Stripe-compatible across a measured, versioned API @@ -63,7 +63,7 @@ The concrete expansion target is defined in `docs/STRIPE_COMPATIBILITY_90_TARGET.md`: - reach at least `90.0%` OpenAPI operation coverage at `L1+` -- move from the current `160 / 587` baseline to at least `529 / 587` +- move from the current `175 / 587` baseline to at least `529 / 587` - keep P0/P1 billing-lab families on deeper `L3-L6` gates where behavior matters - use broad `L1-L2` validation/fixture coverage for safe low-state and @@ -273,13 +273,13 @@ questions quickly. "inventory_version": "stripe-api-inventory-v2", "openapi_version": "3.0.0", "stripe_api_version": "2026-04-22.dahlia", - "source": "stripe/openapi master", - "generated_at": "2026-05-12T00:00:00Z", + "source": "stripe/openapi v2261", + "generated_at": "2026-08-29T00:00:00Z", "summary": { "total_operations": 587, - "implemented_operations": 160, - "inventory_only_operations": 427, - "implemented_percent": 27.3, + "implemented_operations": 175, + "inventory_only_operations": 412, + "implemented_percent": 29.8, "families": [ { "family": "connect", diff --git a/docs/STRIPE_COMPATIBILITY_90_TARGET.md b/docs/STRIPE_COMPATIBILITY_90_TARGET.md index 47287b5..bd70ed1 100644 --- a/docs/STRIPE_COMPATIBILITY_90_TARGET.md +++ b/docs/STRIPE_COMPATIBILITY_90_TARGET.md @@ -1,6 +1,6 @@ # Stripe Compatibility 90% Target -Status date: 2026-05-12 +Status date: 2026-08-29 Billtap's long-running Stripe API compatibility target is measurable coverage of at least 90% of the public Stripe OpenAPI operation inventory, without @@ -11,8 +11,8 @@ claiming that every operation has deep payment-processing behavior. The 90% target is based on generated `stripe-api-inventory.json`: - **Overall target:** `summary.implemented_percent >= 90.0`. -- **Current baseline:** `160 / 587` operations, `27.3%`, using Stripe OpenAPI - `2026-04-22.dahlia` from the local OpenAPI snapshot on 2026-05-12. +- **Current baseline:** `175 / 587` operations, `29.8%`, using Stripe OpenAPI + `2026-04-22.dahlia` (stripe/openapi tag `v2261`), re-measured on 2026-08-29. - **Minimum target count:** `529 / 587` operations at `L1` or higher. - **Remaining inventory-only budget:** at most `58 / 587` operations at `L0`. @@ -52,20 +52,21 @@ toward the 90% target after it has an explicit tested claim at `L1+`. ## Baseline By Family -Latest measured baseline from the local Stripe OpenAPI snapshot on 2026-05-12: +Latest measured baseline from the Stripe OpenAPI `2026-04-22.dahlia` snapshot +(stripe/openapi tag `v2261`), re-measured on 2026-08-29: | Priority | Family | Total | Implemented | Coverage | 90% target count | First target | | --- | --- | ---: | ---: | ---: | ---: | --- | | P0 | webhooks | 7 | 7 | 100.0% | 7 | Expand connected-account routing, thin event fixtures, and replay evidence. | | P0 | checkout | 6 | 3 | 50.0% | 6 | Close checkout route gaps and SDK smoke. | -| P0 | billing | 39 | 12 | 30.8% | 36 | Add renewal, trial, dunning, subscription schedule, coupon, and credit-note scenarios. | +| P0 | billing | 39 | 24 | 61.5% | 36 | Add renewal, trial, dunning, subscription schedule, coupon, and credit-note scenarios. | | P0 | billing_portal | 5 | 1 | 20.0% | 5 | Add portal configurations and session retrieval fixtures. | -| P1 | catalog | 54 | 19 | 35.2% | 49 | Add coupon, promotion code, tax-rate, and product/price search validation. | -| P1 | customers | 31 | 11 | 35.5% | 28 | Add OpenAPI-backed validation, search/list parity, and payment source fixtures. | -| P1 | payments | 41 | 15 | 36.6% | 37 | Add PaymentIntent and SetupIntent create/confirm/capture/cancel state machines. | +| P1 | catalog | 54 | 27 | 50.0% | 49 | Add coupon, promotion code, tax-rate, and product/price search validation. | +| P1 | customers | 31 | 12 | 38.7% | 28 | Add OpenAPI-backed validation, search/list parity, and payment source fixtures. | +| P1 | payments | 41 | 22 | 53.7% | 37 | Add PaymentIntent and SetupIntent create/confirm/capture/cancel state machines. | | P1 | connect | 53 | 53 | 100.0% | 48 | Deepen Connect SDK/adoption smoke, connected-account webhook routing, and v2 Core account inventory evidence. | | P1 | payment_history | 30 | 13 | 43.3% | 27 | Add charge, refund, balance transaction, dispute, and payment history evidence. | -| P3 | auxiliary | 321 | 10 | 3.1% | 289 | Keep inventory visible and add schema/fixture smoke only when adoption requires it. | +| P3 | auxiliary | 321 | 13 | 4.0% | 289 | Keep inventory visible and add schema/fixture smoke only when adoption requires it. | ## PR Chunk Plan @@ -127,9 +128,18 @@ payment-method attach/detach. T10 also does not increase `summary.implemented_operations` by itself. It raises confidence and levels for already counted operations; new operation coverage must come from T3-T9. The planned T3-T9 delta is intentionally larger -than the `+369` operations needed to move the current `160 / 587` baseline to +than the `+354` operations needed to move the current `175 / 587` baseline to the `529 / 587` target. +The tax and invoice-depth waves landed between the 2026-05-12 and 2026-08-29 +measurements raised the generated inventory from `160 / 587` (`27.3%`) to +`175 / 587` (`29.8%`): tax-rate create/list/retrieve/update, customer tax-ID +create/list/retrieve/delete, `POST /v1/invoices/{id}/send`, and invoice-item +list/create/retrieve plus manual invoice create/finalize/lines claims. Billing +is now `24 / 39`, catalog `27 / 54`, and auxiliary `13 / 321`; the previous +baseline table had drifted behind its own summary, so the family rows above +moved further than these operation deltas alone suggest. + ## Derived Gate Checks The overall `summary.implemented_percent` check is necessary but not sufficient. diff --git a/internal/stripecompat/registry.go b/internal/stripecompat/registry.go index 73e029f..040fdad 100644 --- a/internal/stripecompat/registry.go +++ b/internal/stripecompat/registry.go @@ -241,8 +241,8 @@ func DefaultClaims() []Claim { add(http.MethodPost, "/v1/customers/{id}/subscriptions/{id}", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.updated"}}) add(http.MethodDelete, "/v1/customers/{id}/subscriptions/{id}", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.deleted"}}) - add(http.MethodPost, "/v1/subscription_items", Claim{Level: "L3", Stateful: true, ScorecardCases: []string{"subscription_items.create.invalid_quantity"}, SDKSmoke: []string{"stripe-node"}, Risks: []string{"proration_behavior always_invoice/create_prorations reuses subscription-update proration (subscription default_tax_rates only; item tax_rates evidence-only)", "item IDs are index-based (si__) and shift when a middle item is deleted"}}) - add(http.MethodDelete, "/v1/subscription_items/{id}", Claim{Level: "L3", Stateful: true, SDKSmoke: []string{"stripe-node"}, Risks: []string{"proration_behavior supported; last item delete rejected; clear_usage evidence-only (metered usage not modeled)", "item IDs are index-based and shift after middle-item delete"}}) + add(http.MethodPost, "/v1/subscription_items", Claim{Level: "L3", Stateful: true, ScorecardCases: []string{"subscription_items.create.invalid_quantity"}, SDKSmoke: []string{"stripe-node"}, Risks: []string{"proration_behavior always_invoice/create_prorations reuses subscription-update proration (subscription default_tax_rates only; item tax_rates evidence-only)", "item IDs keep the si__ shape but are stored at creation, so deletes no longer shift later item IDs; subscriptions stored before that change are backfilled with their position-derived IDs on next write"}}) + add(http.MethodDelete, "/v1/subscription_items/{id}", Claim{Level: "L3", Stateful: true, SDKSmoke: []string{"stripe-node"}, Risks: []string{"proration_behavior supported; last item delete rejected; clear_usage evidence-only (metered usage not modeled)", "stored item IDs stay stable after a middle-item delete; a later add reuses the lowest unused index"}}) add(http.MethodDelete, "/v1/subscriptions/{id}", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.deleted"}}) add(http.MethodPost, "/v1/subscriptions/{id}/resume", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.updated"}, Risks: []string{"clears local pause_collection evidence; billing-cycle and proration side effects are not modeled"}}) add(http.MethodGet, "/v1/subscriptions/search", Claim{Level: "L3", Stateful: true, Risks: []string{"supports a measured subscription search subset for id, customer, status, and metadata equality clauses joined by AND"}}) From d720759af38b44bc8a1b3bc959c206b6b581d953 Mon Sep 17 00:00:00 2001 From: midagedev Date: Sat, 29 Aug 2026 16:14:55 +0900 Subject: [PATCH 2/5] feat(api): portal configurations, subscription item read/update, nested discount routes Adds billing portal configuration CRUD as local evidence (first configuration becomes the run default), subscription item list/retrieve and update (price/quantity with the shared proration path), and customer-scoped nested subscription discount retrieve/delete. Claims the already-tested invoice void, invoice mark_uncollectible, and checkout expire routes, retiring the implementedWithoutCompatClaim fallback carve-out. Fixes subscription discount deletion: the delete patch removed discount metadata keys, but subscription metadata patches merge, so the discount survived its own deletion. OpenAPI inventory: 175/587 (29.8%) -> 187/587 (31.9%); billing_portal closes to 5/5, billing 31/39, checkout 4/6. --- CHANGELOG.md | 27 ++ README.md | 2 +- docs/COMPATIBILITY.md | 3 +- docs/COMPATIBILITY_TRACKING.md | 2 +- docs/GATE_STATUS.md | 6 +- docs/SIMULATION_CAPACITY_BACKLOG.md | 2 +- docs/STRIPE_API_COMPATIBILITY_ROADMAP.md | 8 +- docs/STRIPE_COMPATIBILITY_90_TARGET.md | 18 +- internal/api/api.go | 166 +++++++++-- internal/api/discounts.go | 7 +- internal/api/local_evidence.go | 57 ++-- internal/api/portal_configurations.go | 280 ++++++++++++++++++ internal/api/portal_configurations_test.go | 150 ++++++++++ .../api/subscription_item_read_update_test.go | 223 ++++++++++++++ internal/api/validation.go | 90 ++++++ internal/billing/service.go | 24 +- internal/stripecompat/registry.go | 13 + internal/stripecompat/registry_test.go | 20 +- 18 files changed, 1028 insertions(+), 70 deletions(-) create mode 100644 internal/api/portal_configurations.go create mode 100644 internal/api/portal_configurations_test.go create mode 100644 internal/api/subscription_item_read_update_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c6108f..7566aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ ## Unreleased +- Added billing portal configurations: `GET/POST + /v1/billing_portal/configurations` and `GET/POST + /v1/billing_portal/configurations/{id}` store local portal-configuration + evidence with Stripe-shaped `business_profile`, `default_return_url`, + `login_page`, `features[...]`, and `metadata` fields, list filters for + `active`/`is_default`, and the first configuration in a run becoming the + default. The hosted portal does not render configuration features. +- `GET /v1/subscription_items` lists resolved items across subscriptions with + the `subscription` filter and the standard list envelope, and + `GET /v1/subscription_items/{id}` retrieves one item. +- `POST /v1/subscription_items/{id}` updates an item's `price` and `quantity` + (plus metadata and evidence-only `tax_rates`), routing + `proration_behavior=always_invoice/create_prorations` through the same + proration path as item create so a quantity bump with `always_invoice` + issues the prorated `subscription_update` invoice. +- `GET/DELETE /v1/customers/{id}/subscriptions/{id}/discount` now mirror the + top-level subscription discount routes and are customer-scoped: a mismatched + customer gets `404`. +- Fixed subscription discount deletion: `DELETE .../subscriptions/{id}/discount` + built a metadata copy with the discount keys removed, but subscription + metadata patches merge, so the stored discount survived its own deletion and + a following `GET` still returned it. The patch now zeroes the discount keys, + which is how merge-mode patches delete. +- Registered compat claims for already-tested routes: invoice `void`, + invoice `mark_uncollectible`, and checkout `expire`. The OpenAPI inventory + moves from `175 / 587` (`29.8%`) to `187 / 587` (`31.9%`): billing + `31 / 39`, billing_portal `5 / 5`, checkout `4 / 6`. - Local evidence objects — coupons, promotion codes, subscription schedules, disputes, tax rates, tax IDs and customer cash balances — are now stored in the run's own database instead of process memory. They were the only objects diff --git a/README.md b/README.md index f04482a..26563c5 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ curl -fsS "http://localhost:8080/api/diagnostics?limit=100" \ | Fixtures | Apply/snapshot/assert APIs | JSON/YAML input, fixture metadata isolation, structured pass/fail reports | | SaaS profile | Generic workspace billing profile | Plans, seats, members, export quota, extra export, payment history, support bundle, platform/connect-style webhook evidence | | Release state | Source plus GHCR image | Local Docker image builds and GHCR image workflow; no package/Homebrew/signed binary yet | -| Stripe API inventory | `175 / 587` operations, `29.8%` L1+ | OpenAPI route inventory is schema-visible for all `587` operations; implemented coverage is tracked in `docs/STRIPE_COMPATIBILITY_90_TARGET.md` | +| Stripe API inventory | `187 / 587` operations, `31.9%` L1+ | OpenAPI route inventory is schema-visible for all `587` operations; implemented coverage is tracked in `docs/STRIPE_COMPATIBILITY_90_TARGET.md` | Detailed compatibility matrix: `docs/COMPATIBILITY.md`. diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index e49a119..78c5b4f 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -107,9 +107,10 @@ traffic can keep using the unprefixed service URL. | Checkout sessions | `POST /v1/checkout/sessions`, `GET /v1/checkout/sessions`, `GET /v1/checkout/sessions/{id}`, `POST /v1/checkout/sessions/{id}/expire` | Supported | Creates subscription- and payment-mode sandbox checkout sessions from request line items and hosted Billtap URLs; `setup` mode is rejected. The Stripe-style session response leaves `line_items` unexpanded. Accepts Stripe SDK form params `allow_promotion_codes`, `discounts[0][coupon]`, `discounts[0][promotion_code]`, `subscription_data[trial_period_days]`, and `subscription_data[default_tax_rates]` (applied to session totals; exclusive/inclusive snapshots; mutually exclusive with `automatic_tax`); trial checkout creates local `trialing` subscription evidence. Payment-mode sessions accept inline `price_data` line items (creating local product/price evidence), `payment_intent_data[...]`, `client_reference_id`, session-level `metadata[...]`, `automatic_tax[enabled]`, and `tax_id_collection[enabled]` (tax snapshot from customer metadata `tax_percent`), and complete into a one-time PaymentIntent without subscription or invoice evidence. `expire` moves an open session to `expired` and emits `checkout.session.expired`; non-open sessions return `invalid_request_error`. Hosted URLs use the request host by default, or `BILLTAP_PUBLIC_BASE_URL` when configured for container-to-host browser flows. `PUBLIC_BASE_PATH`, `BILLTAP_PUBLIC_BASE_PATH`, and `X-Forwarded-Prefix` are reflected in hosted browser URLs. | | Checkout completion | `POST /v1/checkout/sessions/{id}/complete`, `POST /api/checkout/sessions/{id}/complete`, `POST/DELETE /api/checkout/sessions/{id}/promotion_code` | Billtap-specific | Completes a sandbox checkout and creates subscription, invoice, payment intent, timeline, and checkout webhook evidence. Supports success plus deterministic failure aliases such as `card_declined`, `insufficient_funds`, `expired_card`, `incorrect_cvc`, `processing_error`, `authentication_required`, `payment_pending`, `canceled`, and documented Stripe test PaymentMethod IDs such as `pm_card_visa_chargeDeclined`. Hosted checkout can apply or remove a promotion code on an open `allow_promotion_codes` session via `POST/DELETE /api/checkout/sessions/{id}/promotion_code` (form `promotion_code=`), refreshing session totals before completion. | | Billing portal sessions | `POST /v1/billing_portal/sessions` | Partial | Returns a Stripe-like `billing_portal.session` object and Billtap portal URL for a known customer, including `flow`, `locale`, `on_behalf_of`, `return_url`, and `livemode` fields. Accepts `customer`, `return_url`, optional `configuration`, `locale`, `on_behalf_of`, and `flow_data`, with enum and required nested-field validation for the modeled portal flows. The hosted portal can simulate saved payment-method updates and subscription cancellation, then redirect to `return_url`; full Stripe-hosted portal configuration rendering is not modeled. | +| Billing portal configurations | `GET/POST /v1/billing_portal/configurations`, `GET/POST /v1/billing_portal/configurations/{id}` | Partial | Local portal-configuration evidence with Stripe-shaped `business_profile`, `default_return_url`, `login_page`, `features[...]`, and `metadata`. Create applies Stripe-shaped feature defaults; update merges business profile, features, and metadata and bumps `updated`. List filters by `active` and `is_default`; the first configuration in a run becomes the default. The hosted portal does not render configuration features. | | Subscriptions | `POST /v1/subscriptions`, `GET /v1/subscriptions`, `GET /v1/subscriptions/search`, `GET /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}`, `DELETE /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}/resume`, nested `GET/POST/DELETE /v1/customers/{id}/subscriptions...`, `GET/DELETE /v1/subscriptions/{id}/discount` | Partial | Create/list/retrieve subscriptions through the local checkout-completion state path. Update supports item replacement, metadata merge, single coupon or promotion-code discount application, `test_clock`, `cancel_at_period_end`, `pause_collection`, and accepted billing/proration evidence including `proration_date`, `proration_behavior`, `payment_behavior`, `billing_cycle_anchor`, and `trial_end`; resume clears local pause evidence and emits `customer.subscription.updated`. Top-level and nested customer lists support status and metadata equality filters such as `metadata[billtap_fixture_ref]`. Search supports `id`, `customer`, `status`, and metadata equality clauses joined by `AND`. Delete performs immediate sandbox cancellation. Test-clock and scenario clock advances can activate due trials, renew active periods with bounded discount math, fail configured renewals from subscription or customer metadata, and cancel period-end subscriptions in the local billing graph. | | Subscription schedules | `POST /v1/subscription_schedules`, `GET /v1/subscription_schedules`, `GET /v1/subscription_schedules/{id}`, `POST /v1/subscription_schedules/{id}`, `POST /v1/subscription_schedules/{id}/cancel`, `POST /v1/subscription_schedules/{id}/release` | Partial | Local schedule evidence for one due phase on an existing subscription. Test-clock advance applies a due phase by replacing subscription items and emits `customer.subscription.updated`; complex multi-phase billing, proration, invoices, and schedule lifecycle parity are not modeled. | -| Subscription items | `POST /v1/subscription_items`, `DELETE /v1/subscription_items/{id}` | Partial | Add or remove local subscription items. Create and delete accept `proration_behavior` (`none`/`create_prorations`/`always_invoice`) and `proration_date` through the same proration path as subscription update, applying the subscription's `default_tax_rates` after discounts: `always_invoice` issues a paid `subscription_update` invoice and repoints `latest_invoice`, and `create_prorations` defers the delta to the next renewal invoice. Deleting the last item is rejected. Item IDs are stored at creation so deletes no longer shift later IDs. Item-level `tax_rates` are evidence-only, and metered usage is not modeled. | +| Subscription items | `POST /v1/subscription_items`, `GET /v1/subscription_items`, `GET /v1/subscription_items/{id}`, `POST /v1/subscription_items/{id}`, `DELETE /v1/subscription_items/{id}` | Partial | List, retrieve, add, update, and remove local subscription items. List supports the `subscription` filter; update replaces `price`/`quantity` and echoes metadata and evidence-only `tax_rates`. Create, update, and delete accept `proration_behavior` (`none`/`create_prorations`/`always_invoice`) and `proration_date` through the same proration path as subscription update, applying the subscription's `default_tax_rates` after discounts: `always_invoice` issues a paid `subscription_update` invoice and repoints `latest_invoice`, and `create_prorations` defers the delta to the next renewal invoice. Deleting the last item is rejected. Item IDs are stored at creation so deletes no longer shift later IDs. Item-level `tax_rates` are evidence-only, and metered usage is not modeled. | | Invoices | `POST /v1/invoices`, `GET /v1/invoices`, `GET /v1/invoices/search`, `GET /v1/invoices/{id}`, `POST /v1/invoices/{id}/finalize`, `POST /v1/invoices/{id}/pay`, `POST /v1/invoices/{id}/send`, `POST /v1/invoices/{id}/void`, `POST /v1/invoices/{id}/mark_uncollectible`, `GET /v1/invoices/{id}/lines`, `GET /v1/invoices/{id}/payments`, `POST /v1/invoices/create_preview`, `GET/POST /v1/invoices/upcoming` | Partial | List/retrieve/search invoices created by checkout and create a bounded manual one-time invoice flow. Search supports `id`, `customer`, `subscription`, and `status` clauses joined by `AND`. `pay` retries open checkout invoices with deterministic sandbox `payment_method` or `source` aliases, mutating invoice, subscription, payment-intent, timeline, and webhook evidence. Manual invoices accept `customer`, `currency`, `collection_method`, `default_payment_method`, `description`, `auto_advance=false`, `pending_invoice_items_behavior`, `payment_settings[payment_method_types]`, and `metadata[...]`; `pending_invoice_items_behavior=include` attaches the customer's same-currency pending invoice items once at create. `finalize` creates local PaymentIntent evidence, and `pay` applies per-intent or customer-level default outcomes including success, card decline, and `requires_action`. `send` records local email evidence and emits `invoice.sent`; `void` moves an `open` invoice to `void` and emits `invoice.voided`; `mark_uncollectible` moves an `open` invoice to `uncollectible` and emits `invoice.marked_uncollectible`; non-open statuses return `invalid_request_error`. Invoice responses include `confirmation_secret`, local hosted/PDF paths, `payments.data.payment.payment_intent`, metadata, status transitions, automatic tax state, tax totals, payment settings, array-shaped `discounts`, `lines.data` serialized like the lines endpoint, and `post_payment_credit_notes_amount` on paid invoices. Preview endpoints calculate a Stripe-shaped subscription-update proration line from current period bounds, old/new price totals, `subscription_details[items]`, `subscription_details[proration_date]`, `subscription_details[proration_behavior]`, and `subscription_details[billing_cycle_anchor]`; previews without item overrides return the subscription's next billing cycle (items, discounts, and tax with `billing_reason: upcoming`) matching the next renewal invoice. A bounded single-discount subset updates `subtotal`, `total`, and `total_discount_amounts`; invoice rendering, automatic collection, and full dunning lifecycle are outside the modeled subset. | | Invoice items | `POST /v1/invoiceitems`, `GET /v1/invoiceitems`, `GET /v1/invoiceitems/{id}` | Partial | Creates, lists, and retrieves local invoice item evidence for manual one-time invoices. `customer`, `invoice`, `amount`, `currency`, `description`, and `metadata[...]` are accepted, or `pricing[price]` plus `quantity` instead of `amount` (line amount is the price's `unit_amount` times `quantity`, currency from the price). Omitting `invoice` stores the item as a pending customer item that `POST /v1/invoices` with `pending_invoice_items_behavior=include` attaches; `subscription` is accepted and echoed. Tax, discount, and full line mutation parity are not modeled. | | Payment intents | `POST /v1/payment_intents`, `GET /v1/payment_intents`, `GET /v1/payment_intents/search`, `GET /v1/payment_intents/{id}`, `POST /v1/payment_intents/{id}/confirm`, `POST /v1/payment_intents/{id}/capture`, `POST /v1/payment_intents/{id}/cancel` | Partial | Create/list/retrieve/search and mutate local payment intents. Search supports `id`, `customer`, `status`, and metadata equality clauses joined by `AND`. `confirm` supports deterministic sandbox PaymentMethod aliases such as `pm_card_visa`, `pm_card_visa_chargeDeclined`, `pm_card_threeDSecure2Required`, `requires_action`, and local bank-transfer processing. Manual capture moves through `requires_capture` before `capture` succeeds. Direct one-time intents emit `payment_intent.created` and a terminal event when confirmed. Deferred per-intent outcomes and customer defaults are supported; `requires_action` can return either `use_stripe_sdk` or local `redirect_to_url` next-action payloads. | diff --git a/docs/COMPATIBILITY_TRACKING.md b/docs/COMPATIBILITY_TRACKING.md index 6db0ae1..949ff17 100644 --- a/docs/COMPATIBILITY_TRACKING.md +++ b/docs/COMPATIBILITY_TRACKING.md @@ -88,7 +88,7 @@ The long-running expansion goal is at least 90% OpenAPI operation coverage at The concrete target and chunk plan live in `docs/STRIPE_COMPATIBILITY_90_TARGET.md`. In short: -- current baseline: `175 / 587`, `29.8%` +- current baseline: `187 / 587`, `31.9%` - target: at least `529 / 587`, `90.0%` - maximum remaining `L0`: `58 / 587` - P0/P1 billing-lab flows still need deeper `L3-L6` evidence, not just broad diff --git a/docs/GATE_STATUS.md b/docs/GATE_STATUS.md index 491d38f..abda0d5 100644 --- a/docs/GATE_STATUS.md +++ b/docs/GATE_STATUS.md @@ -18,7 +18,7 @@ This is the public gate snapshot. Internal adoption evidence and raw handoff not | G11 Assertion Ergonomics | Passed locally | Structured pass/fail fixture assertions and fixture-scoped snapshots | | G12 Public Release Readiness | Passed locally | Public claims are tied to tests/scorecard cases; scorecard corpus has 50 release-blocking cases; Apache-2.0 `LICENSE` and `NOTICE` are present | | G13 Stripe API Expansion | In progress | Roadmap defines compatibility levels and endpoint-family priorities; OpenAPI inventory generator and optional workflow write JSON/Markdown coverage artifacts | -| G14 Stripe API 90% Program | In progress | `docs/STRIPE_COMPATIBILITY_90_TARGET.md` defines 90% L1+ target, current 175/587 baseline, family thresholds, and chunk plan | +| G14 Stripe API 90% Program | In progress | `docs/STRIPE_COMPATIBILITY_90_TARGET.md` defines 90% L1+ target, current 187/587 baseline, family thresholds, and chunk plan | ## Current Public Claim @@ -35,7 +35,7 @@ under Apache-2.0. - Coverage focus: request validation, protocol parameter acceptance, idempotency mismatch, deterministic checkout payment-error aliases, direct intent state machines, and billing lifecycle retry/renewal mutations -- OpenAPI operation baseline: `175 / 587`, `29.8%` +- OpenAPI operation baseline: `187 / 587`, `31.9%` - Long-running OpenAPI operation target: at least `529 / 587`, `90.0%`, at `L1+` with deeper P0/P1 behavior gates @@ -47,7 +47,7 @@ Verified on 2026-08-29 from branch `main`: - `go run ./cmd/billtap compatibility scorecard --output-dir /tmp/billtap-compatibility` - result: `imported=50 skipped=1 unsupported=1 mismatch=0 error=0` - `go run ./cmd/billtap compatibility inventory --openapi --output-dir /tmp/billtap-inventory --source stripe/openapi-v2261-2026-04-22.dahlia` - - result: `operations=587 implemented=175 inventory_only=412 schema_validated=587 implemented_percent=29.8` + - result: `operations=587 implemented=187 inventory_only=400 schema_validated=587 implemented_percent=31.9` - Current branch verification covers Go tests, compatibility scorecard, and inventory generation; `gofmt -l .` is clean. The forbidden internal keyword scan runs in CI with the configured keyword list. diff --git a/docs/SIMULATION_CAPACITY_BACKLOG.md b/docs/SIMULATION_CAPACITY_BACKLOG.md index 4034f11..48d4ad1 100644 --- a/docs/SIMULATION_CAPACITY_BACKLOG.md +++ b/docs/SIMULATION_CAPACITY_BACKLOG.md @@ -36,7 +36,7 @@ Billtap currently has a stateful Stripe-like local subset with: - webhook retry, duplicate, delay, out-of-order, replay, historical replay, and delivery attempt evidence - diagnostic bundles, request traces, timeline evidence, and dashboard views -- OpenAPI inventory baseline: `175 / 587` implemented operations, `29.8%` +- OpenAPI inventory baseline: `187 / 587` implemented operations, `31.9%` ## P0 Regression-Paired Backlog diff --git a/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md b/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md index 96b4593..3c2184e 100644 --- a/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md +++ b/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md @@ -63,7 +63,7 @@ The concrete expansion target is defined in `docs/STRIPE_COMPATIBILITY_90_TARGET.md`: - reach at least `90.0%` OpenAPI operation coverage at `L1+` -- move from the current `175 / 587` baseline to at least `529 / 587` +- move from the current `187 / 587` baseline to at least `529 / 587` - keep P0/P1 billing-lab families on deeper `L3-L6` gates where behavior matters - use broad `L1-L2` validation/fixture coverage for safe low-state and @@ -277,9 +277,9 @@ questions quickly. "generated_at": "2026-08-29T00:00:00Z", "summary": { "total_operations": 587, - "implemented_operations": 175, - "inventory_only_operations": 412, - "implemented_percent": 29.8, + "implemented_operations": 187, + "inventory_only_operations": 400, + "implemented_percent": 31.9, "families": [ { "family": "connect", diff --git a/docs/STRIPE_COMPATIBILITY_90_TARGET.md b/docs/STRIPE_COMPATIBILITY_90_TARGET.md index bd70ed1..34fc9f5 100644 --- a/docs/STRIPE_COMPATIBILITY_90_TARGET.md +++ b/docs/STRIPE_COMPATIBILITY_90_TARGET.md @@ -11,7 +11,7 @@ claiming that every operation has deep payment-processing behavior. The 90% target is based on generated `stripe-api-inventory.json`: - **Overall target:** `summary.implemented_percent >= 90.0`. -- **Current baseline:** `175 / 587` operations, `29.8%`, using Stripe OpenAPI +- **Current baseline:** `187 / 587` operations, `31.9%`, using Stripe OpenAPI `2026-04-22.dahlia` (stripe/openapi tag `v2261`), re-measured on 2026-08-29. - **Minimum target count:** `529 / 587` operations at `L1` or higher. - **Remaining inventory-only budget:** at most `58 / 587` operations at `L0`. @@ -58,9 +58,9 @@ Latest measured baseline from the Stripe OpenAPI `2026-04-22.dahlia` snapshot | Priority | Family | Total | Implemented | Coverage | 90% target count | First target | | --- | --- | ---: | ---: | ---: | ---: | --- | | P0 | webhooks | 7 | 7 | 100.0% | 7 | Expand connected-account routing, thin event fixtures, and replay evidence. | -| P0 | checkout | 6 | 3 | 50.0% | 6 | Close checkout route gaps and SDK smoke. | -| P0 | billing | 39 | 24 | 61.5% | 36 | Add renewal, trial, dunning, subscription schedule, coupon, and credit-note scenarios. | -| P0 | billing_portal | 5 | 1 | 20.0% | 5 | Add portal configurations and session retrieval fixtures. | +| P0 | checkout | 6 | 4 | 66.7% | 6 | Close checkout session update/line_items routes and SDK smoke. | +| P0 | billing | 39 | 31 | 79.5% | 36 | Add renewal, trial, dunning, subscription schedule, coupon, and credit-note scenarios. | +| P0 | billing_portal | 5 | 5 | 100.0% | 5 | Deepen portal configuration fixtures and hosted portal rendering evidence. | | P1 | catalog | 54 | 27 | 50.0% | 49 | Add coupon, promotion code, tax-rate, and product/price search validation. | | P1 | customers | 31 | 12 | 38.7% | 28 | Add OpenAPI-backed validation, search/list parity, and payment source fixtures. | | P1 | payments | 41 | 22 | 53.7% | 37 | Add PaymentIntent and SetupIntent create/confirm/capture/cancel state machines. | @@ -128,9 +128,17 @@ payment-method attach/detach. T10 also does not increase `summary.implemented_operations` by itself. It raises confidence and levels for already counted operations; new operation coverage must come from T3-T9. The planned T3-T9 delta is intentionally larger -than the `+354` operations needed to move the current `175 / 587` baseline to +than the `+342` operations needed to move the current `187 / 587` baseline to the `529 / 587` target. +The P0 portal/items wave on 2026-08-29 raised the generated inventory from +`175 / 587` (`29.8%`) to `187 / 587` (`31.9%`): billing portal configurations +create/list/retrieve/update, subscription item list/retrieve/update, nested +customer subscription discount retrieve/delete, and claims for the +already-tested invoice `void`/`mark_uncollectible` and checkout `expire` +routes. billing_portal closed to `5 / 5`, billing moved to `31 / 39`, and +checkout to `4 / 6`. + The tax and invoice-depth waves landed between the 2026-05-12 and 2026-08-29 measurements raised the generated inventory from `160 / 587` (`27.3%`) to `175 / 587` (`29.8%`): tax-rate create/list/retrieve/update, customer tax-ID diff --git a/internal/api/api.go b/internal/api/api.go index ad97632..09855f0 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -113,6 +113,8 @@ func (h *Handler) routes() { h.mux.HandleFunc("/v1/checkout/sessions", h.handleCheckoutSessions) h.mux.HandleFunc("/v1/checkout/sessions/", h.handleCheckoutSession) h.mux.HandleFunc("/v1/billing_portal/sessions", h.handleBillingPortalSessions) + h.mux.HandleFunc("/v1/billing_portal/configurations", h.handleBillingPortalConfigurations) + h.mux.HandleFunc("/v1/billing_portal/configurations/", h.handleBillingPortalConfiguration) h.mux.HandleFunc("/v1/subscriptions", h.handleSubscriptions) h.mux.HandleFunc("/v1/subscriptions/search", h.handleSubscriptionSearch) h.mux.HandleFunc("/v1/subscriptions/", h.handleSubscription) @@ -210,25 +212,7 @@ func (h *Handler) methodNotAllowed(w http.ResponseWriter, r *http.Request, allow methodNotAllowed(w, allow) } -func implementedWithoutCompatClaim(method string, path string) bool { - if method != http.MethodPost { - return false - } - if strings.HasPrefix(path, "/v1/invoices/") && (strings.HasSuffix(path, "/void") || strings.HasSuffix(path, "/mark_uncollectible")) { - parts := strings.Split(strings.TrimPrefix(path, "/v1/invoices/"), "/") - return len(parts) == 2 && parts[0] != "" - } - if strings.HasPrefix(path, "/v1/checkout/sessions/") && strings.HasSuffix(path, "/expire") { - id := strings.TrimSuffix(strings.TrimPrefix(path, "/v1/checkout/sessions/"), "/expire") - return id != "" && !strings.Contains(id, "/") - } - return false -} - func (h *Handler) writeKnownUnsupportedRoute(w http.ResponseWriter, r *http.Request) bool { - if implementedWithoutCompatClaim(r.Method, r.URL.Path) { - return false - } route, ok := h.knownRoutes.Lookup(r.Method, r.URL.Path) if !ok { return false @@ -473,6 +457,15 @@ func (h *Handler) handleCustomerSubscription(w http.ResponseWriter, r *http.Requ } if hasNested { if nested == "discount" { + subscription, err := h.billing.GetSubscription(r.Context(), subscriptionID) + if err != nil { + writeResult(w, nil, err) + return + } + if subscription.CustomerID != customerID { + writeResult(w, nil, billing.ErrNotFound) + return + } h.handleSubscriptionDiscount(w, r, subscriptionID) return } @@ -2353,8 +2346,32 @@ func (h *Handler) handleSubscriptionResume(w http.ResponseWriter, r *http.Reques } func (h *Handler) handleSubscriptionItems(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + p := params{values: firstValues(r.URL.Query())} + if err := validateSubscriptionItemList(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + subscriptions, err := h.billing.ListSubscriptions(r.Context()) + if err != nil { + writeResult(w, nil, err) + return + } + filter := strings.TrimSpace(r.URL.Query().Get("subscription")) + data := make([]map[string]any, 0) + for _, subscription := range subscriptions { + if filter != "" && subscription.ID != filter { + continue + } + for idx, item := range subscription.Items { + data = append(data, h.stripeSubscriptionItem(r, subscription, item, idx)) + } + } + writeJSON(w, http.StatusOK, stripeListFromRequest(r, data)) + return + } if r.Method != http.MethodPost { - h.methodNotAllowed(w, r, "POST") + h.methodNotAllowed(w, r, "GET, POST") return } p, err := parseParams(r) @@ -2455,10 +2472,117 @@ func (h *Handler) handleSubscriptionItem(w http.ResponseWriter, r *http.Request) h.notFound(w, r) return } - if r.Method != http.MethodDelete { - h.methodNotAllowed(w, r, "DELETE") + switch r.Method { + case http.MethodGet: + subscription, idx, found, err := h.findSubscriptionItem(r, id) + if err != nil { + writeResult(w, nil, err) + return + } + if !found { + writeResult(w, nil, billing.ErrNotFound) + return + } + writeJSON(w, http.StatusOK, h.stripeSubscriptionItem(r, subscription, subscription.Items[idx], idx)) + case http.MethodPost: + h.handleSubscriptionItemUpdate(w, r, id) + case http.MethodDelete: + h.handleSubscriptionItemDelete(w, r, id) + default: + h.methodNotAllowed(w, r, "GET, POST, DELETE") + } +} + +func (h *Handler) handleSubscriptionItemUpdate(w http.ResponseWriter, r *http.Request, id string) { + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateSubscriptionItemUpdate(p); err != nil { + writeError(w, http.StatusBadRequest, err) return } + subscription, idx, found, err := h.findSubscriptionItem(r, id) + if err != nil { + writeResult(w, nil, err) + return + } + if !found { + writeResult(w, nil, billing.ErrNotFound) + return + } + // Resolve item tax_rates before mutation (evidence only; totals use subscription rates). + var itemTaxRates []billing.AppliedTaxRate + if p.hasDefaultTaxRatesParam("tax_rates") { + itemTaxRates, err = h.appliedTaxRatesFromParams(p, "tax_rates") + if err != nil { + writeResult(w, nil, err) + return + } + } + item := subscription.Items[idx] + if priceID := p.first("price", "price_id"); priceID != "" { + if err := validatePriceExists(h.billing.GetPrice(r.Context(), priceID)); err != nil { + writeResult(w, nil, err) + return + } + item.PriceID = priceID + } + if p.has("quantity") { + item.Quantity = p.int64Default("quantity", item.Quantity) + } + items := append([]billing.LineItem{}, subscription.Items...) + items[idx] = item + + prorationBehavior := p.string("proration_behavior") + var updated billing.Subscription + if prorationBehavior == "always_invoice" || prorationBehavior == "create_prorations" { + prorationDate := time.Time{} + if raw := p.string("proration_date"); raw != "" { + if seconds, parseErr := strconv.ParseInt(raw, 10, 64); parseErr == nil { + prorationDate = time.Unix(seconds, 0).UTC() + } + } + result, err := h.billing.UpdateSubscriptionItemsWithProration(r.Context(), billing.SubscriptionProrationRequest{ + SubscriptionID: subscription.ID, + NewItems: items, + ProrationBehavior: prorationBehavior, + ProrationDate: prorationDate, + BillingCycleAnchor: "", + }) + if err != nil { + writeResult(w, nil, err) + return + } + if result.Invoice != nil && result.Invoice.ID != "" { + h.emitRenewalWebhooks(r, result.PaymentResult, "subscription_update") + } else { + h.emitSubscriptionWebhook(r, "customer.subscription.updated", result.Subscription, webhooks.SourceAPI) + } + updated = result.Subscription + } else { + // none / unspecified: item replacement only (legacy path; no proration invoice). + updated, err = h.billing.PatchSubscription(r.Context(), subscription.ID, billing.SubscriptionPatch{ + Items: items, + ReplaceItems: true, + }) + if err != nil { + writeResult(w, nil, err) + return + } + } + itemResp := h.stripeSubscriptionItem(r, updated, updated.Items[idx], idx) + if p.hasDefaultTaxRatesParam("tax_rates") { + itemResp["tax_rates"] = h.stripeTaxRateObjects(itemTaxRates) + } + if meta := p.metadata(); len(meta) > 0 { + itemResp["metadata"] = meta + } + writeJSON(w, http.StatusOK, itemResp) +} + +func (h *Handler) handleSubscriptionItemDelete(w http.ResponseWriter, r *http.Request, id string) { p, err := parseParamsAllowingDeleteBody(r) if err != nil { writeError(w, http.StatusBadRequest, err) diff --git a/internal/api/discounts.go b/internal/api/discounts.go index 9fc1904..5591945 100644 --- a/internal/api/discounts.go +++ b/internal/api/discounts.go @@ -223,7 +223,12 @@ func (h *Handler) handleSubscriptionDiscount(w http.ResponseWriter, r *http.Requ case http.MethodGet: writeJSON(w, http.StatusOK, h.stripeDiscount(discounts[0], subscription.CustomerID, subscription.ID, "")) case http.MethodDelete: - metadata := billing.ClearDiscountMetadata(copyStringMap(subscription.Metadata)) + // PatchSubscription merges metadata; zeroed keys are what actually + // remove the discount from the stored subscription. + metadata := copyStringMap(subscription.Metadata) + for key, value := range billing.ZeroDiscountMetadata() { + metadata[key] = value + } updated, err := h.billing.PatchSubscription(r.Context(), subscription.ID, billing.SubscriptionPatch{ Metadata: metadata, TimelineSource: "api", diff --git a/internal/api/local_evidence.go b/internal/api/local_evidence.go index 22c48cb..8bdb5c8 100644 --- a/internal/api/local_evidence.go +++ b/internal/api/local_evidence.go @@ -17,13 +17,14 @@ import ( // Evidence kinds. These are the persistence keys, so renaming one orphans the // rows already written under the old name. const ( - kindCoupon = "coupon" - kindPromotionCode = "promotion_code" - kindSchedule = "schedule" - kindDispute = "dispute" - kindTaxRate = "tax_rate" - kindTaxID = "tax_id" - kindCash = "cash" + kindCoupon = "coupon" + kindPromotionCode = "promotion_code" + kindSchedule = "schedule" + kindDispute = "dispute" + kindTaxRate = "tax_rate" + kindTaxID = "tax_id" + kindCash = "cash" + kindPortalConfiguration = "portal_configuration" ) // LocalEvidenceRepository persists evidence objects in the run's own store, so a @@ -35,31 +36,33 @@ type LocalEvidenceRepository interface { } type localEvidenceStore struct { - mu sync.Mutex - repo LocalEvidenceRepository - coupons map[string]map[string]any - promotionCodes map[string]map[string]any - schedules map[string]map[string]any - cashBalances map[string]int64 - cashTxs map[string][]map[string]any - disputes map[string]map[string]any - taxRates map[string]map[string]any - taxIDs map[string]map[string]any + mu sync.Mutex + repo LocalEvidenceRepository + coupons map[string]map[string]any + promotionCodes map[string]map[string]any + schedules map[string]map[string]any + cashBalances map[string]int64 + cashTxs map[string][]map[string]any + disputes map[string]map[string]any + taxRates map[string]map[string]any + taxIDs map[string]map[string]any + portalConfigurations map[string]map[string]any } // newLocalEvidenceStore returns an evidence store. A nil repo keeps everything in // memory, which is what callers without a store (scorecard runs, unit tests) want. func newLocalEvidenceStore(repo LocalEvidenceRepository) *localEvidenceStore { s := &localEvidenceStore{ - repo: repo, - coupons: map[string]map[string]any{}, - promotionCodes: map[string]map[string]any{}, - schedules: map[string]map[string]any{}, - cashBalances: map[string]int64{}, - cashTxs: map[string][]map[string]any{}, - disputes: map[string]map[string]any{}, - taxRates: map[string]map[string]any{}, - taxIDs: map[string]map[string]any{}, + repo: repo, + coupons: map[string]map[string]any{}, + promotionCodes: map[string]map[string]any{}, + schedules: map[string]map[string]any{}, + cashBalances: map[string]int64{}, + cashTxs: map[string][]map[string]any{}, + disputes: map[string]map[string]any{}, + taxRates: map[string]map[string]any{}, + taxIDs: map[string]map[string]any{}, + portalConfigurations: map[string]map[string]any{}, } s.restore() return s @@ -79,6 +82,8 @@ func (s *localEvidenceStore) mapFor(kind string) map[string]map[string]any { return s.taxRates case kindTaxID: return s.taxIDs + case kindPortalConfiguration: + return s.portalConfigurations } return nil } diff --git a/internal/api/portal_configurations.go b/internal/api/portal_configurations.go new file mode 100644 index 0000000..94c5246 --- /dev/null +++ b/internal/api/portal_configurations.go @@ -0,0 +1,280 @@ +package api + +import ( + "fmt" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/hckim/billtap/internal/billing" +) + +// defaultPortalFeatures mirrors the Stripe default portal feature set. The +// hosted portal itself is a local stub, so these drive evidence and echo only. +func defaultPortalFeatures() map[string]any { + return map[string]any{ + "customer_update": map[string]any{ + "enabled": true, + "allowed_updates": []string{"email", "name"}, + }, + "invoice_history": map[string]any{ + "enabled": true, + }, + "payment_method_update": map[string]any{ + "enabled": true, + }, + "subscription_cancel": map[string]any{ + "enabled": true, + "mode": "at_period_end", + "cancellation_reason": nil, + "proration_behavior": nil, + }, + "subscription_update": map[string]any{ + "enabled": true, + "proration_behavior": "none", + "default_allowed_updates": []string{"price", "quantity"}, + }, + } +} + +func portalFeaturesFromParams(p params, features map[string]any) map[string]any { + if features == nil { + features = defaultPortalFeatures() + } + feature := func(name string) map[string]any { + existing, _ := features[name].(map[string]any) + if existing == nil { + existing = map[string]any{} + features[name] = existing + } + return existing + } + if p.has("features[customer_update][enabled]") { + feature("customer_update")["enabled"] = p.boolDefault("features[customer_update][enabled]", true) + } + if allowed := p.list("features[customer_update][allowed_updates]"); len(allowed) > 0 { + feature("customer_update")["allowed_updates"] = allowed + } + if p.has("features[invoice_history][enabled]") { + feature("invoice_history")["enabled"] = p.boolDefault("features[invoice_history][enabled]", true) + } + if p.has("features[payment_method_update][enabled]") { + feature("payment_method_update")["enabled"] = p.boolDefault("features[payment_method_update][enabled]", true) + } + cancel := feature("subscription_cancel") + if p.has("features[subscription_cancel][enabled]") { + cancel["enabled"] = p.boolDefault("features[subscription_cancel][enabled]", true) + } + if p.has("features[subscription_cancel][mode]") { + cancel["mode"] = p.string("features[subscription_cancel][mode]") + } + if p.has("features[subscription_cancel][cancellation_reason]") { + cancel["cancellation_reason"] = emptyToNil(p.string("features[subscription_cancel][cancellation_reason]")) + } + if p.has("features[subscription_cancel][proration_behavior]") { + cancel["proration_behavior"] = emptyToNil(p.string("features[subscription_cancel][proration_behavior]")) + } + update := feature("subscription_update") + if p.has("features[subscription_update][enabled]") { + update["enabled"] = p.boolDefault("features[subscription_update][enabled]", true) + } + if p.has("features[subscription_update][proration_behavior]") { + update["proration_behavior"] = p.string("features[subscription_update][proration_behavior]") + } + if allowed := p.list("features[subscription_update][default_allowed_updates]"); len(allowed) > 0 { + update["default_allowed_updates"] = allowed + } + return features +} + +func (h *Handler) handleBillingPortalConfigurations(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateBillingPortalConfigurationCreate(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + now := time.Now().UTC() + id := "bpc_" + strconv.FormatInt(now.UnixNano(), 36) + h.local.mu.Lock() + isDefault := len(h.local.portalConfigurations) == 0 + configuration := map[string]any{ + "id": id, + "object": "billing_portal.configuration", + "active": true, + "is_default": isDefault, + "business_profile": portalBusinessProfileFromParams(p, nil), + "default_return_url": emptyToNil(p.string("default_return_url")), + "features": portalFeaturesFromParams(p, nil), + "login_page": portalLoginPageFromParams(p, nil), + "metadata": nonNilMap(p.metadata()), + "created": now.Unix(), + "updated": now.Unix(), + "livemode": false, + } + err = h.local.saveLocked(kindPortalConfiguration, id, configuration) + h.local.mu.Unlock() + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + writeJSON(w, http.StatusOK, cloneEvidence(configuration)) + case http.MethodGet: + h.local.mu.Lock() + data := evidenceList(h.local.portalConfigurations) + h.local.mu.Unlock() + // Evidence maps iterate in random order; sort so starting_after pagination is stable. + sort.Slice(data, func(i, j int) bool { + return fmt.Sprint(data[i]["id"]) < fmt.Sprint(data[j]["id"]) + }) + data = filterPortalConfigurations(data, r) + writeJSON(w, http.StatusOK, stripeListFromRequest(r, data)) + default: + h.methodNotAllowed(w, r, "GET, POST") + } +} + +func (h *Handler) handleBillingPortalConfiguration(w http.ResponseWriter, r *http.Request) { + id := strings.Trim(strings.TrimPrefix(r.URL.Path, "/v1/billing_portal/configurations/"), "/") + if id == "" || strings.Contains(id, "/") { + h.notFound(w, r) + return + } + h.local.mu.Lock() + configuration, ok := h.local.portalConfigurations[id] + h.local.mu.Unlock() + if !ok { + writeResult(w, nil, billing.ErrNotFound) + return + } + switch r.Method { + case http.MethodGet: + writeJSON(w, http.StatusOK, cloneEvidence(configuration)) + case http.MethodPost: + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateBillingPortalConfigurationUpdate(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + h.local.mu.Lock() + current := h.local.portalConfigurations[id] + if p.has("active") { + current["active"] = p.boolDefault("active", true) + } + if p.has("business_profile[headline]") || p.has("business_profile[privacy_policy_url]") || p.has("business_profile[terms_of_service_url]") { + current["business_profile"] = portalBusinessProfileFromParams(p, current["business_profile"]) + } + if p.has("default_return_url") { + current["default_return_url"] = emptyToNil(p.string("default_return_url")) + } + if p.has("login_page[logo_url]") { + current["login_page"] = portalLoginPageFromParams(p, current["login_page"]) + } + if portalFeaturesTouched(p) { + features, _ := current["features"].(map[string]any) + current["features"] = portalFeaturesFromParams(p, features) + } + if metadata := p.metadata(); metadata != nil { + merged := map[string]string{} + if existing, ok := current["metadata"].(map[string]string); ok { + for key, value := range existing { + merged[key] = value + } + } + for key, value := range metadata { + merged[key] = value + } + current["metadata"] = nonNilMap(merged) + } + current["updated"] = time.Now().UTC().Unix() + err = h.local.saveLocked(kindPortalConfiguration, id, current) + h.local.mu.Unlock() + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + writeJSON(w, http.StatusOK, cloneEvidence(current)) + default: + h.methodNotAllowed(w, r, "GET, POST") + } +} + +func portalBusinessProfileFromParams(p params, existing any) map[string]any { + profile := map[string]any{"headline": nil, "privacy_policy_url": nil, "terms_of_service_url": nil} + if current, ok := existing.(map[string]any); ok { + for key := range profile { + profile[key] = current[key] + } + } + if p.has("business_profile[headline]") { + profile["headline"] = emptyToNil(p.string("business_profile[headline]")) + } + if p.has("business_profile[privacy_policy_url]") { + profile["privacy_policy_url"] = emptyToNil(p.string("business_profile[privacy_policy_url]")) + } + if p.has("business_profile[terms_of_service_url]") { + profile["terms_of_service_url"] = emptyToNil(p.string("business_profile[terms_of_service_url]")) + } + return profile +} + +func portalLoginPageFromParams(p params, existing any) map[string]any { + loginPage := map[string]any{"enabled": false, "logo_url": nil} + if current, ok := existing.(map[string]any); ok { + for key := range loginPage { + loginPage[key] = current[key] + } + } + if p.has("login_page[logo_url]") { + loginPage["logo_url"] = emptyToNil(p.string("login_page[logo_url]")) + } + return loginPage +} + +func portalFeaturesTouched(p params) bool { + for key := range p.values { + if strings.HasPrefix(key, "features[") { + return true + } + } + return false +} + +func filterPortalConfigurations(data []map[string]any, r *http.Request) []map[string]any { + query := r.URL.Query() + activeFilter := strings.TrimSpace(query.Get("active")) + defaultFilter := strings.TrimSpace(query.Get("is_default")) + if activeFilter == "" && defaultFilter == "" { + return data + } + out := make([]map[string]any, 0, len(data)) + for _, configuration := range data { + if activeFilter != "" { + wantActive := activeFilter == "true" || activeFilter == "1" + active, _ := configuration["active"].(bool) + if active != wantActive { + continue + } + } + if defaultFilter != "" { + wantDefault := defaultFilter == "true" || defaultFilter == "1" + isDefault, _ := configuration["is_default"].(bool) + if isDefault != wantDefault { + continue + } + } + out = append(out, configuration) + } + return out +} diff --git a/internal/api/portal_configurations_test.go b/internal/api/portal_configurations_test.go new file mode 100644 index 0000000..b2b0561 --- /dev/null +++ b/internal/api/portal_configurations_test.go @@ -0,0 +1,150 @@ +package api + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func TestBillingPortalConfigurationsCRUD(t *testing.T) { + handler := newTestHandler(t) + + // Unknown param → 400 parameter_unknown. + status, body := postFormStatus(t, handler, "/v1/billing_portal/configurations", url.Values{ + "business_profile[name]": {"Nope"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("unknown business_profile key status = %d body = %s, want 400", status, body) + } + // Invalid feature enum → 400. + status, body = postFormStatus(t, handler, "/v1/billing_portal/configurations", url.Values{ + "features[subscription_cancel][mode]": {"whenever"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("invalid cancel mode status = %d body = %s, want 400", status, body) + } + // Invalid allowed_updates member → 400. + status, body = postFormStatus(t, handler, "/v1/billing_portal/configurations", url.Values{ + "features[customer_update][allowed_updates][0]": {"address"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("invalid allowed_updates status = %d body = %s, want 400", status, body) + } + + created := postForm[map[string]any](t, handler, "/v1/billing_portal/configurations", url.Values{ + "business_profile[headline]": {"Acme Billing"}, + "business_profile[privacy_policy_url]": {"https://acme.test/privacy"}, + "default_return_url": {"https://acme.test/return"}, + "login_page[logo_url]": {"https://acme.test/logo.png"}, + "features[customer_update][enabled]": {"true"}, + "features[customer_update][allowed_updates][0]": {"email"}, + "features[subscription_cancel][mode]": {"immediately"}, + "features[subscription_update][proration_behavior]": {"always_invoice"}, + "metadata[env]": {"ci"}, + }) + id := fmt.Sprint(created["id"]) + if id == "" || created["object"] != "billing_portal.configuration" { + t.Fatalf("created configuration = %#v, want billing_portal.configuration with id", created) + } + if created["active"] != true || created["is_default"] != true || created["livemode"] != false { + t.Fatalf("created flags = %#v, want active default non-live", created) + } + profile, _ := created["business_profile"].(map[string]any) + if profile["headline"] != "Acme Billing" || profile["privacy_policy_url"] != "https://acme.test/privacy" || profile["terms_of_service_url"] != nil { + t.Fatalf("created business_profile = %#v", profile) + } + features, _ := created["features"].(map[string]any) + cancel, _ := features["subscription_cancel"].(map[string]any) + if cancel["mode"] != "immediately" { + t.Fatalf("created subscription_cancel = %#v, want mode immediately", cancel) + } + customerUpdate, _ := features["customer_update"].(map[string]any) + allowed, _ := customerUpdate["allowed_updates"].([]any) + if customerUpdate["enabled"] != true || len(allowed) != 1 || fmt.Sprint(allowed[0]) != "email" { + t.Fatalf("created customer_update = %#v, want enabled with email only", customerUpdate) + } + update, _ := features["subscription_update"].(map[string]any) + if update["proration_behavior"] != "always_invoice" { + t.Fatalf("created subscription_update = %#v, want always_invoice", update) + } + if _, ok := features["invoice_history"]; !ok { + t.Fatalf("created features missing default invoice_history: %#v", features) + } + loginPage, _ := created["login_page"].(map[string]any) + if fmt.Sprint(loginPage["logo_url"]) != "https://acme.test/logo.png" { + t.Fatalf("created login_page = %#v", loginPage) + } + meta, _ := created["metadata"].(map[string]any) + if fmt.Sprint(meta["env"]) != "ci" { + t.Fatalf("created metadata = %#v, want env=ci", created["metadata"]) + } + + fetched := getJSON[map[string]any](t, handler, "/v1/billing_portal/configurations/"+id) + if fetched["id"] != id || fetched["default_return_url"] != "https://acme.test/return" { + t.Fatalf("GET configuration = %#v, want %s with return url", fetched, id) + } + + // Second configuration is not the default; list filters narrow by flag. + second := postForm[map[string]any](t, handler, "/v1/billing_portal/configurations", url.Values{ + "business_profile[headline]": {"Secondary"}, + }) + if second["is_default"] != false { + t.Fatalf("second configuration is_default = %#v, want false", second["is_default"]) + } + listed := getJSON[struct { + Data []map[string]any `json:"data"` + }](t, handler, "/v1/billing_portal/configurations") + if len(listed.Data) != 2 { + t.Fatalf("list configurations = %d items, want 2", len(listed.Data)) + } + defaults := getJSON[struct { + Data []map[string]any `json:"data"` + }](t, handler, "/v1/billing_portal/configurations?is_default=true") + if len(defaults.Data) != 1 || fmt.Sprint(defaults.Data[0]["id"]) != id { + t.Fatalf("is_default filter = %#v, want the first configuration only", defaults.Data) + } + + // Retrieve misses are 404. + missingReq := httptest.NewRequest(http.MethodGet, "/v1/billing_portal/configurations/bpc_missing", nil) + missingRec := httptest.NewRecorder() + handler.ServeHTTP(missingRec, missingReq) + if missingRec.Code != http.StatusNotFound { + t.Fatalf("missing configuration status = %d body = %s, want 404", missingRec.Code, missingRec.Body.String()) + } + + updated := postForm[map[string]any](t, handler, "/v1/billing_portal/configurations/"+id, url.Values{ + "active": {"false"}, + "business_profile[headline]": {"Acme Billing v2"}, + "features[subscription_cancel][mode]": {"at_period_end"}, + "features[invoice_history][enabled]": {"false"}, + "metadata[env]": {"ci2"}, + }) + if updated["active"] != false { + t.Fatalf("updated active = %#v, want false", updated["active"]) + } + profile, _ = updated["business_profile"].(map[string]any) + if profile["headline"] != "Acme Billing v2" || profile["privacy_policy_url"] != "https://acme.test/privacy" { + t.Fatalf("updated business_profile = %#v, want new headline keeping privacy url", profile) + } + features, _ = updated["features"].(map[string]any) + cancel, _ = features["subscription_cancel"].(map[string]any) + if cancel["mode"] != "at_period_end" { + t.Fatalf("updated subscription_cancel = %#v, want at_period_end", cancel) + } + invoiceHistory, _ := features["invoice_history"].(map[string]any) + if invoiceHistory["enabled"] != false { + t.Fatalf("updated invoice_history = %#v, want disabled", invoiceHistory) + } + meta, _ = updated["metadata"].(map[string]any) + if fmt.Sprint(meta["env"]) != "ci2" { + t.Fatalf("updated metadata = %#v, want merged env=ci2", updated["metadata"]) + } + activeFiltered := getJSON[struct { + Data []map[string]any `json:"data"` + }](t, handler, "/v1/billing_portal/configurations?active=false") + if len(activeFiltered.Data) != 1 || fmt.Sprint(activeFiltered.Data[0]["id"]) != id { + t.Fatalf("active=false filter = %#v, want the updated configuration", activeFiltered.Data) + } +} diff --git a/internal/api/subscription_item_read_update_test.go b/internal/api/subscription_item_read_update_test.go new file mode 100644 index 0000000..e5ec740 --- /dev/null +++ b/internal/api/subscription_item_read_update_test.go @@ -0,0 +1,223 @@ +package api + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/hckim/billtap/internal/billing" +) + +type itemReadResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Subscription string `json:"subscription"` + Quantity int64 `json:"quantity"` + Price struct { + ID string `json:"id"` + } `json:"price"` + Metadata map[string]string `json:"metadata"` +} + +func setupItemReadFixtures(t *testing.T, handler http.Handler) (customer billing.Customer, first, second billing.Price, subscription prorationSubResponse) { + t.Helper() + customer = postForm[billing.Customer](t, handler, "/v1/customers", url.Values{"email": {"items-read@example.test"}}) + product := postForm[billing.Product](t, handler, "/v1/products", url.Values{"name": {"Item Read Plan"}}) + first = postForm[billing.Price](t, handler, "/v1/prices", url.Values{ + "product": {product.ID}, + "currency": {"usd"}, + "unit_amount": {"5000"}, + "recurring[interval]": {"month"}, + }) + second = postForm[billing.Price](t, handler, "/v1/prices", url.Values{ + "product": {product.ID}, + "currency": {"usd"}, + "unit_amount": {"7000"}, + "recurring[interval]": {"month"}, + }) + subscription = postForm[prorationSubResponse](t, handler, "/v1/subscriptions", url.Values{ + "customer": {customer.ID}, + "items[0][price]": {first.ID}, + "items[0][quantity]": {"1"}, + }) + return customer, first, second, subscription +} + +func TestSubscriptionItemListAndRetrieve(t *testing.T) { + handler := newTestHandler(t) + _, first, _, subscription := setupItemReadFixtures(t, handler) + secondItem := postForm[itemReadResponse](t, handler, "/v1/subscription_items", url.Values{ + "subscription": {subscription.ID}, + "price": {first.ID}, + "quantity": {"2"}, + }) + + listed := getJSON[struct { + Data []itemReadResponse `json:"data"` + }](t, handler, "/v1/subscription_items") + if len(listed.Data) != 2 { + t.Fatalf("list items = %d, want 2", len(listed.Data)) + } + for _, item := range listed.Data { + if item.Subscription != subscription.ID || item.Price.ID != first.ID { + t.Fatalf("listed item = %#v, want subscription %s price %s", item, subscription.ID, first.ID) + } + } + + filtered := getJSON[struct { + Data []itemReadResponse `json:"data"` + }](t, handler, "/v1/subscription_items?subscription=sub_other") + if len(filtered.Data) != 0 { + t.Fatalf("filtered items = %d, want 0 for other subscription", len(filtered.Data)) + } + + fetched := getJSON[itemReadResponse](t, handler, "/v1/subscription_items/"+secondItem.ID) + if fetched.ID != secondItem.ID || fetched.Quantity != 2 || fetched.Subscription != subscription.ID { + t.Fatalf("retrieved item = %#v, want created item", fetched) + } + + missingReq := httptest.NewRequest(http.MethodGet, "/v1/subscription_items/si_missing", nil) + missingRec := httptest.NewRecorder() + handler.ServeHTTP(missingRec, missingReq) + if missingRec.Code != http.StatusNotFound { + t.Fatalf("missing item status = %d body = %s, want 404", missingRec.Code, missingRec.Body.String()) + } + + // List validation: unknown filter params are rejected. + badReq := httptest.NewRequest(http.MethodGet, "/v1/subscription_items?price="+first.ID, nil) + badRec := httptest.NewRecorder() + handler.ServeHTTP(badRec, badReq) + if badRec.Code != http.StatusBadRequest { + t.Fatalf("unknown list filter status = %d body = %s, want 400", badRec.Code, badRec.Body.String()) + } +} + +func TestSubscriptionItemUpdateQuantityAndPrice(t *testing.T) { + handler := newTestHandler(t) + _, first, second, subscription := setupItemReadFixtures(t, handler) + itemID := subscription.Items.Data[0].ID + + // Unknown params are rejected. + status, body := postFormStatus(t, handler, "/v1/subscription_items/"+itemID, url.Values{ + "payment_behavior": {"pending_if_incomplete"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("unknown update param status = %d body = %s, want 400", status, body) + } + // quantity must stay positive. + status, body = postFormStatus(t, handler, "/v1/subscription_items/"+itemID, url.Values{ + "quantity": {"0"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("zero quantity status = %d body = %s, want 400", status, body) + } + // Missing item is 404. + status, body = postFormStatus(t, handler, "/v1/subscription_items/si_missing", url.Values{ + "quantity": {"2"}, + }) + if status != http.StatusNotFound { + t.Fatalf("missing item update status = %d body = %s, want 404", status, body) + } + + updated := postForm[itemReadResponse](t, handler, "/v1/subscription_items/"+itemID, url.Values{ + "quantity": {"3"}, + "metadata[k]": {"v"}, + "proration_behavior": {"none"}, + }) + if updated.ID != itemID || updated.Quantity != 3 || updated.Price.ID != first.ID { + t.Fatalf("updated item = %#v, want same id quantity 3 price %s", updated, first.ID) + } + if updated.Metadata["k"] != "v" { + t.Fatalf("updated item metadata = %#v, want k=v", updated.Metadata) + } + + // Price swap keeps the item id and subscription shape. + swapped := postForm[itemReadResponse](t, handler, "/v1/subscription_items/"+itemID, url.Values{ + "price": {second.ID}, + }) + if swapped.ID != itemID || swapped.Price.ID != second.ID { + t.Fatalf("swapped item = %#v, want same id price %s", swapped, second.ID) + } + subscriptionAfter := getJSON[prorationSubResponse](t, handler, "/v1/subscriptions/"+subscription.ID) + if len(subscriptionAfter.Items.Data) != 1 || subscriptionAfter.Items.Data[0].ID != itemID { + t.Fatalf("subscription after item update = %#v, want single stable item", subscriptionAfter.Items.Data) + } + if subscriptionAfter.Items.Data[0].Price.ID != second.ID { + t.Fatalf("subscription item price after swap = %s, want %s", subscriptionAfter.Items.Data[0].Price.ID, second.ID) + } +} + +func TestSubscriptionItemUpdateAlwaysInvoiceBillsDelta(t *testing.T) { + handler := newTestHandler(t) + _, _, _, subscription := setupItemReadFixtures(t, handler) + itemID := subscription.Items.Data[0].ID + + updated := postForm[itemReadResponse](t, handler, "/v1/subscription_items/"+itemID, url.Values{ + "quantity": {"2"}, + "proration_behavior": {"always_invoice"}, + "proration_date": {fmt.Sprint(subscription.CurrentPeriodStart)}, + }) + if updated.Quantity != 2 || updated.ID != itemID { + t.Fatalf("always_invoice item update = %#v, want quantity 2 stable id", updated) + } + after := getJSON[prorationSubResponse](t, handler, "/v1/subscriptions/"+subscription.ID) + if after.LatestInvoice == subscription.LatestInvoice { + t.Fatalf("latest_invoice unchanged after always_invoice update: %s", after.LatestInvoice) + } +} + +func TestNestedCustomerSubscriptionDiscountRoutes(t *testing.T) { + handler := newTestHandler(t) + customer, first, _, _ := setupItemReadFixtures(t, handler) + coupon := postForm[struct { + ID string `json:"id"` + }](t, handler, "/v1/coupons", url.Values{ + "percent_off": {"10"}, + "duration": {"forever"}, + }) + + discounted := postForm[prorationSubResponse](t, handler, "/v1/subscriptions", url.Values{ + "customer": {customer.ID}, + "items[0][price]": {first.ID}, + "items[0][quantity]": {"1"}, + "discounts[0][coupon]": {coupon.ID}, + }) + nestedPath := "/v1/customers/" + customer.ID + "/subscriptions/" + discounted.ID + "/discount" + + discount := getJSON[struct { + Object string `json:"object"` + Coupon struct { + ID string `json:"id"` + } `json:"coupon"` + Subscription string `json:"subscription"` + }](t, handler, nestedPath) + if discount.Object != "discount" || discount.Coupon.ID != coupon.ID || discount.Subscription != discounted.ID { + t.Fatalf("nested discount = %#v, want %s coupon on %s", discount, coupon.ID, discounted.ID) + } + + // The nested route is customer-scoped: another customer sees 404. + other := postForm[billing.Customer](t, handler, "/v1/customers", url.Values{"email": {"other@example.test"}}) + scopedReq := httptest.NewRequest(http.MethodGet, "/v1/customers/"+other.ID+"/subscriptions/"+discounted.ID+"/discount", nil) + scopedRec := httptest.NewRecorder() + handler.ServeHTTP(scopedRec, scopedReq) + if scopedRec.Code != http.StatusNotFound { + t.Fatalf("other-customer nested discount status = %d body = %s, want 404", scopedRec.Code, scopedRec.Body.String()) + } + + deleted := deleteForm[struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + }](t, handler, nestedPath, url.Values{}) + if !deleted.Deleted || deleted.Object != "discount" { + t.Fatalf("nested discount delete = %#v, want deleted discount", deleted) + } + afterReq := httptest.NewRequest(http.MethodGet, nestedPath, nil) + afterRec := httptest.NewRecorder() + handler.ServeHTTP(afterRec, afterReq) + if afterRec.Code != http.StatusNotFound { + t.Fatalf("nested discount after delete status = %d body = %s, want 404", afterRec.Code, afterRec.Body.String()) + } +} diff --git a/internal/api/validation.go b/internal/api/validation.go index f4e667a..a9a213f 100644 --- a/internal/api/validation.go +++ b/internal/api/validation.go @@ -51,6 +51,7 @@ var ( schedulePhaseParamRE = regexp.MustCompile(`^phases\[\d+\]\[(start_date|end_date|iterations|items|plans)\].*$`) invoicePreviewItemParamRE = regexp.MustCompile(`^((subscription_details|subscriptionDetails)\[items\]\[\d+\]\[(id|price|price_id|quantity)\]|(subscription_items|items)\[\d+\]\[(id|price|price_id|quantity)\])$`) invoicePaymentSettingsRE = regexp.MustCompile(`^payment_settings(\[[^\]]+\])+$`) + portalFeatureParamRE = regexp.MustCompile(`^features\[(customer_update|invoice_history|payment_method_update|subscription_cancel|subscription_update)\]\[(enabled|mode|proration_behavior|cancellation_reason|allowed_updates|default_allowed_updates)\](\[\d*\])?$`) ) var stripePaymentMethodTypes = []string{ @@ -1525,6 +1526,95 @@ func validateBillingPortalSessionCreate(p params) error { return nil } +func validateBillingPortalConfigurationCreate(p params) error { + if err := p.validate(paramSpec{ + Allowed: []string{ + "business_profile[headline]", + "business_profile[privacy_policy_url]", + "business_profile[terms_of_service_url]", + "default_return_url", + "login_page[logo_url]", + }, + AllowedRegex: []*regexp.Regexp{portalFeatureParamRE}, + EnumParams: map[string][]string{ + "features[subscription_cancel][mode]": {"at_period_end", "immediately"}, + "features[subscription_cancel][proration_behavior]": {"always_invoice", "create_prorations", "none"}, + "features[subscription_update][proration_behavior]": {"always_invoice", "create_prorations", "none"}, + }, + AllowMetadata: true, + }); err != nil { + return err + } + return validatePortalFeatureUpdateLists(p) +} + +func validateBillingPortalConfigurationUpdate(p params) error { + if err := p.validate(paramSpec{ + Allowed: []string{ + "active", + "business_profile[headline]", + "business_profile[privacy_policy_url]", + "business_profile[terms_of_service_url]", + "default_return_url", + "login_page[logo_url]", + }, + AllowedRegex: []*regexp.Regexp{portalFeatureParamRE}, + BoolParams: []string{"active"}, + EnumParams: map[string][]string{ + "features[subscription_cancel][mode]": {"at_period_end", "immediately"}, + "features[subscription_cancel][proration_behavior]": {"always_invoice", "create_prorations", "none"}, + "features[subscription_update][proration_behavior]": {"always_invoice", "create_prorations", "none"}, + }, + AllowMetadata: true, + }); err != nil { + return err + } + return validatePortalFeatureUpdateLists(p) +} + +func validatePortalFeatureUpdateLists(p params) error { + for _, allowed := range p.list("features[customer_update][allowed_updates]") { + if allowed != "email" && allowed != "name" { + return invalidParam("features[customer_update][allowed_updates]", "Invalid allowed_updates value. Allowed values are email, name.") + } + } + for _, allowed := range p.list("features[subscription_update][default_allowed_updates]") { + switch allowed { + case "price", "quantity": + default: + return invalidParam("features[subscription_update][default_allowed_updates]", "Invalid default_allowed_updates value. Allowed values are price, quantity.") + } + } + return nil +} + +func validateSubscriptionItemList(p params) error { + return p.validate(paramSpec{ + Allowed: []string{"ending_before", "expand", "limit", "starting_after", "subscription"}, + Int64Params: []string{"limit"}, + Positive: []string{"limit"}, + }) +} + +func validateSubscriptionItemUpdate(p params) error { + return p.validate(paramSpec{ + Allowed: []string{ + "price", + "price_id", + "quantity", + "proration_behavior", + "proration_date", + }, + AllowedRegex: []*regexp.Regexp{taxRatesParamRE}, + EnumParams: map[string][]string{ + "proration_behavior": {"always_invoice", "create_prorations", "none"}, + }, + Int64Params: []string{"quantity"}, + Positive: []string{"quantity"}, + AllowMetadata: true, + }) +} + func validatePaymentMethodList(p params) error { return p.validate(paramSpec{ Allowed: []string{"allow_redisplay", "customer", "customer_account", "ending_before", "limit", "starting_after", "type"}, diff --git a/internal/billing/service.go b/internal/billing/service.go index c7d824c..6691063 100644 --- a/internal/billing/service.go +++ b/internal/billing/service.go @@ -3260,7 +3260,26 @@ func ClearDiscountMetadata(metadata map[string]string) map[string]string { if metadata == nil { return metadata } - for _, key := range []string{ + for _, key := range DiscountMetadataKeys() { + delete(metadata, key) + } + return metadata +} + +// ZeroDiscountMetadata returns a metadata patch that clears the local discount +// keys. PatchSubscription merges metadata and deletes keys whose patch value is +// the empty string, so subscription patches need zeroed keys rather than a +// copy with the keys removed. +func ZeroDiscountMetadata() map[string]string { + patch := map[string]string{} + for _, key := range DiscountMetadataKeys() { + patch[key] = "" + } + return patch +} + +func DiscountMetadataKeys() []string { + return []string{ MetadataDiscountCouponID, MetadataDiscountPromotionCodeID, MetadataDiscountPercentOff, @@ -3269,10 +3288,7 @@ func ClearDiscountMetadata(metadata map[string]string) map[string]string { MetadataDiscountDuration, MetadataDiscountCreated, MetadataDiscountAppliesTo, - } { - delete(metadata, key) } - return metadata } func DiscountsFromMetadata(metadata map[string]string) []Discount { diff --git a/internal/stripecompat/registry.go b/internal/stripecompat/registry.go index 040fdad..7f03537 100644 --- a/internal/stripecompat/registry.go +++ b/internal/stripecompat/registry.go @@ -209,7 +209,13 @@ func DefaultClaims() []Claim { add(http.MethodPost, "/v1/checkout/sessions", Claim{Level: "L4", Stateful: true, ScorecardCases: []string{"checkout.sessions.create.java_sdk_optional_params"}, SDKSmoke: []string{"stripe-node"}, Risks: []string{"subscription and payment modes only (setup unsupported)", "automatic_tax is a metadata-driven simulation (customer metadata tax_percent); no jurisdiction or address-based calculation", "subscription_data[default_tax_rates] snapshots tax rates onto checkout/subscription totals (exclusive/inclusive math); mutually exclusive with automatic_tax"}}) add(http.MethodGet, "/v1/checkout/sessions", Claim{Level: "L4", Stateful: true, SDKSmoke: []string{"stripe-node"}, Risks: []string{"subscription and payment modes only (setup unsupported)"}}) add(http.MethodGet, "/v1/checkout/sessions/{id}", Claim{Level: "L4", Stateful: true, SDKSmoke: []string{"stripe-node"}, Risks: []string{"subscription and payment modes only (setup unsupported)"}}) + add(http.MethodPost, "/v1/checkout/sessions/{id}/expire", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"checkout.session.expired"}, Risks: []string{"expires open sessions only; non-open sessions are rejected and payment recovery is not modeled"}}) add(http.MethodPost, "/v1/billing_portal/sessions", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.updated", "customer.subscription.deleted", "payment_method.attached", "customer.updated"}, Risks: []string{"hosted portal is a local stub; portal configuration rendering and full Stripe-hosted portal behavior are not modeled"}}) + portalConfigurationRisk := []string{"local portal configuration evidence only; the hosted portal does not render configuration features"} + add(http.MethodGet, "/v1/billing_portal/configurations", Claim{Level: "L2", Stateful: true, Risks: portalConfigurationRisk}) + add(http.MethodPost, "/v1/billing_portal/configurations", Claim{Level: "L3", Stateful: true, Risks: portalConfigurationRisk}) + add(http.MethodGet, "/v1/billing_portal/configurations/{id}", Claim{Level: "L2", Stateful: true, Risks: portalConfigurationRisk}) + add(http.MethodPost, "/v1/billing_portal/configurations/{id}", Claim{Level: "L3", Stateful: true, Risks: portalConfigurationRisk}) taxRateRisk := []string{"local tax-rate evidence; default_tax_rates snapshots apply rates to checkout/subscription/invoice/renewal totals (inclusive+exclusive math); automatic_tax remains a separate customer-metadata simulation"} add(http.MethodGet, "/v1/tax_rates", Claim{Level: "L2", Stateful: true, Risks: taxRateRisk}) @@ -240,8 +246,13 @@ func DefaultClaims() []Claim { add(http.MethodGet, "/v1/customers/{id}/subscriptions/{id}", Claim{Level: "L3", Stateful: true}) add(http.MethodPost, "/v1/customers/{id}/subscriptions/{id}", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.updated"}}) add(http.MethodDelete, "/v1/customers/{id}/subscriptions/{id}", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.deleted"}}) + add(http.MethodGet, "/v1/customers/{id}/subscriptions/{id}/discount", Claim{Level: "L3", Stateful: true, Risks: []string{"returns the subscription's single local discount only"}}) + add(http.MethodDelete, "/v1/customers/{id}/subscriptions/{id}/discount", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.discount.deleted", "customer.subscription.updated"}, Risks: []string{"clears the subscription's single local discount only"}}) add(http.MethodPost, "/v1/subscription_items", Claim{Level: "L3", Stateful: true, ScorecardCases: []string{"subscription_items.create.invalid_quantity"}, SDKSmoke: []string{"stripe-node"}, Risks: []string{"proration_behavior always_invoice/create_prorations reuses subscription-update proration (subscription default_tax_rates only; item tax_rates evidence-only)", "item IDs keep the si__ shape but are stored at creation, so deletes no longer shift later item IDs; subscriptions stored before that change are backfilled with their position-derived IDs on next write"}}) + add(http.MethodGet, "/v1/subscription_items", Claim{Level: "L3", Stateful: true, Risks: []string{"lists resolved items across subscriptions; supports the subscription filter and standard list envelope only"}}) + add(http.MethodGet, "/v1/subscription_items/{id}", Claim{Level: "L3", Stateful: true}) + add(http.MethodPost, "/v1/subscription_items/{id}", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.updated"}, Risks: []string{"price/quantity replacement with the same proration subset as item create (always_invoice/create_prorations); payment_behavior and discounts are not modeled", "item tax_rates are evidence on the response only; totals use subscription default_tax_rates"}}) add(http.MethodDelete, "/v1/subscription_items/{id}", Claim{Level: "L3", Stateful: true, SDKSmoke: []string{"stripe-node"}, Risks: []string{"proration_behavior supported; last item delete rejected; clear_usage evidence-only (metered usage not modeled)", "stored item IDs stay stable after a middle-item delete; a later add reuses the lowest unused index"}}) add(http.MethodDelete, "/v1/subscriptions/{id}", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.deleted"}}) add(http.MethodPost, "/v1/subscriptions/{id}/resume", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.updated"}, Risks: []string{"clears local pause_collection evidence; billing-cycle and proration side effects are not modeled"}}) @@ -259,6 +270,8 @@ func DefaultClaims() []Claim { add(http.MethodPost, "/v1/invoices/{id}/finalize", Claim{Level: "L3", Stateful: true, ScorecardCases: []string{"invoices.one_time_invoice_flow.succeeds"}, WebhookEvents: []string{"invoice.finalized", "payment_intent.created", "invoice.sent"}, Risks: []string{"manual one-time invoice finalization only; automatic collection and full invoice lifecycle automation are not modeled"}}) add(http.MethodPost, "/v1/invoices/{id}/send", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"invoice.sent"}, Risks: []string{"local email evidence only (metadata + invoice.sent event); no real email delivery", "charge_automatically invoices also allowed as evidence-only send", "paid invoices can be re-sent (evidence only)"}}) add(http.MethodPost, "/v1/invoices/{id}/pay", Claim{Level: "L3", Stateful: true, ScorecardCases: []string{"invoices.pay.failed_invoice_succeeds", "invoices.pay.failed_invoice_declines_again", "invoices.one_time_invoice_flow.succeeds"}, WebhookEvents: []string{"payment_intent.succeeded", "payment_intent.payment_failed", "payment_intent.requires_action", "invoice.payment_succeeded", "invoice.payment_failed", "invoice.paid", "customer.subscription.updated"}, Risks: []string{"local retry/payment mutation only; send, void, collection, and dunning automation are not modeled"}}) + add(http.MethodPost, "/v1/invoices/{id}/void", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"invoice.voided"}, Risks: []string{"moves open invoices to void with local evidence only; unvoiding is not modeled"}}) + add(http.MethodPost, "/v1/invoices/{id}/mark_uncollectible", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"invoice.marked_uncollectible"}, Risks: []string{"moves open invoices to uncollectible with local evidence only; reopening is not modeled"}}) add(http.MethodGet, "/v1/invoices/{id}/lines", Claim{Level: "L2", Stateful: true, Risks: []string{"returns local invoice items for manual one-time invoices only"}}) add(http.MethodGet, "/v1/invoices/{id}/payments", Claim{Level: "L2", Stateful: true, Risks: []string{"returns local invoice payment evidence only"}}) add(http.MethodPost, "/v1/invoices/create_preview", Claim{Level: "L3", Stateful: true, Risks: []string{"local next-period upcoming when no item overrides (with pending create_prorations + default_tax_rates/automatic_tax); item overrides keep subscription-update proration; pending invoice items and full multi-item Stripe parity are not modeled"}}) diff --git a/internal/stripecompat/registry_test.go b/internal/stripecompat/registry_test.go index 451aaaf..590de8f 100644 --- a/internal/stripecompat/registry_test.go +++ b/internal/stripecompat/registry_test.go @@ -8,8 +8,8 @@ import ( func TestDefaultRegistryContainsCurrentPublicClaims(t *testing.T) { registry := DefaultRegistry() claims := registry.Claims() - if len(claims) != 181 { - t.Fatalf("default claims = %d, want 181", len(claims)) + if len(claims) != 193 { + t.Fatalf("default claims = %d, want 193", len(claims)) } checkout, ok := registry.Lookup(http.MethodPost, "/v1/checkout/sessions") @@ -103,6 +103,22 @@ func TestDefaultRegistryContainsCurrentPublicClaims(t *testing.T) { if !ok || portal.Level != "L3" || len(portal.WebhookEvents) < 2 { t.Fatalf("portal claim = %#v ok=%t, want webhook-backed L3 portal session", portal, ok) } + portalConfiguration, ok := registry.Lookup(http.MethodPost, "/v1/billing_portal/configurations/bpc_123") + if !ok || portalConfiguration.Level != "L3" || !portalConfiguration.Stateful { + t.Fatalf("portal configuration claim = %#v ok=%t, want L3 stateful", portalConfiguration, ok) + } + invoiceVoid, ok := registry.Lookup(http.MethodPost, "/v1/invoices/in_123/void") + if !ok || invoiceVoid.Level != "L3" || len(invoiceVoid.WebhookEvents) != 1 { + t.Fatalf("invoice void claim = %#v ok=%t, want L3 with invoice.voided", invoiceVoid, ok) + } + itemUpdate, ok := registry.Lookup(http.MethodPost, "/v1/subscription_items/si_123") + if !ok || itemUpdate.Level != "L3" || !itemUpdate.Stateful { + t.Fatalf("subscription item update claim = %#v ok=%t, want L3 stateful", itemUpdate, ok) + } + nestedDiscount, ok := registry.Lookup(http.MethodDelete, "/v1/customers/cus_123/subscriptions/sub_123/discount") + if !ok || nestedDiscount.Level != "L3" || !nestedDiscount.Stateful { + t.Fatalf("nested customer subscription discount claim = %#v ok=%t, want L3 stateful", nestedDiscount, ok) + } schedule, ok := registry.Lookup(http.MethodPost, "/v1/subscription_schedules/sub_sched_123/release") if !ok || schedule.Level != "L2" || !schedule.Stateful { t.Fatalf("subscription schedule release claim = %#v ok=%t, want L2 stateful", schedule, ok) From 7693a9f9c6795b124c2e9e84e6b641588a82cb63 Mon Sep 17 00:00:00 2001 From: midagedev Date: Sat, 29 Aug 2026 16:32:09 +0900 Subject: [PATCH 3/5] feat(api): checkout session update/line_items, draft invoice mutation, product delete GET /v1/checkout/sessions/{id}/line_items returns expanded Stripe item objects, and POST /v1/checkout/sessions/{id} updates open sessions (metadata merge plus line_items[N][quantity] overrides, prices immutable). Checkout closes to 6/6 OpenAPI operations. Draft invoices gain POST /v1/invoices/{id} (description, days_until_due, default_payment_method, metadata), DELETE /v1/invoices/{id} (removes attached lines and timeline evidence), and add_lines/update_lines/ remove_lines through the invoice-item path with subtotal/total/amount_due recomputation. DELETE /v1/products/{id} removes local product evidence; prices keep referencing the deleted id. Repository gains DeleteInvoice, UpdateInvoiceItem, DeleteInvoiceItem, and DeleteProduct; UpdateCheckoutSession now persists line_items so quantity overrides survive reloads. OpenAPI inventory: 187/587 (31.9%) -> 195/587 (33.2%); billing 36/39, catalog 28/54. --- CHANGELOG.md | 18 ++ README.md | 2 +- docs/COMPATIBILITY.md | 6 +- docs/COMPATIBILITY_TRACKING.md | 2 +- docs/GATE_STATUS.md | 6 +- docs/SIMULATION_CAPACITY_BACKLOG.md | 2 +- docs/STRIPE_API_COMPATIBILITY_ROADMAP.md | 8 +- docs/STRIPE_COMPATIBILITY_90_TARGET.md | 17 +- internal/api/api.go | 322 ++++++++++++++++++- internal/api/checkout_invoice_lines_test.go | 331 ++++++++++++++++++++ internal/api/validation.go | 55 ++++ internal/billing/service.go | 236 ++++++++++++++ internal/storage/billing.go | 130 +++++++- internal/stripecompat/registry.go | 8 + internal/stripecompat/registry_test.go | 4 +- 15 files changed, 1123 insertions(+), 24 deletions(-) create mode 100644 internal/api/checkout_invoice_lines_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7566aea..f36a9a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ ## Unreleased +- `GET /v1/checkout/sessions/{id}/line_items` returns the session's line items + as expanded Stripe `item` objects (pre-discount amounts; per-line + discount/tax splits are not modeled). +- `POST /v1/checkout/sessions/{id}` updates an open session: metadata merge + plus `line_items[N][quantity]` overrides with immutable prices. Non-open + sessions, out-of-range indexes, and non-positive quantities are rejected. + Checkout sessions are now full inventory: `6 / 6` operations. +- `POST /v1/invoices/{id}` updates draft invoices (description, `days_until_due`, + `default_payment_method`, metadata merge) and `DELETE /v1/invoices/{id}` + deletes a draft together with its attached lines and timeline evidence. +- `POST /v1/invoices/{id}/add_lines`, `update_lines`, and `remove_lines` mutate + draft-invoice lines through the invoice-item path, recomputing + `subtotal`/`total`/`amount_due` per change. +- `DELETE /v1/products/{id}` removes local product evidence; existing prices + keep referencing the deleted product id. +- The OpenAPI inventory moves from `187 / 587` (`31.9%`) to `195 / 587` + (`33.2%`): checkout closes to `6 / 6`, billing moves to `36 / 39`, and + catalog to `28 / 54`. - Added billing portal configurations: `GET/POST /v1/billing_portal/configurations` and `GET/POST /v1/billing_portal/configurations/{id}` store local portal-configuration diff --git a/README.md b/README.md index 26563c5..635092a 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ curl -fsS "http://localhost:8080/api/diagnostics?limit=100" \ | Fixtures | Apply/snapshot/assert APIs | JSON/YAML input, fixture metadata isolation, structured pass/fail reports | | SaaS profile | Generic workspace billing profile | Plans, seats, members, export quota, extra export, payment history, support bundle, platform/connect-style webhook evidence | | Release state | Source plus GHCR image | Local Docker image builds and GHCR image workflow; no package/Homebrew/signed binary yet | -| Stripe API inventory | `187 / 587` operations, `31.9%` L1+ | OpenAPI route inventory is schema-visible for all `587` operations; implemented coverage is tracked in `docs/STRIPE_COMPATIBILITY_90_TARGET.md` | +| Stripe API inventory | `195 / 587` operations, `33.2%` L1+ | OpenAPI route inventory is schema-visible for all `587` operations; implemented coverage is tracked in `docs/STRIPE_COMPATIBILITY_90_TARGET.md` | Detailed compatibility matrix: `docs/COMPATIBILITY.md`. diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 78c5b4f..7b22ee8 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -97,21 +97,21 @@ traffic can keep using the unprefixed service URL. | Resource | Endpoints | Level | Scope | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Customers | `POST /v1/customers`, `GET /v1/customers`, `GET /v1/customers/search`, `GET /v1/customers/{id}`, `POST /v1/customers/{id}` | Supported | Create, list, retrieve, update, and search `email`, `name`, and metadata. List supports `email` and `limit` filters. Search supports a measured Stripe Search Query Language subset for `id`, `email`, `name`, and metadata equality clauses joined by `AND`. | -| Products | `POST /v1/products`, `GET /v1/products`, `GET /v1/products/{id}`, `POST /v1/products/{id}` | Supported | Create, list, retrieve, and update local service products with metadata. | +| Products | `POST /v1/products`, `GET /v1/products`, `GET /v1/products/{id}`, `POST /v1/products/{id}`, `DELETE /v1/products/{id}` | Supported | Create, list, retrieve, update, and delete local service products with metadata. Deletion removes the product; existing prices keep referencing the deleted product id. | | Product search | `GET /v1/products/search` | Partial | Supports metadata equality filters such as `metadata['tenantId']:'saas'` and `active:'true'`. This is not Stripe Search Query Language parity. | | Prices | `POST /v1/prices`, `GET /v1/prices`, `GET /v1/prices/{id}`, `POST /v1/prices/{id}`, `GET /v1/prices/search` | Supported | Create, list, retrieve, update, and search prices. Supports `product`, `currency`, `unit_amount`, `lookup_key`, recurring interval fields, `active`, and metadata. List supports `product`, `active`, `type=recurring`, and `type=one_time`. Search supports a measured Stripe Search Query Language subset for `active`, `type`, `lookup_key`, and metadata equality clauses joined by `AND`, including one-time price lookup by `lookup_key`. | | Coupons | `POST /v1/coupons`, `GET /v1/coupons`, `GET /v1/coupons/{id}`, `POST /v1/coupons/{id}`, `DELETE /v1/coupons/{id}` | Partial | Create/list/retrieve local coupon evidence for SDK and fixture smoke paths. Amount-off, percent-off, duration, metadata, and deletion markers are modeled. A bounded single-discount subset applies coupons to customer defaults, checkout sessions, subscriptions, invoice preview math, and renewal invoices; redemption limits and expiration analytics are not modeled. | | Promotion codes | `POST /v1/promotion_codes`, `GET /v1/promotion_codes`, `GET /v1/promotion_codes/{id}`, `POST /v1/promotion_codes/{id}` | Partial | Create/list/retrieve local promotion-code evidence linked to a local coupon. List supports `code`, `coupon`, `customer`, and `active` filters. Promotion codes can be applied through `discounts[0][promotion_code]`; minimum amounts, expiration rules, and promotion analytics are not modeled. | | Tax rates | `GET/POST /v1/tax_rates`, `GET/POST /v1/tax_rates/{id}` | Partial | Create/list/retrieve/update local tax-rate evidence. Checkout `subscription_data[default_tax_rates][]` and subscription `default_tax_rates` resolve `txr_*` IDs and snapshot inclusive/exclusive rates onto sessions, subscriptions, invoices, renewal/proration math, and previews, applied after discounts; `automatic_tax` is mutually exclusive. Fixture packs can seed tax rates with explicit IDs. Jurisdiction and address-based calculation are not modeled; `automatic_tax` remains a customer-metadata `tax_percent` simulation. | | Customer tax IDs | `GET/POST /v1/customers/{id}/tax_ids`, `GET/DELETE /v1/customers/{id}/tax_ids/{id}` | Partial | Local customer tax-ID evidence store backing checkout `tax_id_collection[enabled]` flows. Provider verification is not modeled. | -| Checkout sessions | `POST /v1/checkout/sessions`, `GET /v1/checkout/sessions`, `GET /v1/checkout/sessions/{id}`, `POST /v1/checkout/sessions/{id}/expire` | Supported | Creates subscription- and payment-mode sandbox checkout sessions from request line items and hosted Billtap URLs; `setup` mode is rejected. The Stripe-style session response leaves `line_items` unexpanded. Accepts Stripe SDK form params `allow_promotion_codes`, `discounts[0][coupon]`, `discounts[0][promotion_code]`, `subscription_data[trial_period_days]`, and `subscription_data[default_tax_rates]` (applied to session totals; exclusive/inclusive snapshots; mutually exclusive with `automatic_tax`); trial checkout creates local `trialing` subscription evidence. Payment-mode sessions accept inline `price_data` line items (creating local product/price evidence), `payment_intent_data[...]`, `client_reference_id`, session-level `metadata[...]`, `automatic_tax[enabled]`, and `tax_id_collection[enabled]` (tax snapshot from customer metadata `tax_percent`), and complete into a one-time PaymentIntent without subscription or invoice evidence. `expire` moves an open session to `expired` and emits `checkout.session.expired`; non-open sessions return `invalid_request_error`. Hosted URLs use the request host by default, or `BILLTAP_PUBLIC_BASE_URL` when configured for container-to-host browser flows. `PUBLIC_BASE_PATH`, `BILLTAP_PUBLIC_BASE_PATH`, and `X-Forwarded-Prefix` are reflected in hosted browser URLs. | +| Checkout sessions | `POST /v1/checkout/sessions`, `GET /v1/checkout/sessions`, `GET /v1/checkout/sessions/{id}`, `POST /v1/checkout/sessions/{id}`, `POST /v1/checkout/sessions/{id}/expire`, `GET /v1/checkout/sessions/{id}/line_items` | Supported | Creates subscription- and payment-mode sandbox checkout sessions from request line items and hosted Billtap URLs; `setup` mode is rejected. The Stripe-style session response leaves `line_items` unexpanded; `GET .../line_items` returns them as expanded `item` objects with pre-discount amounts (per-line discount/tax splits are not modeled). Accepts Stripe SDK form params `allow_promotion_codes`, `discounts[0][coupon]`, `discounts[0][promotion_code]`, `subscription_data[trial_period_days]`, and `subscription_data[default_tax_rates]` (applied to session totals; exclusive/inclusive snapshots; mutually exclusive with `automatic_tax`); trial checkout creates local `trialing` subscription evidence. Payment-mode sessions accept inline `price_data` line items (creating local product/price evidence), `payment_intent_data[...]`, `client_reference_id`, session-level `metadata[...]`, `automatic_tax[enabled]`, and `tax_id_collection[enabled]` (tax snapshot from customer metadata `tax_percent`), and complete into a one-time PaymentIntent without subscription or invoice evidence. `POST .../sessions/{id}` updates an open session (metadata merge plus `line_items[N][quantity]` overrides with immutable prices); `expire` moves an open session to `expired` and emits `checkout.session.expired`; non-open sessions return `invalid_request_error`. Hosted URLs use the request host by default, or `BILLTAP_PUBLIC_BASE_URL` when configured for container-to-host browser flows. `PUBLIC_BASE_PATH`, `BILLTAP_PUBLIC_BASE_PATH`, and `X-Forwarded-Prefix` are reflected in hosted browser URLs. | | Checkout completion | `POST /v1/checkout/sessions/{id}/complete`, `POST /api/checkout/sessions/{id}/complete`, `POST/DELETE /api/checkout/sessions/{id}/promotion_code` | Billtap-specific | Completes a sandbox checkout and creates subscription, invoice, payment intent, timeline, and checkout webhook evidence. Supports success plus deterministic failure aliases such as `card_declined`, `insufficient_funds`, `expired_card`, `incorrect_cvc`, `processing_error`, `authentication_required`, `payment_pending`, `canceled`, and documented Stripe test PaymentMethod IDs such as `pm_card_visa_chargeDeclined`. Hosted checkout can apply or remove a promotion code on an open `allow_promotion_codes` session via `POST/DELETE /api/checkout/sessions/{id}/promotion_code` (form `promotion_code=`), refreshing session totals before completion. | | Billing portal sessions | `POST /v1/billing_portal/sessions` | Partial | Returns a Stripe-like `billing_portal.session` object and Billtap portal URL for a known customer, including `flow`, `locale`, `on_behalf_of`, `return_url`, and `livemode` fields. Accepts `customer`, `return_url`, optional `configuration`, `locale`, `on_behalf_of`, and `flow_data`, with enum and required nested-field validation for the modeled portal flows. The hosted portal can simulate saved payment-method updates and subscription cancellation, then redirect to `return_url`; full Stripe-hosted portal configuration rendering is not modeled. | | Billing portal configurations | `GET/POST /v1/billing_portal/configurations`, `GET/POST /v1/billing_portal/configurations/{id}` | Partial | Local portal-configuration evidence with Stripe-shaped `business_profile`, `default_return_url`, `login_page`, `features[...]`, and `metadata`. Create applies Stripe-shaped feature defaults; update merges business profile, features, and metadata and bumps `updated`. List filters by `active` and `is_default`; the first configuration in a run becomes the default. The hosted portal does not render configuration features. | | Subscriptions | `POST /v1/subscriptions`, `GET /v1/subscriptions`, `GET /v1/subscriptions/search`, `GET /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}`, `DELETE /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}/resume`, nested `GET/POST/DELETE /v1/customers/{id}/subscriptions...`, `GET/DELETE /v1/subscriptions/{id}/discount` | Partial | Create/list/retrieve subscriptions through the local checkout-completion state path. Update supports item replacement, metadata merge, single coupon or promotion-code discount application, `test_clock`, `cancel_at_period_end`, `pause_collection`, and accepted billing/proration evidence including `proration_date`, `proration_behavior`, `payment_behavior`, `billing_cycle_anchor`, and `trial_end`; resume clears local pause evidence and emits `customer.subscription.updated`. Top-level and nested customer lists support status and metadata equality filters such as `metadata[billtap_fixture_ref]`. Search supports `id`, `customer`, `status`, and metadata equality clauses joined by `AND`. Delete performs immediate sandbox cancellation. Test-clock and scenario clock advances can activate due trials, renew active periods with bounded discount math, fail configured renewals from subscription or customer metadata, and cancel period-end subscriptions in the local billing graph. | | Subscription schedules | `POST /v1/subscription_schedules`, `GET /v1/subscription_schedules`, `GET /v1/subscription_schedules/{id}`, `POST /v1/subscription_schedules/{id}`, `POST /v1/subscription_schedules/{id}/cancel`, `POST /v1/subscription_schedules/{id}/release` | Partial | Local schedule evidence for one due phase on an existing subscription. Test-clock advance applies a due phase by replacing subscription items and emits `customer.subscription.updated`; complex multi-phase billing, proration, invoices, and schedule lifecycle parity are not modeled. | | Subscription items | `POST /v1/subscription_items`, `GET /v1/subscription_items`, `GET /v1/subscription_items/{id}`, `POST /v1/subscription_items/{id}`, `DELETE /v1/subscription_items/{id}` | Partial | List, retrieve, add, update, and remove local subscription items. List supports the `subscription` filter; update replaces `price`/`quantity` and echoes metadata and evidence-only `tax_rates`. Create, update, and delete accept `proration_behavior` (`none`/`create_prorations`/`always_invoice`) and `proration_date` through the same proration path as subscription update, applying the subscription's `default_tax_rates` after discounts: `always_invoice` issues a paid `subscription_update` invoice and repoints `latest_invoice`, and `create_prorations` defers the delta to the next renewal invoice. Deleting the last item is rejected. Item IDs are stored at creation so deletes no longer shift later IDs. Item-level `tax_rates` are evidence-only, and metered usage is not modeled. | -| Invoices | `POST /v1/invoices`, `GET /v1/invoices`, `GET /v1/invoices/search`, `GET /v1/invoices/{id}`, `POST /v1/invoices/{id}/finalize`, `POST /v1/invoices/{id}/pay`, `POST /v1/invoices/{id}/send`, `POST /v1/invoices/{id}/void`, `POST /v1/invoices/{id}/mark_uncollectible`, `GET /v1/invoices/{id}/lines`, `GET /v1/invoices/{id}/payments`, `POST /v1/invoices/create_preview`, `GET/POST /v1/invoices/upcoming` | Partial | List/retrieve/search invoices created by checkout and create a bounded manual one-time invoice flow. Search supports `id`, `customer`, `subscription`, and `status` clauses joined by `AND`. `pay` retries open checkout invoices with deterministic sandbox `payment_method` or `source` aliases, mutating invoice, subscription, payment-intent, timeline, and webhook evidence. Manual invoices accept `customer`, `currency`, `collection_method`, `default_payment_method`, `description`, `auto_advance=false`, `pending_invoice_items_behavior`, `payment_settings[payment_method_types]`, and `metadata[...]`; `pending_invoice_items_behavior=include` attaches the customer's same-currency pending invoice items once at create. `finalize` creates local PaymentIntent evidence, and `pay` applies per-intent or customer-level default outcomes including success, card decline, and `requires_action`. `send` records local email evidence and emits `invoice.sent`; `void` moves an `open` invoice to `void` and emits `invoice.voided`; `mark_uncollectible` moves an `open` invoice to `uncollectible` and emits `invoice.marked_uncollectible`; non-open statuses return `invalid_request_error`. Invoice responses include `confirmation_secret`, local hosted/PDF paths, `payments.data.payment.payment_intent`, metadata, status transitions, automatic tax state, tax totals, payment settings, array-shaped `discounts`, `lines.data` serialized like the lines endpoint, and `post_payment_credit_notes_amount` on paid invoices. Preview endpoints calculate a Stripe-shaped subscription-update proration line from current period bounds, old/new price totals, `subscription_details[items]`, `subscription_details[proration_date]`, `subscription_details[proration_behavior]`, and `subscription_details[billing_cycle_anchor]`; previews without item overrides return the subscription's next billing cycle (items, discounts, and tax with `billing_reason: upcoming`) matching the next renewal invoice. A bounded single-discount subset updates `subtotal`, `total`, and `total_discount_amounts`; invoice rendering, automatic collection, and full dunning lifecycle are outside the modeled subset. | +| Invoices | `POST /v1/invoices`, `GET /v1/invoices`, `GET /v1/invoices/search`, `GET /v1/invoices/{id}`, `POST /v1/invoices/{id}`, `DELETE /v1/invoices/{id}`, `POST /v1/invoices/{id}/finalize`, `POST /v1/invoices/{id}/pay`, `POST /v1/invoices/{id}/send`, `POST /v1/invoices/{id}/void`, `POST /v1/invoices/{id}/mark_uncollectible`, `POST /v1/invoices/{id}/add_lines`, `POST /v1/invoices/{id}/update_lines`, `POST /v1/invoices/{id}/remove_lines`, `GET /v1/invoices/{id}/lines`, `GET /v1/invoices/{id}/payments`, `POST /v1/invoices/create_preview`, `GET/POST /v1/invoices/upcoming` | Partial | List/retrieve/search invoices created by checkout and create a bounded manual one-time invoice flow. Search supports `id`, `customer`, `subscription`, and `status` clauses joined by `AND`. `pay` retries open checkout invoices with deterministic sandbox `payment_method` or `source` aliases, mutating invoice, subscription, payment-intent, timeline, and webhook evidence. Manual invoices accept `customer`, `currency`, `collection_method`, `default_payment_method`, `description`, `auto_advance=false`, `pending_invoice_items_behavior`, `payment_settings[payment_method_types]`, and `metadata[...]`; `pending_invoice_items_behavior=include` attaches the customer's same-currency pending invoice items once at create. Draft invoices can be updated in place (`POST /v1/invoices/{id}`: `description`, `days_until_due`, `default_payment_method`, metadata merge) and deleted with their attached lines and timeline evidence (`DELETE`); `add_lines`/`update_lines`/`remove_lines` mutate draft lines through the invoice-item path and recompute `subtotal`/`total`/`amount_due`. `finalize` creates local PaymentIntent evidence, and `pay` applies per-intent or customer-level default outcomes including success, card decline, and `requires_action`. `send` records local email evidence and emits `invoice.sent`; `void` moves an `open` invoice to `void` and emits `invoice.voided`; `mark_uncollectible` moves an `open` invoice to `uncollectible` and emits `invoice.marked_uncollectible`; non-open statuses return `invalid_request_error`. Invoice responses include `confirmation_secret`, local hosted/PDF paths, `payments.data.payment.payment_intent`, metadata, status transitions, automatic tax state, tax totals, payment settings, array-shaped `discounts`, `lines.data` serialized like the lines endpoint, and `post_payment_credit_notes_amount` on paid invoices. Preview endpoints calculate a Stripe-shaped subscription-update proration line from current period bounds, old/new price totals, `subscription_details[items]`, `subscription_details[proration_date]`, `subscription_details[proration_behavior]`, and `subscription_details[billing_cycle_anchor]`; previews without item overrides return the subscription's next billing cycle (items, discounts, and tax with `billing_reason: upcoming`) matching the next renewal invoice. A bounded single-discount subset updates `subtotal`, `total`, and `total_discount_amounts`; invoice rendering, automatic collection, and full dunning lifecycle are outside the modeled subset. | | Invoice items | `POST /v1/invoiceitems`, `GET /v1/invoiceitems`, `GET /v1/invoiceitems/{id}` | Partial | Creates, lists, and retrieves local invoice item evidence for manual one-time invoices. `customer`, `invoice`, `amount`, `currency`, `description`, and `metadata[...]` are accepted, or `pricing[price]` plus `quantity` instead of `amount` (line amount is the price's `unit_amount` times `quantity`, currency from the price). Omitting `invoice` stores the item as a pending customer item that `POST /v1/invoices` with `pending_invoice_items_behavior=include` attaches; `subscription` is accepted and echoed. Tax, discount, and full line mutation parity are not modeled. | | Payment intents | `POST /v1/payment_intents`, `GET /v1/payment_intents`, `GET /v1/payment_intents/search`, `GET /v1/payment_intents/{id}`, `POST /v1/payment_intents/{id}/confirm`, `POST /v1/payment_intents/{id}/capture`, `POST /v1/payment_intents/{id}/cancel` | Partial | Create/list/retrieve/search and mutate local payment intents. Search supports `id`, `customer`, `status`, and metadata equality clauses joined by `AND`. `confirm` supports deterministic sandbox PaymentMethod aliases such as `pm_card_visa`, `pm_card_visa_chargeDeclined`, `pm_card_threeDSecure2Required`, `requires_action`, and local bank-transfer processing. Manual capture moves through `requires_capture` before `capture` succeeds. Direct one-time intents emit `payment_intent.created` and a terminal event when confirmed. Deferred per-intent outcomes and customer defaults are supported; `requires_action` can return either `use_stripe_sdk` or local `redirect_to_url` next-action payloads. | | Setup intents | `POST /v1/setup_intents`, `GET /v1/setup_intents`, `GET /v1/setup_intents/{id}`, `POST /v1/setup_intents/{id}/confirm`, `POST /v1/setup_intents/{id}/cancel` | Partial | Create/list/retrieve and mutate local setup intents with deterministic success, decline, and authentication-required aliases. Mandates, bank-account verification, and full SCA behavior are not modeled. | diff --git a/docs/COMPATIBILITY_TRACKING.md b/docs/COMPATIBILITY_TRACKING.md index 949ff17..4b19339 100644 --- a/docs/COMPATIBILITY_TRACKING.md +++ b/docs/COMPATIBILITY_TRACKING.md @@ -88,7 +88,7 @@ The long-running expansion goal is at least 90% OpenAPI operation coverage at The concrete target and chunk plan live in `docs/STRIPE_COMPATIBILITY_90_TARGET.md`. In short: -- current baseline: `187 / 587`, `31.9%` +- current baseline: `195 / 587`, `33.2%` - target: at least `529 / 587`, `90.0%` - maximum remaining `L0`: `58 / 587` - P0/P1 billing-lab flows still need deeper `L3-L6` evidence, not just broad diff --git a/docs/GATE_STATUS.md b/docs/GATE_STATUS.md index abda0d5..c244026 100644 --- a/docs/GATE_STATUS.md +++ b/docs/GATE_STATUS.md @@ -18,7 +18,7 @@ This is the public gate snapshot. Internal adoption evidence and raw handoff not | G11 Assertion Ergonomics | Passed locally | Structured pass/fail fixture assertions and fixture-scoped snapshots | | G12 Public Release Readiness | Passed locally | Public claims are tied to tests/scorecard cases; scorecard corpus has 50 release-blocking cases; Apache-2.0 `LICENSE` and `NOTICE` are present | | G13 Stripe API Expansion | In progress | Roadmap defines compatibility levels and endpoint-family priorities; OpenAPI inventory generator and optional workflow write JSON/Markdown coverage artifacts | -| G14 Stripe API 90% Program | In progress | `docs/STRIPE_COMPATIBILITY_90_TARGET.md` defines 90% L1+ target, current 187/587 baseline, family thresholds, and chunk plan | +| G14 Stripe API 90% Program | In progress | `docs/STRIPE_COMPATIBILITY_90_TARGET.md` defines 90% L1+ target, current 195/587 baseline, family thresholds, and chunk plan | ## Current Public Claim @@ -35,7 +35,7 @@ under Apache-2.0. - Coverage focus: request validation, protocol parameter acceptance, idempotency mismatch, deterministic checkout payment-error aliases, direct intent state machines, and billing lifecycle retry/renewal mutations -- OpenAPI operation baseline: `187 / 587`, `31.9%` +- OpenAPI operation baseline: `195 / 587`, `33.2%` - Long-running OpenAPI operation target: at least `529 / 587`, `90.0%`, at `L1+` with deeper P0/P1 behavior gates @@ -47,7 +47,7 @@ Verified on 2026-08-29 from branch `main`: - `go run ./cmd/billtap compatibility scorecard --output-dir /tmp/billtap-compatibility` - result: `imported=50 skipped=1 unsupported=1 mismatch=0 error=0` - `go run ./cmd/billtap compatibility inventory --openapi --output-dir /tmp/billtap-inventory --source stripe/openapi-v2261-2026-04-22.dahlia` - - result: `operations=587 implemented=187 inventory_only=400 schema_validated=587 implemented_percent=31.9` + - result: `operations=587 implemented=195 inventory_only=392 schema_validated=587 implemented_percent=33.2` - Current branch verification covers Go tests, compatibility scorecard, and inventory generation; `gofmt -l .` is clean. The forbidden internal keyword scan runs in CI with the configured keyword list. diff --git a/docs/SIMULATION_CAPACITY_BACKLOG.md b/docs/SIMULATION_CAPACITY_BACKLOG.md index 48d4ad1..0b9d19d 100644 --- a/docs/SIMULATION_CAPACITY_BACKLOG.md +++ b/docs/SIMULATION_CAPACITY_BACKLOG.md @@ -36,7 +36,7 @@ Billtap currently has a stateful Stripe-like local subset with: - webhook retry, duplicate, delay, out-of-order, replay, historical replay, and delivery attempt evidence - diagnostic bundles, request traces, timeline evidence, and dashboard views -- OpenAPI inventory baseline: `187 / 587` implemented operations, `31.9%` +- OpenAPI inventory baseline: `195 / 587` implemented operations, `33.2%` ## P0 Regression-Paired Backlog diff --git a/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md b/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md index 3c2184e..ab6c84e 100644 --- a/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md +++ b/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md @@ -63,7 +63,7 @@ The concrete expansion target is defined in `docs/STRIPE_COMPATIBILITY_90_TARGET.md`: - reach at least `90.0%` OpenAPI operation coverage at `L1+` -- move from the current `187 / 587` baseline to at least `529 / 587` +- move from the current `195 / 587` baseline to at least `529 / 587` - keep P0/P1 billing-lab families on deeper `L3-L6` gates where behavior matters - use broad `L1-L2` validation/fixture coverage for safe low-state and @@ -277,9 +277,9 @@ questions quickly. "generated_at": "2026-08-29T00:00:00Z", "summary": { "total_operations": 587, - "implemented_operations": 187, - "inventory_only_operations": 400, - "implemented_percent": 31.9, + "implemented_operations": 195, + "inventory_only_operations": 392, + "implemented_percent": 33.2, "families": [ { "family": "connect", diff --git a/docs/STRIPE_COMPATIBILITY_90_TARGET.md b/docs/STRIPE_COMPATIBILITY_90_TARGET.md index 34fc9f5..4d85c61 100644 --- a/docs/STRIPE_COMPATIBILITY_90_TARGET.md +++ b/docs/STRIPE_COMPATIBILITY_90_TARGET.md @@ -11,7 +11,7 @@ claiming that every operation has deep payment-processing behavior. The 90% target is based on generated `stripe-api-inventory.json`: - **Overall target:** `summary.implemented_percent >= 90.0`. -- **Current baseline:** `187 / 587` operations, `31.9%`, using Stripe OpenAPI +- **Current baseline:** `195 / 587` operations, `33.2%`, using Stripe OpenAPI `2026-04-22.dahlia` (stripe/openapi tag `v2261`), re-measured on 2026-08-29. - **Minimum target count:** `529 / 587` operations at `L1` or higher. - **Remaining inventory-only budget:** at most `58 / 587` operations at `L0`. @@ -58,10 +58,10 @@ Latest measured baseline from the Stripe OpenAPI `2026-04-22.dahlia` snapshot | Priority | Family | Total | Implemented | Coverage | 90% target count | First target | | --- | --- | ---: | ---: | ---: | ---: | --- | | P0 | webhooks | 7 | 7 | 100.0% | 7 | Expand connected-account routing, thin event fixtures, and replay evidence. | -| P0 | checkout | 6 | 4 | 66.7% | 6 | Close checkout session update/line_items routes and SDK smoke. | -| P0 | billing | 39 | 31 | 79.5% | 36 | Add renewal, trial, dunning, subscription schedule, coupon, and credit-note scenarios. | +| P0 | checkout | 6 | 6 | 100.0% | 6 | Deepen SDK smoke and hosted-page parity evidence. | +| P0 | billing | 39 | 36 | 92.3% | 36 | Close the remaining invoice adjunct routes (`attach_payment`, single-line update, `migrate`). | | P0 | billing_portal | 5 | 5 | 100.0% | 5 | Deepen portal configuration fixtures and hosted portal rendering evidence. | -| P1 | catalog | 54 | 27 | 50.0% | 49 | Add coupon, promotion code, tax-rate, and product/price search validation. | +| P1 | catalog | 54 | 28 | 51.9% | 49 | Add coupon, promotion code, tax-rate, and product/price search validation. | | P1 | customers | 31 | 12 | 38.7% | 28 | Add OpenAPI-backed validation, search/list parity, and payment source fixtures. | | P1 | payments | 41 | 22 | 53.7% | 37 | Add PaymentIntent and SetupIntent create/confirm/capture/cancel state machines. | | P1 | connect | 53 | 53 | 100.0% | 48 | Deepen Connect SDK/adoption smoke, connected-account webhook routing, and v2 Core account inventory evidence. | @@ -128,9 +128,16 @@ payment-method attach/detach. T10 also does not increase `summary.implemented_operations` by itself. It raises confidence and levels for already counted operations; new operation coverage must come from T3-T9. The planned T3-T9 delta is intentionally larger -than the `+342` operations needed to move the current `187 / 587` baseline to +than the `+334` operations needed to move the current `195 / 587` baseline to the `529 / 587` target. +The P0 checkout/invoice wave on 2026-08-29 raised the generated inventory from +`187 / 587` (`31.9%`) to `195 / 587` (`33.2%`): checkout session update and +line_items listing, draft invoice update/delete and add_lines/update_lines/ +remove_lines, and product deletion. Checkout closes to `6 / 6`, billing moves +to `36 / 39`, and catalog to `28 / 54`; three of the four P0 families are now +at 100%. + The P0 portal/items wave on 2026-08-29 raised the generated inventory from `175 / 587` (`29.8%`) to `187 / 587` (`31.9%`): billing portal configurations create/list/retrieve/update, subscription item list/retrieve/update, nested diff --git a/internal/api/api.go b/internal/api/api.go index 09855f0..1f2b867 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -558,8 +558,14 @@ func (h *Handler) handleProduct(w http.ResponseWriter, r *http.Request) { Metadata: p.metadata(), }) writeResult(w, stripeProduct(product), err) + case http.MethodDelete: + if err := h.billing.DeleteProduct(r.Context(), id); err != nil { + writeResult(w, nil, err) + return + } + writeJSON(w, http.StatusOK, stripeDeleted(id, "product")) default: - h.methodNotAllowed(w, r, "GET, POST") + h.methodNotAllowed(w, r, "GET, POST, DELETE") } } @@ -1682,12 +1688,29 @@ func (h *Handler) handleCheckoutSession(w http.ResponseWriter, r *http.Request) h.expireCheckoutSession(w, r, id) return } + if strings.HasSuffix(rest, "/line_items") { + id := strings.TrimSuffix(rest, "/line_items") + if id == "" || strings.Contains(id, "/") { + h.notFound(w, r) + return + } + if r.Method != http.MethodGet { + h.methodNotAllowed(w, r, "GET") + return + } + h.handleCheckoutSessionLineItems(w, r, id) + return + } if rest == "" || strings.Contains(rest, "/") { h.notFound(w, r) return } + if r.Method == http.MethodPost { + h.handleCheckoutSessionUpdate(w, r, rest) + return + } if r.Method != http.MethodGet { - h.methodNotAllowed(w, r, "GET") + h.methodNotAllowed(w, r, "GET, POST") return } session, err := h.billing.GetCheckoutSession(r.Context(), rest) @@ -1703,6 +1726,110 @@ func (h *Handler) handleCheckoutSession(w http.ResponseWriter, r *http.Request) writeResult(w, payload, err) } +// handleCheckoutSessionUpdate implements the bounded POST /v1/checkout/sessions/{id} +// subset: metadata merge plus line_items[N][quantity] overrides on open sessions. +func (h *Handler) handleCheckoutSessionUpdate(w http.ResponseWriter, r *http.Request, id string) { + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateCheckoutSessionUpdate(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + session, err := h.billing.GetCheckoutSession(r.Context(), id) + if err != nil { + writeResult(w, nil, err) + return + } + items := append([]billing.LineItem{}, session.LineItems...) + overridden := false + for idx := range items { + key := fmt.Sprintf("line_items[%d][quantity]", idx) + if !p.has(key) { + continue + } + quantity := p.int64(key) + if quantity <= 0 { + writeError(w, http.StatusBadRequest, invalidParam(key, "Must be at least 1.")) + return + } + items[idx].Quantity = quantity + overridden = true + } + for key := range p.values { + matches := checkoutSessionLineItemQuantityRE.FindStringSubmatch(key) + if matches == nil { + continue + } + index, parseErr := strconv.Atoi(matches[1]) + if parseErr != nil || index >= len(items) { + writeError(w, http.StatusBadRequest, invalidParam(key, "No such line item on this session.")) + return + } + } + var itemOverrides []billing.LineItem + if overridden { + itemOverrides = items + } + updated, err := h.billing.UpdateCheckoutSessionDetails(r.Context(), id, p.metadata(), itemOverrides) + if err == nil { + updated.URL = h.absoluteURL(r, updated.URL) + } + writeResult(w, h.stripeCheckoutSession(r, updated), err) +} + +// handleCheckoutSessionLineItems returns expanded line items for a session +// (GET /v1/checkout/sessions/{id}/line_items). Line amounts are pre-discount; +// per-line discount/tax splits are not modeled. +func (h *Handler) handleCheckoutSessionLineItems(w http.ResponseWriter, r *http.Request, id string) { + session, err := h.billing.GetCheckoutSession(r.Context(), id) + if err != nil { + writeResult(w, nil, err) + return + } + data := make([]map[string]any, 0, len(session.LineItems)) + for idx, item := range session.LineItems { + quantity := item.Quantity + if quantity <= 0 { + quantity = 1 + } + amount := int64(0) + currency := "usd" + var priceObject map[string]any + if price, priceErr := h.billing.GetPrice(r.Context(), item.PriceID); priceErr == nil { + amount = price.UnitAmount * quantity + currency = price.Currency + priceObject = stripePrice(price) + } else { + priceObject = map[string]any{ + "id": item.PriceID, + "object": billing.ObjectPrice, + "currency": currency, + "unit_amount": 0, + "livemode": false, + } + } + data = append(data, map[string]any{ + "id": fmt.Sprintf("li_%s_%d", session.ID, idx), + "object": "item", + "currency": strings.ToLower(currency), + "quantity": quantity, + "amount_subtotal": amount, + "amount_total": amount, + "amount_discount": 0, + "amount_tax": 0, + "description": nil, + "price": priceObject, + "discounts": []map[string]any{}, + "taxes": []map[string]any{}, + "metadata": map[string]string{}, + }) + } + writeJSON(w, http.StatusOK, stripeListFromRequest(r, data)) +} + func (h *Handler) handleBillingPortalSessions(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { h.methodNotAllowed(w, r, "POST") @@ -3700,12 +3827,111 @@ func (h *Handler) handleInvoice(w http.ResponseWriter, r *http.Request) { writeResult(w, stripeListFromRequest(r, payments), nil) return } + if len(parts) == 2 && parts[1] == "add_lines" { + if r.Method != http.MethodPost { + h.methodNotAllowed(w, r, "POST") + return + } + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateInvoiceAddLines(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + lines, err := invoiceLineAdditionsFromParams(p) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + invoice, err := h.billing.AddInvoiceLines(r.Context(), id, lines) + writeResult(w, h.stripeInvoice(r.Context(), invoice), err) + return + } + if len(parts) == 2 && parts[1] == "update_lines" { + if r.Method != http.MethodPost { + h.methodNotAllowed(w, r, "POST") + return + } + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateInvoiceLineUpdate(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + patches := invoiceLinePatchesFromParams(p) + if len(patches) == 0 { + writeError(w, http.StatusBadRequest, missingParam("line_items[0][id]")) + return + } + var invoice billing.Invoice + for _, line := range patches { + var err error + invoice, err = h.billing.UpdateInvoiceLine(r.Context(), id, line.id, line.amount, line.description) + if err != nil { + writeResult(w, nil, err) + return + } + } + writeResult(w, h.stripeInvoice(r.Context(), invoice), nil) + return + } + if len(parts) == 2 && parts[1] == "remove_lines" { + if r.Method != http.MethodPost { + h.methodNotAllowed(w, r, "POST") + return + } + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateInvoiceLineRemove(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + lineIDs := invoiceLineIDsFromParams(p) + if len(lineIDs) == 0 { + writeError(w, http.StatusBadRequest, missingParam("line_items[0][id]")) + return + } + invoice, err := h.billing.RemoveInvoiceLines(r.Context(), id, lineIDs) + writeResult(w, h.stripeInvoice(r.Context(), invoice), err) + return + } if len(parts) != 1 { h.notFound(w, r) return } + if r.Method == http.MethodPost { + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateInvoiceUpdate(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + invoice, err := h.billing.UpdateInvoiceDetails(r.Context(), id, invoiceMetadataFromParams(p)) + writeResult(w, h.stripeInvoice(r.Context(), invoice), err) + return + } + if r.Method == http.MethodDelete { + if err := h.billing.DeleteInvoice(r.Context(), id); err != nil { + writeResult(w, nil, err) + return + } + writeJSON(w, http.StatusOK, stripeDeleted(id, billing.ObjectInvoice)) + return + } if r.Method != http.MethodGet { - h.methodNotAllowed(w, r, "GET") + h.methodNotAllowed(w, r, "GET, POST, DELETE") return } invoice, err := h.billing.GetInvoice(r.Context(), id) @@ -6229,6 +6455,96 @@ func copyPaymentSettingsMetadata(metadata map[string]string, p params) map[strin return metadata } +// invoiceLineIndexes collects the line_items[N] indexes present in the params. +func invoiceLineIndexes(p params) map[int]bool { + indexes := map[int]bool{} + for key := range p.values { + matches := invoiceLineItemParamRE.FindStringSubmatch(key) + if matches == nil { + continue + } + if index, err := strconv.Atoi(matches[1]); err == nil { + indexes[index] = true + } + } + return indexes +} + +func sortedInvoiceLineIndexes(p params) []int { + indexes := invoiceLineIndexes(p) + sorted := make([]int, 0, len(indexes)) + for index := range indexes { + sorted = append(sorted, index) + } + sort.Ints(sorted) + return sorted +} + +// invoiceLineAdditionsFromParams builds new draft-invoice lines from +// line_items[N][amount]/[description]/[currency] form params (add_lines). +func invoiceLineAdditionsFromParams(p params) ([]billing.InvoiceItem, error) { + sorted := sortedInvoiceLineIndexes(p) + if len(sorted) == 0 { + return nil, missingParam("line_items[0][amount]") + } + lines := make([]billing.InvoiceItem, 0, len(sorted)) + for _, index := range sorted { + amountKey := fmt.Sprintf("line_items[%d][amount]", index) + if !p.has(amountKey) { + return nil, missingParam(amountKey) + } + lines = append(lines, billing.InvoiceItem{ + Amount: p.int64(amountKey), + Currency: p.string(fmt.Sprintf("line_items[%d][currency]", index)), + Description: p.string(fmt.Sprintf("line_items[%d][description]", index)), + }) + } + return lines, nil +} + +type invoiceLinePatch struct { + id string + amount *int64 + description *string +} + +// invoiceLinePatchesFromParams builds per-line patches keyed by +// line_items[N][id] with optional amount/description (update_lines). +func invoiceLinePatchesFromParams(p params) []invoiceLinePatch { + patches := make([]invoiceLinePatch, 0) + for _, index := range sortedInvoiceLineIndexes(p) { + idKey := fmt.Sprintf("line_items[%d][id]", index) + if !p.has(idKey) { + continue + } + patch := invoiceLinePatch{id: p.string(idKey)} + amountKey := fmt.Sprintf("line_items[%d][amount]", index) + if p.has(amountKey) { + amount := p.int64(amountKey) + patch.amount = &amount + } + descriptionKey := fmt.Sprintf("line_items[%d][description]", index) + if p.has(descriptionKey) { + description := p.string(descriptionKey) + patch.description = &description + } + patches = append(patches, patch) + } + return patches +} + +// invoiceLineIDsFromParams collects line_items[N][id] values (remove_lines). +func invoiceLineIDsFromParams(p params) []string { + ids := make([]string, 0) + for _, index := range sortedInvoiceLineIndexes(p) { + idKey := fmt.Sprintf("line_items[%d][id]", index) + if p.has(idKey) { + ids = append(ids, p.string(idKey)) + } + } + return ids +} + func paymentIntentMetadata(p params) map[string]string { metadata := p.metadata() outcome := firstNonEmptyString( diff --git a/internal/api/checkout_invoice_lines_test.go b/internal/api/checkout_invoice_lines_test.go new file mode 100644 index 0000000..1e15d69 --- /dev/null +++ b/internal/api/checkout_invoice_lines_test.go @@ -0,0 +1,331 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/hckim/billtap/internal/billing" +) + +func setupCheckoutLineFixtures(t *testing.T, handler http.Handler) (customer billing.Customer, price billing.Price) { + t.Helper() + customer = postForm[billing.Customer](t, handler, "/v1/customers", url.Values{"email": {"cs-lines@example.test"}}) + product := postForm[billing.Product](t, handler, "/v1/products", url.Values{"name": {"Line Item Plan"}}) + price = postForm[billing.Price](t, handler, "/v1/prices", url.Values{ + "product": {product.ID}, + "currency": {"usd"}, + "unit_amount": {"2500"}, + "recurring[interval]": {"month"}, + }) + return customer, price +} + +func TestCheckoutSessionLineItems(t *testing.T) { + handler := newTestHandler(t) + customer, price := setupCheckoutLineFixtures(t, handler) + + session := postForm[billing.CheckoutSession](t, handler, "/v1/checkout/sessions", url.Values{ + "customer": {customer.ID}, + "line_items[0][price]": {price.ID}, + "line_items[0][quantity]": {"1"}, + "line_items[1][price]": {price.ID}, + "line_items[1][quantity]": {"2"}, + "success_url": {"http://app.test/success"}, + "cancel_url": {"http://app.test/cancel"}, + }) + + listed := getJSON[struct { + Data []struct { + ID string `json:"id"` + Object string `json:"object"` + Quantity int64 `json:"quantity"` + AmountTotal int64 `json:"amount_total"` + AmountSubtotal int64 `json:"amount_subtotal"` + Currency string `json:"currency"` + Price struct { + ID string `json:"id"` + } `json:"price"` + } `json:"data"` + }](t, handler, "/v1/checkout/sessions/"+session.ID+"/line_items") + if len(listed.Data) != 2 { + t.Fatalf("session line_items = %d, want 2", len(listed.Data)) + } + for idx, item := range listed.Data { + if item.Object != "item" || item.Price.ID != price.ID { + t.Fatalf("line item %d = %#v, want expanded price %s", idx, item, price.ID) + } + } + if listed.Data[0].Quantity != 1 || listed.Data[0].AmountTotal != 2500 || listed.Data[0].AmountSubtotal != 2500 { + t.Fatalf("first line item = %#v, want quantity 1 amount 2500", listed.Data[0]) + } + if listed.Data[1].Quantity != 2 || listed.Data[1].AmountTotal != 5000 { + t.Fatalf("second line item = %#v, want quantity 2 amount 5000", listed.Data[1]) + } + + missingReq := httptest.NewRequest(http.MethodGet, "/v1/checkout/sessions/cs_missing/line_items", nil) + missingRec := httptest.NewRecorder() + handler.ServeHTTP(missingRec, missingReq) + if missingRec.Code != http.StatusNotFound { + t.Fatalf("missing session line_items status = %d body = %s, want 404", missingRec.Code, missingRec.Body.String()) + } +} + +func TestCheckoutSessionUpdateMetadataAndQuantities(t *testing.T) { + handler := newTestHandler(t) + customer, price := setupCheckoutLineFixtures(t, handler) + + session := postForm[billing.CheckoutSession](t, handler, "/v1/checkout/sessions", url.Values{ + "customer": {customer.ID}, + "line_items[0][price]": {price.ID}, + "line_items[0][quantity]": {"1"}, + "line_items[1][price]": {price.ID}, + "line_items[1][quantity]": {"1"}, + "success_url": {"http://app.test/success"}, + "cancel_url": {"http://app.test/cancel"}, + }) + + // Unknown update params are rejected. + status, body := postFormStatus(t, handler, "/v1/checkout/sessions/"+session.ID, url.Values{ + "shipping_options[0][shipping_rate]": {"shr_1"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("unknown update param status = %d body = %s, want 400", status, body) + } + // Out-of-range line index is rejected. + status, body = postFormStatus(t, handler, "/v1/checkout/sessions/"+session.ID, url.Values{ + "line_items[5][quantity]": {"2"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("out-of-range line_items status = %d body = %s, want 400", status, body) + } + // Quantities must stay positive. + status, body = postFormStatus(t, handler, "/v1/checkout/sessions/"+session.ID, url.Values{ + "line_items[0][quantity]": {"0"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("zero quantity status = %d body = %s, want 400", status, body) + } + + updated := postForm[struct { + Metadata map[string]string `json:"metadata"` + AmountTotal int64 `json:"amount_total"` + }](t, handler, "/v1/checkout/sessions/"+session.ID, url.Values{ + "metadata[env]": {"ci"}, + "line_items[1][quantity]": {"3"}, + }) + if updated.Metadata["env"] != "ci" { + t.Fatalf("updated metadata = %#v, want env=ci", updated.Metadata) + } + if updated.AmountTotal != 2500*4 { + t.Fatalf("updated amount_total = %d, want %d", updated.AmountTotal, 2500*4) + } + + // Non-open sessions reject updates. + completion := postJSON[map[string]json.RawMessage](t, handler, "/api/checkout/sessions/"+session.ID+"/complete", map[string]string{"outcome": "payment_succeeded"}) + if completion["session"] == nil { + t.Fatalf("completion returned no session") + } + status, body = postFormStatus(t, handler, "/v1/checkout/sessions/"+session.ID, url.Values{ + "metadata[env]": {"done"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("completed session update status = %d body = %s, want 400", status, body) + } +} + +func TestInvoiceUpdateAndDelete(t *testing.T) { + handler := newTestHandler(t) + customer := postForm[billing.Customer](t, handler, "/v1/customers", url.Values{"email": {"inv-update@example.test"}}) + invoice := postForm[struct { + ID string `json:"id"` + Created int64 `json:"created"` + }](t, handler, "/v1/invoices", url.Values{ + "customer": {customer.ID}, + }) + + // Unknown params are rejected. + status, body := postFormStatus(t, handler, "/v1/invoices/"+invoice.ID, url.Values{ + "auto_advance": {"false"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("unknown invoice update param status = %d body = %s, want 400", status, body) + } + + updated := postForm[struct { + Description string `json:"description"` + DueDate int64 `json:"due_date"` + Metadata map[string]string `json:"metadata"` + }](t, handler, "/v1/invoices/"+invoice.ID, url.Values{ + "description": {"Consulting"}, + "days_until_due": {"14"}, + "metadata[env]": {"ci"}, + }) + if updated.Description != "Consulting" { + t.Fatalf("updated description = %q, want Consulting", updated.Description) + } + if updated.Metadata["env"] != "ci" { + t.Fatalf("updated metadata = %#v, want env=ci", updated.Metadata) + } + if want := invoice.Created + 14*86400; updated.DueDate != want { + t.Fatalf("updated due_date = %d, want %d (created %d + 14d)", updated.DueDate, want, invoice.Created) + } + + // Attached lines are deleted with the draft invoice. + item := postForm[struct { + ID string `json:"id"` + }](t, handler, "/v1/invoiceitems", url.Values{ + "customer": {customer.ID}, + "invoice": {invoice.ID}, + "amount": {"5000"}, + "currency": {"usd"}, + "description": {"Initial"}, + }) + deleted := deleteForm[struct { + ID string `json:"id"` + Deleted bool `json:"deleted"` + }](t, handler, "/v1/invoices/"+invoice.ID, url.Values{}) + if !deleted.Deleted || deleted.ID != invoice.ID { + t.Fatalf("invoice delete = %#v, want deleted marker", deleted) + } + afterReq := httptest.NewRequest(http.MethodGet, "/v1/invoices/"+invoice.ID, nil) + afterRec := httptest.NewRecorder() + handler.ServeHTTP(afterRec, afterReq) + if afterRec.Code != http.StatusNotFound { + t.Fatalf("deleted invoice GET status = %d body = %s, want 404", afterRec.Code, afterRec.Body.String()) + } + itemReq := httptest.NewRequest(http.MethodGet, "/v1/invoiceitems/"+item.ID, nil) + itemRec := httptest.NewRecorder() + handler.ServeHTTP(itemRec, itemReq) + if itemRec.Code != http.StatusNotFound { + t.Fatalf("attached item after invoice delete status = %d, want 404", itemRec.Code) + } + + // Non-draft invoices cannot be deleted. + other := postForm[struct { + ID string `json:"id"` + }](t, handler, "/v1/invoices", url.Values{ + "customer": {customer.ID}, + }) + _ = postJSON[map[string]json.RawMessage](t, handler, "/v1/invoices/"+other.ID+"/finalize", map[string]string{}) + status, body = deleteFormStatus(t, handler, "/v1/invoices/"+other.ID, url.Values{}) + if status != http.StatusBadRequest { + t.Fatalf("finalized invoice delete status = %d body = %s, want 400", status, body) + } +} + +func TestInvoiceLineMutation(t *testing.T) { + handler := newTestHandler(t) + customer := postForm[billing.Customer](t, handler, "/v1/customers", url.Values{"email": {"inv-lines@example.test"}}) + invoice := postForm[struct { + ID string `json:"id"` + }](t, handler, "/v1/invoices", url.Values{ + "customer": {customer.ID}, + }) + first := postForm[struct { + ID string `json:"id"` + }](t, handler, "/v1/invoiceitems", url.Values{ + "customer": {customer.ID}, + "invoice": {invoice.ID}, + "amount": {"5000"}, + "currency": {"usd"}, + "description": {"First"}, + }) + + type invoiceState struct { + Subtotal int64 `json:"subtotal"` + Total int64 `json:"total"` + AmountDue int64 `json:"amount_due"` + } + + // add_lines appends attached lines and recomputes totals. + added := postForm[invoiceState](t, handler, "/v1/invoices/"+invoice.ID+"/add_lines", url.Values{ + "line_items[0][amount]": {"2000"}, + "line_items[0][description]": {"Second"}, + "line_items[0][currency]": {"usd"}, + }) + if added.Subtotal != 7000 || added.Total != 7000 || added.AmountDue != 7000 { + t.Fatalf("after add_lines = %#v, want 7000", added) + } + // add_lines requires amounts. + status, body := postFormStatus(t, handler, "/v1/invoices/"+invoice.ID+"/add_lines", url.Values{ + "line_items[0][description]": {"No amount"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("add_lines without amount status = %d body = %s, want 400", status, body) + } + + // update_lines patches a line by id and recomputes totals. + updated := postForm[invoiceState](t, handler, "/v1/invoices/"+invoice.ID+"/update_lines", url.Values{ + "line_items[0][id]": {first.ID}, + "line_items[0][amount]": {"3000"}, + "line_items[0][description]": {"First (revised)"}, + }) + if updated.Subtotal != 5000 || updated.Total != 5000 || updated.AmountDue != 5000 { + t.Fatalf("after update_lines = %#v, want 5000", updated) + } + lines := getJSON[struct { + Data []struct { + ID string `json:"id"` + Amount int64 `json:"amount"` + Description string `json:"description"` + } `json:"data"` + }](t, handler, "/v1/invoices/"+invoice.ID+"/lines") + if len(lines.Data) != 2 { + t.Fatalf("invoice lines = %d, want 2", len(lines.Data)) + } + for _, line := range lines.Data { + if line.ID == first.ID && (line.Amount != 3000 || line.Description != "First (revised)") { + t.Fatalf("updated line = %#v, want amount 3000 revised description", line) + } + } + // update_lines without id is rejected. + status, body = postFormStatus(t, handler, "/v1/invoices/"+invoice.ID+"/update_lines", url.Values{ + "line_items[0][amount]": {"1000"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("update_lines without id status = %d body = %s, want 400", status, body) + } + + // remove_lines detaches a line and recomputes totals. + removed := postForm[invoiceState](t, handler, "/v1/invoices/"+invoice.ID+"/remove_lines", url.Values{ + "line_items[0][id]": {first.ID}, + }) + if removed.Subtotal != 2000 || removed.Total != 2000 || removed.AmountDue != 2000 { + t.Fatalf("after remove_lines = %#v, want 2000", removed) + } + // Line mutations require draft invoices. + _ = postJSON[map[string]json.RawMessage](t, handler, "/v1/invoices/"+invoice.ID+"/finalize", map[string]string{}) + status, body = postFormStatus(t, handler, "/v1/invoices/"+invoice.ID+"/add_lines", url.Values{ + "line_items[0][amount]": {"1000"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("add_lines on finalized invoice status = %d body = %s, want 400", status, body) + } +} + +func TestProductDelete(t *testing.T) { + handler := newTestHandler(t) + product := postForm[billing.Product](t, handler, "/v1/products", url.Values{"name": {"Doomed Plan"}}) + + deleted := deleteForm[struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + }](t, handler, "/v1/products/"+product.ID, url.Values{}) + if !deleted.Deleted || deleted.Object != "product" || deleted.ID != product.ID { + t.Fatalf("product delete = %#v, want deleted product marker", deleted) + } + afterReq := httptest.NewRequest(http.MethodGet, "/v1/products/"+product.ID, nil) + afterRec := httptest.NewRecorder() + handler.ServeHTTP(afterRec, afterReq) + if afterRec.Code != http.StatusNotFound { + t.Fatalf("deleted product GET status = %d body = %s, want 404", afterRec.Code, afterRec.Body.String()) + } + status, body := deleteFormStatus(t, handler, "/v1/products/"+product.ID, url.Values{}) + if status != http.StatusNotFound { + t.Fatalf("re-delete product status = %d body = %s, want 404", status, body) + } +} diff --git a/internal/api/validation.go b/internal/api/validation.go index a9a213f..3e4ee68 100644 --- a/internal/api/validation.go +++ b/internal/api/validation.go @@ -52,6 +52,10 @@ var ( invoicePreviewItemParamRE = regexp.MustCompile(`^((subscription_details|subscriptionDetails)\[items\]\[\d+\]\[(id|price|price_id|quantity)\]|(subscription_items|items)\[\d+\]\[(id|price|price_id|quantity)\])$`) invoicePaymentSettingsRE = regexp.MustCompile(`^payment_settings(\[[^\]]+\])+$`) portalFeatureParamRE = regexp.MustCompile(`^features\[(customer_update|invoice_history|payment_method_update|subscription_cancel|subscription_update)\]\[(enabled|mode|proration_behavior|cancellation_reason|allowed_updates|default_allowed_updates)\](\[\d*\])?$`) + // Session update line items are quantity-only overrides of existing items. + checkoutSessionLineItemQuantityRE = regexp.MustCompile(`^line_items\[(\d+)\]\[quantity\]$`) + // Draft-invoice line mutation params (add_lines/update_lines/remove_lines). + invoiceLineItemParamRE = regexp.MustCompile(`^line_items\[(\d+)\]\[(id|amount|description|currency)\]$`) ) var stripePaymentMethodTypes = []string{ @@ -1596,6 +1600,57 @@ func validateSubscriptionItemList(p params) error { }) } +// validateCheckoutSessionUpdate allows the bounded POST /v1/checkout/sessions/{id} +// subset: metadata merge and line_items[N][quantity] overrides. +func validateCheckoutSessionUpdate(p params) error { + return p.validate(paramSpec{ + Allowed: []string{"expand"}, + AllowedRegex: []*regexp.Regexp{checkoutSessionLineItemQuantityRE}, + AllowMetadata: true, + }) +} + +func validateInvoiceUpdate(p params) error { + return p.validate(paramSpec{ + Allowed: []string{"description", "days_until_due", "default_payment_method"}, + AllowMetadata: true, + }) +} + +func validateInvoiceAddLines(p params) error { + return p.validate(paramSpec{ + AllowedRegex: []*regexp.Regexp{invoiceLineItemParamRE}, + }) +} + +func validateInvoiceLineUpdate(p params) error { + if err := p.validate(paramSpec{ + AllowedRegex: []*regexp.Regexp{invoiceLineItemParamRE}, + }); err != nil { + return err + } + for index := range invoiceLineIndexes(p) { + if !p.has(fmt.Sprintf("line_items[%d][id]", index)) { + return missingParam(fmt.Sprintf("line_items[%d][id]", index)) + } + } + return nil +} + +func validateInvoiceLineRemove(p params) error { + if err := p.validate(paramSpec{ + AllowedRegex: []*regexp.Regexp{invoiceLineItemParamRE}, + }); err != nil { + return err + } + for index := range invoiceLineIndexes(p) { + if !p.has(fmt.Sprintf("line_items[%d][id]", index)) { + return missingParam(fmt.Sprintf("line_items[%d][id]", index)) + } + } + return nil +} + func validateSubscriptionItemUpdate(p params) error { return p.validate(paramSpec{ Allowed: []string{ diff --git a/internal/billing/service.go b/internal/billing/service.go index 6691063..67d8320 100644 --- a/internal/billing/service.go +++ b/internal/billing/service.go @@ -72,6 +72,7 @@ type Repository interface { GetProduct(context.Context, string) (Product, error) ListProducts(context.Context) ([]Product, error) UpdateProduct(context.Context, string, Product) (Product, error) + DeleteProduct(context.Context, string) error CreatePrice(context.Context, Price) (Price, error) GetPrice(context.Context, string) (Price, error) @@ -94,7 +95,10 @@ type Repository interface { ListInvoices(context.Context) ([]Invoice, error) ListInvoicesFiltered(context.Context, InvoiceFilter) ([]Invoice, error) UpdateInvoice(context.Context, Invoice, []TimelineEntry) (Invoice, error) + DeleteInvoice(context.Context, string) error CreateInvoiceItem(context.Context, InvoiceItem, Invoice, []TimelineEntry) (InvoiceItem, Invoice, error) + UpdateInvoiceItem(context.Context, InvoiceItem, Invoice, []TimelineEntry) (InvoiceItem, Invoice, error) + DeleteInvoiceItem(context.Context, string, Invoice, []TimelineEntry) (Invoice, error) AttachInvoiceItems(context.Context, Invoice, []string, []TimelineEntry) (Invoice, error) ListInvoiceItemsFiltered(context.Context, InvoiceItemFilter) ([]InvoiceItem, error) FinalizeInvoice(context.Context, Invoice, PaymentIntent, []TimelineEntry) (Invoice, PaymentIntent, error) @@ -202,6 +206,15 @@ func (s *Service) UpdateProduct(ctx context.Context, id string, in Product) (Pro return s.repo.UpdateProduct(ctx, id, in) } +// DeleteProduct removes local product evidence (DELETE /v1/products/{id}). +// Existing prices keep referencing the deleted product id. +func (s *Service) DeleteProduct(ctx context.Context, id string) error { + if strings.TrimSpace(id) == "" { + return fmt.Errorf("%w: product is required", ErrInvalidInput) + } + return s.repo.DeleteProduct(ctx, id) +} + func (s *Service) CreatePrice(ctx context.Context, in Price) (Price, error) { if strings.TrimSpace(in.ProductID) == "" { return Price{}, fmt.Errorf("%w: product is required", ErrInvalidInput) @@ -450,6 +463,52 @@ func (s *Service) ExpireCheckoutSession(ctx context.Context, sessionID string) ( )}) } +// UpdateCheckoutSessionDetails merges metadata and replaces line-item +// quantities on an open session (POST /v1/checkout/sessions/{id}). +func (s *Service) UpdateCheckoutSessionDetails(ctx context.Context, sessionID string, metadata map[string]string, items []LineItem) (CheckoutSession, error) { + if strings.TrimSpace(sessionID) == "" { + return CheckoutSession{}, fmt.Errorf("%w: session is required", ErrInvalidInput) + } + session, err := s.repo.GetCheckoutSession(ctx, sessionID) + if err != nil { + return CheckoutSession{}, err + } + if strings.ToLower(strings.TrimSpace(session.Status)) != "open" { + return CheckoutSession{}, fmt.Errorf("%w: status must be open", ErrInvalidInput) + } + at := s.now() + if metadata != nil { + session.Metadata = copyMap(session.Metadata) + if session.Metadata == nil { + session.Metadata = map[string]string{} + } + for key, value := range metadata { + if value == "" { + delete(session.Metadata, key) + } else { + session.Metadata[key] = value + } + } + } + if items != nil { + session.LineItems = items + } + return s.repo.UpdateCheckoutSession(ctx, session, []TimelineEntry{billingTimelineEntry( + "checkout_session_updated_"+session.ID+"_"+at.Format(time.RFC3339Nano), + "checkout.session.updated", + "Checkout session updated", + ObjectCheckoutSession, + session.ID, + session.CustomerID, + session.ID, + session.SubscriptionID, + session.InvoiceID, + session.PaymentIntentID, + map[string]string{"source": "checkout.session.update"}, + at, + )}) +} + func (s *Service) CompleteCheckout(ctx context.Context, sessionID string, outcome string) (CheckoutSession, error) { return s.completeCheckout(ctx, sessionID, outcome, CheckoutCompletionOptions{}) } @@ -886,6 +945,183 @@ func (s *Service) CreateInvoice(ctx context.Context, in Invoice) (Invoice, error return s.attachPendingInvoiceItems(ctx, created) } +// requireDraftInvoice loads an invoice and rejects anything that already left +// the draft state; Stripe line/detail edits apply to drafts only. +func (s *Service) requireDraftInvoice(ctx context.Context, invoiceID string) (Invoice, error) { + invoice, err := s.repo.GetInvoice(ctx, invoiceID) + if err != nil { + return Invoice{}, err + } + if invoice.Status != "draft" { + return Invoice{}, fmt.Errorf("%w: invoice status must be draft", ErrInvalidInput) + } + return invoice, nil +} + +// UpdateInvoiceDetails applies the bounded draft-invoice update subset +// (POST /v1/invoices/{id}): metadata merge plus the evidenced description, +// days_until_due, and default_payment_method keys. +func (s *Service) UpdateInvoiceDetails(ctx context.Context, invoiceID string, metadata map[string]string) (Invoice, error) { + invoice, err := s.requireDraftInvoice(ctx, invoiceID) + if err != nil { + return Invoice{}, err + } + at := s.now() + if metadata != nil { + invoice.Metadata = copyMap(invoice.Metadata) + if invoice.Metadata == nil { + invoice.Metadata = map[string]string{} + } + for key, value := range metadata { + if value == "" { + delete(invoice.Metadata, key) + } else { + invoice.Metadata[key] = value + } + } + } + return s.repo.UpdateInvoice(ctx, invoice, []TimelineEntry{billingTimelineEntry( + "invoice_updated_"+invoice.ID+"_"+at.Format(time.RFC3339Nano), + "invoice.updated", + "Invoice updated", + ObjectInvoice, + invoice.ID, + invoice.CustomerID, + "", + invoice.SubscriptionID, + invoice.ID, + invoice.PaymentIntentID, + map[string]string{"source": "invoice.update"}, + at, + )}) +} + +// DeleteInvoice removes a draft invoice together with its attached lines and +// timeline evidence (DELETE /v1/invoices/{id}). +func (s *Service) DeleteInvoice(ctx context.Context, invoiceID string) error { + if _, err := s.requireDraftInvoice(ctx, invoiceID); err != nil { + return err + } + return s.repo.DeleteInvoice(ctx, invoiceID) +} + +// AddInvoiceLines attaches new lines to a draft invoice +// (POST /v1/invoices/{id}/add_lines), reusing the invoice-item path so +// subtotal/total/amount_due and timeline evidence stay consistent. +func (s *Service) AddInvoiceLines(ctx context.Context, invoiceID string, lines []InvoiceItem) (Invoice, error) { + invoice, err := s.requireDraftInvoice(ctx, invoiceID) + if err != nil { + return Invoice{}, err + } + if len(lines) == 0 { + return Invoice{}, fmt.Errorf("%w: line_items are required", ErrInvalidInput) + } + for _, line := range lines { + if line.Amount == 0 { + return Invoice{}, fmt.Errorf("%w: line_items amount is required", ErrInvalidInput) + } + line.CustomerID = invoice.CustomerID + line.InvoiceID = invoice.ID + line.Currency = firstNonEmpty(strings.TrimSpace(line.Currency), invoice.Currency) + _, updated, err := s.CreateInvoiceItem(ctx, line) + if err != nil { + return Invoice{}, err + } + invoice = updated + } + return invoice, nil +} + +// findInvoiceLine locates one attached line on a draft invoice. +func (s *Service) findInvoiceLine(ctx context.Context, invoice Invoice, lineID string) (InvoiceItem, error) { + items, err := s.repo.ListInvoiceItemsFiltered(ctx, InvoiceItemFilter{InvoiceID: invoice.ID}) + if err != nil { + return InvoiceItem{}, err + } + for _, item := range items { + if item.ID == lineID { + return item, nil + } + } + return InvoiceItem{}, ErrNotFound +} + +// UpdateInvoiceLine patches one attached draft-invoice line +// (POST /v1/invoices/{id}/update_lines): amount and description updates +// recompute subtotal/total/amount_due. +func (s *Service) UpdateInvoiceLine(ctx context.Context, invoiceID string, lineID string, amount *int64, description *string) (Invoice, error) { + invoice, err := s.requireDraftInvoice(ctx, invoiceID) + if err != nil { + return Invoice{}, err + } + item, err := s.findInvoiceLine(ctx, invoice, lineID) + if err != nil { + return Invoice{}, err + } + at := s.now() + if amount != nil && *amount != item.Amount { + addInvoiceItemAmount(&invoice, *amount-item.Amount, item.Currency) + item.Amount = *amount + } + if description != nil { + item.Description = *description + } + _, updated, err := s.repo.UpdateInvoiceItem(ctx, item, invoice, []TimelineEntry{billingTimelineEntry( + "invoiceline_updated_"+item.ID+"_"+at.Format(time.RFC3339Nano), + "invoice.updated", + "Invoice line updated", + ObjectInvoice, + invoice.ID, + invoice.CustomerID, + "", + invoice.SubscriptionID, + invoice.ID, + invoice.PaymentIntentID, + map[string]string{"source": "invoice.update_lines", "line": item.ID}, + at, + )}) + return updated, err +} + +// RemoveInvoiceLines detaches lines from a draft invoice +// (POST /v1/invoices/{id}/remove_lines), recomputing totals per removed line. +func (s *Service) RemoveInvoiceLines(ctx context.Context, invoiceID string, lineIDs []string) (Invoice, error) { + invoice, err := s.requireDraftInvoice(ctx, invoiceID) + if err != nil { + return Invoice{}, err + } + if len(lineIDs) == 0 { + return Invoice{}, fmt.Errorf("%w: line_items are required", ErrInvalidInput) + } + for _, lineID := range lineIDs { + item, err := s.findInvoiceLine(ctx, invoice, lineID) + if err != nil { + return Invoice{}, err + } + at := s.now() + addInvoiceItemAmount(&invoice, -item.Amount, item.Currency) + updated, err := s.repo.DeleteInvoiceItem(ctx, item.ID, invoice, []TimelineEntry{billingTimelineEntry( + "invoiceline_removed_"+item.ID+"_"+at.Format(time.RFC3339Nano), + "invoice.updated", + "Invoice line removed", + ObjectInvoice, + invoice.ID, + invoice.CustomerID, + "", + invoice.SubscriptionID, + invoice.ID, + invoice.PaymentIntentID, + map[string]string{"source": "invoice.remove_lines", "line": item.ID}, + at, + )}) + if err != nil { + return Invoice{}, err + } + invoice = updated + } + return invoice, nil +} + func (s *Service) ListInvoices(ctx context.Context) ([]Invoice, error) { return s.repo.ListInvoices(ctx) } diff --git a/internal/storage/billing.go b/internal/storage/billing.go index 128db0c..f1650f3 100644 --- a/internal/storage/billing.go +++ b/internal/storage/billing.go @@ -129,6 +129,23 @@ func (s *SQLiteStore) UpdateProduct(ctx context.Context, id string, in billing.P return s.GetProduct(ctx, id) } +// DeleteProduct removes local product evidence. Prices keep referencing the +// deleted product id, matching Stripe's product-deletion behavior. +func (s *SQLiteStore) DeleteProduct(ctx context.Context, id string) error { + result, err := s.db.ExecContext(ctx, `DELETE FROM products WHERE id = ?`, id) + if err != nil { + return err + } + changed, err := result.RowsAffected() + if err != nil { + return err + } + if changed == 0 { + return billing.ErrNotFound + } + return nil +} + func (s *SQLiteStore) CreatePrice(ctx context.Context, p billing.Price) (billing.Price, error) { if p.CreatedAt.IsZero() { p.CreatedAt = time.Now().UTC() @@ -479,7 +496,7 @@ func (s *SQLiteStore) UpdateCheckoutSession(ctx context.Context, cs billing.Chec return billing.CheckoutSession{}, err } defer tx.Rollback() - result, err := tx.ExecContext(ctx, `UPDATE checkout_sessions SET status = ?, metadata = ? WHERE id = ?`, cs.Status, encodeMap(cs.Metadata), cs.ID) + result, err := tx.ExecContext(ctx, `UPDATE checkout_sessions SET status = ?, metadata = ?, line_items = ? WHERE id = ?`, cs.Status, encodeMap(cs.Metadata), encodeLineItems(cs.LineItems), cs.ID) if err != nil { return billing.CheckoutSession{}, err } @@ -869,6 +886,117 @@ func (s *SQLiteStore) CreateInvoiceItem(ctx context.Context, item billing.Invoic return createdItem, updatedInvoice, nil } +// UpdateInvoiceItem saves an edited draft-invoice line together with the +// recomputed invoice totals in one transaction. +func (s *SQLiteStore) UpdateInvoiceItem(ctx context.Context, item billing.InvoiceItem, invoice billing.Invoice, timeline []billing.TimelineEntry) (billing.InvoiceItem, billing.Invoice, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return billing.InvoiceItem{}, billing.Invoice{}, err + } + defer tx.Rollback() + result, err := tx.ExecContext(ctx, `UPDATE invoice_items SET amount = ?, description = ?, metadata = ? WHERE id = ?`, item.Amount, item.Description, encodeMap(item.Metadata), item.ID) + if err != nil { + return billing.InvoiceItem{}, billing.Invoice{}, err + } + changed, err := result.RowsAffected() + if err != nil { + return billing.InvoiceItem{}, billing.Invoice{}, err + } + if changed == 0 { + return billing.InvoiceItem{}, billing.Invoice{}, billing.ErrNotFound + } + if invoice.ID != "" { + if err := updateInvoiceTx(ctx, tx, invoice); err != nil { + return billing.InvoiceItem{}, billing.Invoice{}, err + } + } + for _, entry := range timeline { + if err := s.insertTimeline(ctx, tx, entry); err != nil { + return billing.InvoiceItem{}, billing.Invoice{}, err + } + } + if err := tx.Commit(); err != nil { + return billing.InvoiceItem{}, billing.Invoice{}, err + } + updatedItem, err := s.GetInvoiceItem(ctx, item.ID) + if err != nil { + return billing.InvoiceItem{}, billing.Invoice{}, err + } + if invoice.ID == "" { + return updatedItem, billing.Invoice{}, nil + } + updatedInvoice, err := s.GetInvoice(ctx, invoice.ID) + if err != nil { + return billing.InvoiceItem{}, billing.Invoice{}, err + } + return updatedItem, updatedInvoice, nil +} + +// DeleteInvoiceItem removes one attached invoice line and saves the recomputed +// invoice totals in the same transaction. +func (s *SQLiteStore) DeleteInvoiceItem(ctx context.Context, itemID string, invoice billing.Invoice, timeline []billing.TimelineEntry) (billing.Invoice, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return billing.Invoice{}, err + } + defer tx.Rollback() + result, err := tx.ExecContext(ctx, `DELETE FROM invoice_items WHERE id = ?`, itemID) + if err != nil { + return billing.Invoice{}, err + } + changed, err := result.RowsAffected() + if err != nil { + return billing.Invoice{}, err + } + if changed == 0 { + return billing.Invoice{}, billing.ErrNotFound + } + if invoice.ID != "" { + if err := updateInvoiceTx(ctx, tx, invoice); err != nil { + return billing.Invoice{}, err + } + } + for _, entry := range timeline { + if err := s.insertTimeline(ctx, tx, entry); err != nil { + return billing.Invoice{}, err + } + } + if err := tx.Commit(); err != nil { + return billing.Invoice{}, err + } + if invoice.ID == "" { + return billing.Invoice{}, nil + } + return s.GetInvoice(ctx, invoice.ID) +} + +// DeleteInvoice removes a draft invoice together with its attached lines and +// timeline evidence. Children go first so invoice foreign keys stay satisfied. +func (s *SQLiteStore) DeleteInvoice(ctx context.Context, id string) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + var exists int + if err := tx.QueryRowContext(ctx, `SELECT COUNT(1) FROM invoices WHERE id = ?`, id).Scan(&exists); err != nil { + return err + } + if exists == 0 { + return billing.ErrNotFound + } + if _, err := tx.ExecContext(ctx, `DELETE FROM invoice_items WHERE invoice_id = ?`, id); err != nil { + return err + } + if _, err := tx.ExecContext(ctx, `DELETE FROM timeline_entries WHERE invoice_id = ?`, id); err != nil { + return err + } + if _, err := tx.ExecContext(ctx, `DELETE FROM invoices WHERE id = ?`, id); err != nil { + return err + } + return tx.Commit() +} + func (s *SQLiteStore) AttachInvoiceItems(ctx context.Context, invoice billing.Invoice, itemIDs []string, timeline []billing.TimelineEntry) (billing.Invoice, error) { if invoice.ID == "" { return billing.Invoice{}, billing.ErrInvalidInput diff --git a/internal/stripecompat/registry.go b/internal/stripecompat/registry.go index 7f03537..a8823ac 100644 --- a/internal/stripecompat/registry.go +++ b/internal/stripecompat/registry.go @@ -164,6 +164,7 @@ func DefaultClaims() []Claim { } add(http.MethodGet, "/v1/customers/search", Claim{Level: "L3", Stateful: true, Risks: []string{"supports a measured customer search subset for id, email, name, and metadata equality clauses joined by AND"}}) add(http.MethodGet, "/v1/products/search", Claim{Level: "L2", Risks: []string{"metadata equality filters only; no Stripe Search Query Language parity"}}) + add(http.MethodDelete, "/v1/products/{id}", Claim{Level: "L3", Stateful: true, Risks: []string{"removes local product evidence; existing prices keep referencing the deleted product id"}}) add(http.MethodGet, "/v1/prices/search", Claim{Level: "L3", Stateful: true, Risks: []string{"supports a measured prices search subset for active, type, lookup_key, and metadata equality clauses joined by AND"}}) for _, method := range []string{http.MethodGet, http.MethodPost} { @@ -210,6 +211,8 @@ func DefaultClaims() []Claim { add(http.MethodGet, "/v1/checkout/sessions", Claim{Level: "L4", Stateful: true, SDKSmoke: []string{"stripe-node"}, Risks: []string{"subscription and payment modes only (setup unsupported)"}}) add(http.MethodGet, "/v1/checkout/sessions/{id}", Claim{Level: "L4", Stateful: true, SDKSmoke: []string{"stripe-node"}, Risks: []string{"subscription and payment modes only (setup unsupported)"}}) add(http.MethodPost, "/v1/checkout/sessions/{id}/expire", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"checkout.session.expired"}, Risks: []string{"expires open sessions only; non-open sessions are rejected and payment recovery is not modeled"}}) + add(http.MethodGet, "/v1/checkout/sessions/{id}/line_items", Claim{Level: "L3", Stateful: true, Risks: []string{"line amounts are pre-discount; per-line discount and tax splits are not modeled"}}) + add(http.MethodPost, "/v1/checkout/sessions/{id}", Claim{Level: "L3", Stateful: true, Risks: []string{"open sessions only; metadata merge plus line_items[N][quantity] overrides with immutable prices; other Stripe update params are rejected"}}) add(http.MethodPost, "/v1/billing_portal/sessions", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"customer.subscription.updated", "customer.subscription.deleted", "payment_method.attached", "customer.updated"}, Risks: []string{"hosted portal is a local stub; portal configuration rendering and full Stripe-hosted portal behavior are not modeled"}}) portalConfigurationRisk := []string{"local portal configuration evidence only; the hosted portal does not render configuration features"} add(http.MethodGet, "/v1/billing_portal/configurations", Claim{Level: "L2", Stateful: true, Risks: portalConfigurationRisk}) @@ -272,6 +275,11 @@ func DefaultClaims() []Claim { add(http.MethodPost, "/v1/invoices/{id}/pay", Claim{Level: "L3", Stateful: true, ScorecardCases: []string{"invoices.pay.failed_invoice_succeeds", "invoices.pay.failed_invoice_declines_again", "invoices.one_time_invoice_flow.succeeds"}, WebhookEvents: []string{"payment_intent.succeeded", "payment_intent.payment_failed", "payment_intent.requires_action", "invoice.payment_succeeded", "invoice.payment_failed", "invoice.paid", "customer.subscription.updated"}, Risks: []string{"local retry/payment mutation only; send, void, collection, and dunning automation are not modeled"}}) add(http.MethodPost, "/v1/invoices/{id}/void", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"invoice.voided"}, Risks: []string{"moves open invoices to void with local evidence only; unvoiding is not modeled"}}) add(http.MethodPost, "/v1/invoices/{id}/mark_uncollectible", Claim{Level: "L3", Stateful: true, WebhookEvents: []string{"invoice.marked_uncollectible"}, Risks: []string{"moves open invoices to uncollectible with local evidence only; reopening is not modeled"}}) + add(http.MethodPost, "/v1/invoices/{id}", Claim{Level: "L3", Stateful: true, Risks: []string{"draft invoices only; bounded subset (description, days_until_due, default_payment_method, metadata merge)"}}) + add(http.MethodDelete, "/v1/invoices/{id}", Claim{Level: "L3", Stateful: true, Risks: []string{"draft invoices only; removes attached lines and timeline evidence with the invoice"}}) + add(http.MethodPost, "/v1/invoices/{id}/add_lines", Claim{Level: "L3", Stateful: true, Risks: []string{"draft invoices only; amount/description/currency lines through the invoice-item path; price_data and tax lines are not modeled"}}) + add(http.MethodPost, "/v1/invoices/{id}/update_lines", Claim{Level: "L3", Stateful: true, Risks: []string{"draft invoices only; amount/description updates recompute subtotal/total/amount_due"}}) + add(http.MethodPost, "/v1/invoices/{id}/remove_lines", Claim{Level: "L3", Stateful: true, Risks: []string{"draft invoices only; removing every line leaves a zero-amount draft"}}) add(http.MethodGet, "/v1/invoices/{id}/lines", Claim{Level: "L2", Stateful: true, Risks: []string{"returns local invoice items for manual one-time invoices only"}}) add(http.MethodGet, "/v1/invoices/{id}/payments", Claim{Level: "L2", Stateful: true, Risks: []string{"returns local invoice payment evidence only"}}) add(http.MethodPost, "/v1/invoices/create_preview", Claim{Level: "L3", Stateful: true, Risks: []string{"local next-period upcoming when no item overrides (with pending create_prorations + default_tax_rates/automatic_tax); item overrides keep subscription-update proration; pending invoice items and full multi-item Stripe parity are not modeled"}}) diff --git a/internal/stripecompat/registry_test.go b/internal/stripecompat/registry_test.go index 590de8f..bc7c15f 100644 --- a/internal/stripecompat/registry_test.go +++ b/internal/stripecompat/registry_test.go @@ -8,8 +8,8 @@ import ( func TestDefaultRegistryContainsCurrentPublicClaims(t *testing.T) { registry := DefaultRegistry() claims := registry.Claims() - if len(claims) != 193 { - t.Fatalf("default claims = %d, want 193", len(claims)) + if len(claims) != 201 { + t.Fatalf("default claims = %d, want 201", len(claims)) } checkout, ok := registry.Lookup(http.MethodPost, "/v1/checkout/sessions") From 0fe6022fddafc5a55b204bccdeb769b161b35118 Mon Sep 17 00:00:00 2001 From: midagedev Date: Sat, 29 Aug 2026 16:58:48 +0900 Subject: [PATCH 4/5] feat(api): billing family to 100% with line/payment/migrate adjuncts POST /v1/invoices/{id}/lines/{line_item_id} updates one draft line (amount/description/metadata) with the update_lines totals recomputation; attach_payment records a customer-matched PaymentIntent or payment record on a draft invoice as evidence while collection still runs through finalize/pay; POST /v1/subscriptions/{id}/migrate records billing_mode=flexible as subscription metadata evidence. POST /v1/payment_intents/{id} merges metadata and an evidenced description (amount/currency/status immutable), and GET /v1/credit_notes/{id}/lines returns one line derived from the stored note amount and memo/reason. OpenAPI inventory: 195/587 (33.2%) -> 200/587 (34.1%). Billing closes to 39/39, so all four P0 families (webhooks, checkout, billing, billing_portal) are now at 100%. --- CHANGELOG.md | 19 ++ README.md | 2 +- docs/COMPATIBILITY.md | 8 +- docs/COMPATIBILITY_TRACKING.md | 2 +- docs/GATE_STATUS.md | 6 +- docs/SIMULATION_CAPACITY_BACKLOG.md | 2 +- docs/STRIPE_API_COMPATIBILITY_ROADMAP.md | 8 +- docs/STRIPE_COMPATIBILITY_90_TARGET.md | 18 +- internal/api/api.go | 140 ++++++++++++- internal/api/billing_completion_test.go | 249 +++++++++++++++++++++++ internal/api/validation.go | 34 ++++ internal/billing/service.go | 88 +++++++- internal/stripecompat/registry.go | 5 + internal/stripecompat/registry_test.go | 4 +- 14 files changed, 559 insertions(+), 26 deletions(-) create mode 100644 internal/api/billing_completion_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f36a9a9..ae5ddfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ ## Unreleased +- The billing family is now full inventory (`39 / 39` OpenAPI operations): + all four P0 families (webhooks, checkout, billing, billing_portal) are at + 100%. +- `POST /v1/invoices/{id}/lines/{line_item_id}` updates one draft-invoice line + (`amount`, `description`, metadata) with the same totals recomputation as + `update_lines`. +- `POST /v1/invoices/{id}/attach_payment` records an attached PaymentIntent + (customer-matched) or payment record on a draft invoice as evidence; + collection still runs through `finalize`/`pay`. +- `POST /v1/subscriptions/{id}/migrate` records + `billing_mode[type]=flexible` (plus optional `proration_discounts`) as + subscription metadata evidence; flexible-billing proration recalculation is + not modeled. +- `POST /v1/payment_intents/{id}` updates intent metadata and evidenced + description; amount, currency, and status stay immutable. +- `GET /v1/credit_notes/{id}/lines` returns one line derived from the stored + credit-note amount and memo/reason. +- The OpenAPI inventory moves from `195 / 587` (`33.2%`) to `200 / 587` + (`34.1%`): billing `39 / 39`, payments `23 / 41`, payment_history `14 / 30`. - `GET /v1/checkout/sessions/{id}/line_items` returns the session's line items as expanded Stripe `item` objects (pre-discount amounts; per-line discount/tax splits are not modeled). diff --git a/README.md b/README.md index 635092a..7b14e73 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ curl -fsS "http://localhost:8080/api/diagnostics?limit=100" \ | Fixtures | Apply/snapshot/assert APIs | JSON/YAML input, fixture metadata isolation, structured pass/fail reports | | SaaS profile | Generic workspace billing profile | Plans, seats, members, export quota, extra export, payment history, support bundle, platform/connect-style webhook evidence | | Release state | Source plus GHCR image | Local Docker image builds and GHCR image workflow; no package/Homebrew/signed binary yet | -| Stripe API inventory | `195 / 587` operations, `33.2%` L1+ | OpenAPI route inventory is schema-visible for all `587` operations; implemented coverage is tracked in `docs/STRIPE_COMPATIBILITY_90_TARGET.md` | +| Stripe API inventory | `200 / 587` operations, `34.1%` L1+ | OpenAPI route inventory is schema-visible for all `587` operations; implemented coverage is tracked in `docs/STRIPE_COMPATIBILITY_90_TARGET.md` | Detailed compatibility matrix: `docs/COMPATIBILITY.md`. diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 7b22ee8..1aa4a54 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -108,18 +108,18 @@ traffic can keep using the unprefixed service URL. | Checkout completion | `POST /v1/checkout/sessions/{id}/complete`, `POST /api/checkout/sessions/{id}/complete`, `POST/DELETE /api/checkout/sessions/{id}/promotion_code` | Billtap-specific | Completes a sandbox checkout and creates subscription, invoice, payment intent, timeline, and checkout webhook evidence. Supports success plus deterministic failure aliases such as `card_declined`, `insufficient_funds`, `expired_card`, `incorrect_cvc`, `processing_error`, `authentication_required`, `payment_pending`, `canceled`, and documented Stripe test PaymentMethod IDs such as `pm_card_visa_chargeDeclined`. Hosted checkout can apply or remove a promotion code on an open `allow_promotion_codes` session via `POST/DELETE /api/checkout/sessions/{id}/promotion_code` (form `promotion_code=`), refreshing session totals before completion. | | Billing portal sessions | `POST /v1/billing_portal/sessions` | Partial | Returns a Stripe-like `billing_portal.session` object and Billtap portal URL for a known customer, including `flow`, `locale`, `on_behalf_of`, `return_url`, and `livemode` fields. Accepts `customer`, `return_url`, optional `configuration`, `locale`, `on_behalf_of`, and `flow_data`, with enum and required nested-field validation for the modeled portal flows. The hosted portal can simulate saved payment-method updates and subscription cancellation, then redirect to `return_url`; full Stripe-hosted portal configuration rendering is not modeled. | | Billing portal configurations | `GET/POST /v1/billing_portal/configurations`, `GET/POST /v1/billing_portal/configurations/{id}` | Partial | Local portal-configuration evidence with Stripe-shaped `business_profile`, `default_return_url`, `login_page`, `features[...]`, and `metadata`. Create applies Stripe-shaped feature defaults; update merges business profile, features, and metadata and bumps `updated`. List filters by `active` and `is_default`; the first configuration in a run becomes the default. The hosted portal does not render configuration features. | -| Subscriptions | `POST /v1/subscriptions`, `GET /v1/subscriptions`, `GET /v1/subscriptions/search`, `GET /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}`, `DELETE /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}/resume`, nested `GET/POST/DELETE /v1/customers/{id}/subscriptions...`, `GET/DELETE /v1/subscriptions/{id}/discount` | Partial | Create/list/retrieve subscriptions through the local checkout-completion state path. Update supports item replacement, metadata merge, single coupon or promotion-code discount application, `test_clock`, `cancel_at_period_end`, `pause_collection`, and accepted billing/proration evidence including `proration_date`, `proration_behavior`, `payment_behavior`, `billing_cycle_anchor`, and `trial_end`; resume clears local pause evidence and emits `customer.subscription.updated`. Top-level and nested customer lists support status and metadata equality filters such as `metadata[billtap_fixture_ref]`. Search supports `id`, `customer`, `status`, and metadata equality clauses joined by `AND`. Delete performs immediate sandbox cancellation. Test-clock and scenario clock advances can activate due trials, renew active periods with bounded discount math, fail configured renewals from subscription or customer metadata, and cancel period-end subscriptions in the local billing graph. | +| Subscriptions | `POST /v1/subscriptions`, `GET /v1/subscriptions`, `GET /v1/subscriptions/search`, `GET /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}`, `DELETE /v1/subscriptions/{id}`, `POST /v1/subscriptions/{id}/resume`, `POST /v1/subscriptions/{id}/migrate` (billing-mode evidence), nested `GET/POST/DELETE /v1/customers/{id}/subscriptions...`, `GET/DELETE /v1/subscriptions/{id}/discount` | Partial | Create/list/retrieve subscriptions through the local checkout-completion state path. Update supports item replacement, metadata merge, single coupon or promotion-code discount application, `test_clock`, `cancel_at_period_end`, `pause_collection`, and accepted billing/proration evidence including `proration_date`, `proration_behavior`, `payment_behavior`, `billing_cycle_anchor`, and `trial_end`; resume clears local pause evidence and emits `customer.subscription.updated`. Top-level and nested customer lists support status and metadata equality filters such as `metadata[billtap_fixture_ref]`. Search supports `id`, `customer`, `status`, and metadata equality clauses joined by `AND`. Delete performs immediate sandbox cancellation. Test-clock and scenario clock advances can activate due trials, renew active periods with bounded discount math, fail configured renewals from subscription or customer metadata, and cancel period-end subscriptions in the local billing graph. | | Subscription schedules | `POST /v1/subscription_schedules`, `GET /v1/subscription_schedules`, `GET /v1/subscription_schedules/{id}`, `POST /v1/subscription_schedules/{id}`, `POST /v1/subscription_schedules/{id}/cancel`, `POST /v1/subscription_schedules/{id}/release` | Partial | Local schedule evidence for one due phase on an existing subscription. Test-clock advance applies a due phase by replacing subscription items and emits `customer.subscription.updated`; complex multi-phase billing, proration, invoices, and schedule lifecycle parity are not modeled. | | Subscription items | `POST /v1/subscription_items`, `GET /v1/subscription_items`, `GET /v1/subscription_items/{id}`, `POST /v1/subscription_items/{id}`, `DELETE /v1/subscription_items/{id}` | Partial | List, retrieve, add, update, and remove local subscription items. List supports the `subscription` filter; update replaces `price`/`quantity` and echoes metadata and evidence-only `tax_rates`. Create, update, and delete accept `proration_behavior` (`none`/`create_prorations`/`always_invoice`) and `proration_date` through the same proration path as subscription update, applying the subscription's `default_tax_rates` after discounts: `always_invoice` issues a paid `subscription_update` invoice and repoints `latest_invoice`, and `create_prorations` defers the delta to the next renewal invoice. Deleting the last item is rejected. Item IDs are stored at creation so deletes no longer shift later IDs. Item-level `tax_rates` are evidence-only, and metered usage is not modeled. | -| Invoices | `POST /v1/invoices`, `GET /v1/invoices`, `GET /v1/invoices/search`, `GET /v1/invoices/{id}`, `POST /v1/invoices/{id}`, `DELETE /v1/invoices/{id}`, `POST /v1/invoices/{id}/finalize`, `POST /v1/invoices/{id}/pay`, `POST /v1/invoices/{id}/send`, `POST /v1/invoices/{id}/void`, `POST /v1/invoices/{id}/mark_uncollectible`, `POST /v1/invoices/{id}/add_lines`, `POST /v1/invoices/{id}/update_lines`, `POST /v1/invoices/{id}/remove_lines`, `GET /v1/invoices/{id}/lines`, `GET /v1/invoices/{id}/payments`, `POST /v1/invoices/create_preview`, `GET/POST /v1/invoices/upcoming` | Partial | List/retrieve/search invoices created by checkout and create a bounded manual one-time invoice flow. Search supports `id`, `customer`, `subscription`, and `status` clauses joined by `AND`. `pay` retries open checkout invoices with deterministic sandbox `payment_method` or `source` aliases, mutating invoice, subscription, payment-intent, timeline, and webhook evidence. Manual invoices accept `customer`, `currency`, `collection_method`, `default_payment_method`, `description`, `auto_advance=false`, `pending_invoice_items_behavior`, `payment_settings[payment_method_types]`, and `metadata[...]`; `pending_invoice_items_behavior=include` attaches the customer's same-currency pending invoice items once at create. Draft invoices can be updated in place (`POST /v1/invoices/{id}`: `description`, `days_until_due`, `default_payment_method`, metadata merge) and deleted with their attached lines and timeline evidence (`DELETE`); `add_lines`/`update_lines`/`remove_lines` mutate draft lines through the invoice-item path and recompute `subtotal`/`total`/`amount_due`. `finalize` creates local PaymentIntent evidence, and `pay` applies per-intent or customer-level default outcomes including success, card decline, and `requires_action`. `send` records local email evidence and emits `invoice.sent`; `void` moves an `open` invoice to `void` and emits `invoice.voided`; `mark_uncollectible` moves an `open` invoice to `uncollectible` and emits `invoice.marked_uncollectible`; non-open statuses return `invalid_request_error`. Invoice responses include `confirmation_secret`, local hosted/PDF paths, `payments.data.payment.payment_intent`, metadata, status transitions, automatic tax state, tax totals, payment settings, array-shaped `discounts`, `lines.data` serialized like the lines endpoint, and `post_payment_credit_notes_amount` on paid invoices. Preview endpoints calculate a Stripe-shaped subscription-update proration line from current period bounds, old/new price totals, `subscription_details[items]`, `subscription_details[proration_date]`, `subscription_details[proration_behavior]`, and `subscription_details[billing_cycle_anchor]`; previews without item overrides return the subscription's next billing cycle (items, discounts, and tax with `billing_reason: upcoming`) matching the next renewal invoice. A bounded single-discount subset updates `subtotal`, `total`, and `total_discount_amounts`; invoice rendering, automatic collection, and full dunning lifecycle are outside the modeled subset. | +| Invoices | `POST /v1/invoices`, `GET /v1/invoices`, `GET /v1/invoices/search`, `GET /v1/invoices/{id}`, `POST /v1/invoices/{id}`, `DELETE /v1/invoices/{id}`, `POST /v1/invoices/{id}/finalize`, `POST /v1/invoices/{id}/pay`, `POST /v1/invoices/{id}/send`, `POST /v1/invoices/{id}/void`, `POST /v1/invoices/{id}/mark_uncollectible`, `POST /v1/invoices/{id}/attach_payment`, `POST /v1/invoices/{id}/add_lines`, `POST /v1/invoices/{id}/update_lines`, `POST /v1/invoices/{id}/remove_lines`, `POST /v1/invoices/{id}/lines/{line_item_id}`, `GET /v1/invoices/{id}/lines`, `GET /v1/invoices/{id}/payments`, `POST /v1/invoices/create_preview`, `GET/POST /v1/invoices/upcoming` | Partial | List/retrieve/search invoices created by checkout and create a bounded manual one-time invoice flow. Search supports `id`, `customer`, `subscription`, and `status` clauses joined by `AND`. `pay` retries open checkout invoices with deterministic sandbox `payment_method` or `source` aliases, mutating invoice, subscription, payment-intent, timeline, and webhook evidence. Manual invoices accept `customer`, `currency`, `collection_method`, `default_payment_method`, `description`, `auto_advance=false`, `pending_invoice_items_behavior`, `payment_settings[payment_method_types]`, and `metadata[...]`; `pending_invoice_items_behavior=include` attaches the customer's same-currency pending invoice items once at create. Draft invoices can be updated in place (`POST /v1/invoices/{id}`: `description`, `days_until_due`, `default_payment_method`, metadata merge) and deleted with their attached lines and timeline evidence (`DELETE`); `add_lines`/`update_lines`/`remove_lines` (and the single-line `POST .../lines/{line_item_id}`) mutate draft lines through the invoice-item path and recompute `subtotal`/`total`/`amount_due`; `attach_payment` records a customer-matched PaymentIntent or payment record on a draft as evidence while collection still runs through `finalize`/`pay`. `finalize` creates local PaymentIntent evidence, and `pay` applies per-intent or customer-level default outcomes including success, card decline, and `requires_action`. `send` records local email evidence and emits `invoice.sent`; `void` moves an `open` invoice to `void` and emits `invoice.voided`; `mark_uncollectible` moves an `open` invoice to `uncollectible` and emits `invoice.marked_uncollectible`; non-open statuses return `invalid_request_error`. Invoice responses include `confirmation_secret`, local hosted/PDF paths, `payments.data.payment.payment_intent`, metadata, status transitions, automatic tax state, tax totals, payment settings, array-shaped `discounts`, `lines.data` serialized like the lines endpoint, and `post_payment_credit_notes_amount` on paid invoices. Preview endpoints calculate a Stripe-shaped subscription-update proration line from current period bounds, old/new price totals, `subscription_details[items]`, `subscription_details[proration_date]`, `subscription_details[proration_behavior]`, and `subscription_details[billing_cycle_anchor]`; previews without item overrides return the subscription's next billing cycle (items, discounts, and tax with `billing_reason: upcoming`) matching the next renewal invoice. A bounded single-discount subset updates `subtotal`, `total`, and `total_discount_amounts`; invoice rendering, automatic collection, and full dunning lifecycle are outside the modeled subset. | | Invoice items | `POST /v1/invoiceitems`, `GET /v1/invoiceitems`, `GET /v1/invoiceitems/{id}` | Partial | Creates, lists, and retrieves local invoice item evidence for manual one-time invoices. `customer`, `invoice`, `amount`, `currency`, `description`, and `metadata[...]` are accepted, or `pricing[price]` plus `quantity` instead of `amount` (line amount is the price's `unit_amount` times `quantity`, currency from the price). Omitting `invoice` stores the item as a pending customer item that `POST /v1/invoices` with `pending_invoice_items_behavior=include` attaches; `subscription` is accepted and echoed. Tax, discount, and full line mutation parity are not modeled. | -| Payment intents | `POST /v1/payment_intents`, `GET /v1/payment_intents`, `GET /v1/payment_intents/search`, `GET /v1/payment_intents/{id}`, `POST /v1/payment_intents/{id}/confirm`, `POST /v1/payment_intents/{id}/capture`, `POST /v1/payment_intents/{id}/cancel` | Partial | Create/list/retrieve/search and mutate local payment intents. Search supports `id`, `customer`, `status`, and metadata equality clauses joined by `AND`. `confirm` supports deterministic sandbox PaymentMethod aliases such as `pm_card_visa`, `pm_card_visa_chargeDeclined`, `pm_card_threeDSecure2Required`, `requires_action`, and local bank-transfer processing. Manual capture moves through `requires_capture` before `capture` succeeds. Direct one-time intents emit `payment_intent.created` and a terminal event when confirmed. Deferred per-intent outcomes and customer defaults are supported; `requires_action` can return either `use_stripe_sdk` or local `redirect_to_url` next-action payloads. | +| Payment intents | `POST /v1/payment_intents`, `GET /v1/payment_intents`, `GET /v1/payment_intents/search`, `GET /v1/payment_intents/{id}`, `POST /v1/payment_intents/{id}` (metadata/description update), `POST /v1/payment_intents/{id}/confirm`, `POST /v1/payment_intents/{id}/capture`, `POST /v1/payment_intents/{id}/cancel` | Partial | Create/list/retrieve/search and mutate local payment intents. Search supports `id`, `customer`, `status`, and metadata equality clauses joined by `AND`. `confirm` supports deterministic sandbox PaymentMethod aliases such as `pm_card_visa`, `pm_card_visa_chargeDeclined`, `pm_card_threeDSecure2Required`, `requires_action`, and local bank-transfer processing. Manual capture moves through `requires_capture` before `capture` succeeds. Direct one-time intents emit `payment_intent.created` and a terminal event when confirmed. Deferred per-intent outcomes and customer defaults are supported; `requires_action` can return either `use_stripe_sdk` or local `redirect_to_url` next-action payloads. | | Setup intents | `POST /v1/setup_intents`, `GET /v1/setup_intents`, `GET /v1/setup_intents/{id}`, `POST /v1/setup_intents/{id}/confirm`, `POST /v1/setup_intents/{id}/cancel` | Partial | Create/list/retrieve and mutate local setup intents with deterministic success, decline, and authentication-required aliases. Mandates, bank-account verification, and full SCA behavior are not modeled. | | Payment methods | `GET/POST /v1/payment_methods`, `GET/POST /v1/payment_methods/{id}`, `POST /v1/payment_methods/{id}/attach`, `POST /v1/payment_methods/{id}/detach`, `GET /v1/customers/{id}/payment_methods?type=card`, `GET /v1/customers/{id}/payment_methods/{id}` | Partial | Returns deterministic sandbox card PaymentMethod projections for known customers, including SDK-friendly `billing_details`, `card.checks`, `country`, `fingerprint`, `funding`, `networks`, `three_d_secure_usage`, `metadata`, and `redaction` fields. Query validation covers `type`, `allow_redisplay`, `limit`, and unknown parameters; non-card valid types return an empty local list. Customer fixtures can opt into empty or explicit payment-method lists for no-card and multi-card local scenarios. Create/attach/detach mutate local customer payment-method metadata and reject real card data; general update is accepted as a compatibility no-op for projected local cards. | | Customer cash balance | `GET /v1/customers/{id}/cash_balance`, `POST /v1/customers/{id}/cash_balance`, `GET /v1/customers/{id}/cash_balance_transactions`, `GET /v1/customers/{id}/cash_balance_transactions/{id}`, `POST /v1/test_helpers/customers/{id}/fund_cash_balance` | Partial | Local cash-balance evidence for bank-transfer smoke tests. The test-helper funding endpoint records a cash-balance transaction and settles processing bank-transfer PaymentIntents for that customer. Banking rails, reconciliation, currency conversion, and customer-balance invoice application are not modeled. | | Connect platform evidence | `GET /v1/account`, `POST /v1/accounts`, `GET /v1/accounts`, `GET /v1/accounts/{id}`, `POST /v1/accounts/{id}`, `DELETE /v1/accounts/{id}`, `POST /v1/account_links`, `POST /v1/account_sessions`, account capabilities, people/persons, external accounts, transfers/reversals, payouts, application fees/refunds | Partial | Persist local connected-account profiles, capability status, person evidence, bank-account evidence, transfers, transfer reversals, payouts, and application-fee refunds. Account links, account sessions, and login links return local URLs/client secrets. Account deletion returns a local deletion marker. Request traces preserve `Stripe-Account` routing evidence, and local Connect evidence can emit `transfer.*`, `payout.*`, and `application_fee.refunded` webhooks. KYC, identity verification, bank verification, real onboarding, balance movement, account closure, and settlement behavior are not modeled. | | Refunds | `POST /v1/refunds`, `GET /v1/refunds`, `GET/POST /v1/refunds/{id}`, `POST /v1/refunds/{id}/cancel` | Partial | Create/list/retrieve/update local refund evidence against an invoice, payment intent, or charge-like ID. Omitting `amount` refunds the full remaining refundable balance on the invoice or payment intent; requested amounts cannot exceed that balance. Creation emits `charge.refunded` and `charge.refund.updated`; status updates and test-clock settlement emit `charge.refund.updated`. Refund fixtures can seed `pending` and settle on `settle_at`/`available_on`. Balance transactions and processor accounting are outside the modeled subset. | -| Credit notes | `POST /v1/credit_notes`, `GET /v1/credit_notes`, `GET /v1/credit_notes/{id}`, `POST /v1/credit_notes/{id}/void` | Partial | Create/list/retrieve/void local credit note evidence for an invoice and emit `credit_note.created` / `credit_note.voided`. `memo`, `out_of_band_amount`, and `refund_amount` are accepted, persisted, and echoed with derived `credit_amount`; `out_of_band_amount` is external settlement and does not change customer cash balance. Line-level tax, discount, PDF, and customer-balance math are outside the modeled subset. | +| Credit notes | `POST /v1/credit_notes`, `GET /v1/credit_notes`, `GET /v1/credit_notes/{id}`, `GET /v1/credit_notes/{id}/lines`, `POST /v1/credit_notes/{id}/void` | Partial | Create/list/retrieve/void local credit note evidence for an invoice and emit `credit_note.created` / `credit_note.voided`. `memo`, `out_of_band_amount`, and `refund_amount` are accepted, persisted, and echoed with derived `credit_amount`; `out_of_band_amount` is external settlement and does not change customer cash balance. `GET .../lines` returns one line derived from the stored amount and memo/reason. Line-level tax, discount, PDF, and customer-balance math are outside the modeled subset. | | Disputes | `GET /v1/disputes`, `GET /v1/disputes/{id}`, `POST /v1/disputes/{id}`, `POST /v1/disputes/{id}/close`, `GET /v1/charges/{id}/dispute`, `POST /v1/charges/{id}/dispute` | Partial | Create, retrieve, list, update evidence, and close local dispute evidence for chargeback-style tests. Fixture seeding and APIs emit `charge.dispute.created`, `charge.dispute.updated`, `charge.dispute.funds_withdrawn`, and `charge.dispute.closed` where applicable. Representment deadlines, balance movements, and processor outcomes are outside the modeled subset. | | Test clocks | `POST /v1/test_helpers/test_clocks`, `GET /v1/test_helpers/test_clocks`, `GET /v1/test_helpers/test_clocks/{id}`, `POST /v1/test_helpers/test_clocks/{id}/advance` | Partial | Create/retrieve/list/advance persisted local clocks. Customers, subscriptions, and pending refunds can be attached with `test_clock`; advancing a clock processes attached trial activation, renewals, configured renewal failures, period-end cancellation, and refund settlement. | | Webhook endpoints | `POST /v1/webhook_endpoints`, `GET /v1/webhook_endpoints`, `GET /v1/webhook_endpoints/{id}`, `POST /v1/webhook_endpoints/{id}`, `PATCH /v1/webhook_endpoints/{id}`, `DELETE /v1/webhook_endpoints/{id}`, `GET /v1/webhook_endpoints/{id}/attempts` | Supported | Manage local webhook endpoints and inspect endpoint-scoped delivery attempts. Secrets are generated when omitted and masked in API responses. `enabled_events` supports exact event names, `*`, and prefix wildcards such as `invoice.*`. `PATCH` accepts the same local mutable fields as `POST`, including the `enabled` alias for `active`. | diff --git a/docs/COMPATIBILITY_TRACKING.md b/docs/COMPATIBILITY_TRACKING.md index 4b19339..8d47c8c 100644 --- a/docs/COMPATIBILITY_TRACKING.md +++ b/docs/COMPATIBILITY_TRACKING.md @@ -88,7 +88,7 @@ The long-running expansion goal is at least 90% OpenAPI operation coverage at The concrete target and chunk plan live in `docs/STRIPE_COMPATIBILITY_90_TARGET.md`. In short: -- current baseline: `195 / 587`, `33.2%` +- current baseline: `200 / 587`, `34.1%` - target: at least `529 / 587`, `90.0%` - maximum remaining `L0`: `58 / 587` - P0/P1 billing-lab flows still need deeper `L3-L6` evidence, not just broad diff --git a/docs/GATE_STATUS.md b/docs/GATE_STATUS.md index c244026..33d83ad 100644 --- a/docs/GATE_STATUS.md +++ b/docs/GATE_STATUS.md @@ -18,7 +18,7 @@ This is the public gate snapshot. Internal adoption evidence and raw handoff not | G11 Assertion Ergonomics | Passed locally | Structured pass/fail fixture assertions and fixture-scoped snapshots | | G12 Public Release Readiness | Passed locally | Public claims are tied to tests/scorecard cases; scorecard corpus has 50 release-blocking cases; Apache-2.0 `LICENSE` and `NOTICE` are present | | G13 Stripe API Expansion | In progress | Roadmap defines compatibility levels and endpoint-family priorities; OpenAPI inventory generator and optional workflow write JSON/Markdown coverage artifacts | -| G14 Stripe API 90% Program | In progress | `docs/STRIPE_COMPATIBILITY_90_TARGET.md` defines 90% L1+ target, current 195/587 baseline, family thresholds, and chunk plan | +| G14 Stripe API 90% Program | In progress | `docs/STRIPE_COMPATIBILITY_90_TARGET.md` defines 90% L1+ target, current 200/587 baseline, family thresholds, and chunk plan | ## Current Public Claim @@ -35,7 +35,7 @@ under Apache-2.0. - Coverage focus: request validation, protocol parameter acceptance, idempotency mismatch, deterministic checkout payment-error aliases, direct intent state machines, and billing lifecycle retry/renewal mutations -- OpenAPI operation baseline: `195 / 587`, `33.2%` +- OpenAPI operation baseline: `200 / 587`, `34.1%` - Long-running OpenAPI operation target: at least `529 / 587`, `90.0%`, at `L1+` with deeper P0/P1 behavior gates @@ -47,7 +47,7 @@ Verified on 2026-08-29 from branch `main`: - `go run ./cmd/billtap compatibility scorecard --output-dir /tmp/billtap-compatibility` - result: `imported=50 skipped=1 unsupported=1 mismatch=0 error=0` - `go run ./cmd/billtap compatibility inventory --openapi --output-dir /tmp/billtap-inventory --source stripe/openapi-v2261-2026-04-22.dahlia` - - result: `operations=587 implemented=195 inventory_only=392 schema_validated=587 implemented_percent=33.2` + - result: `operations=587 implemented=200 inventory_only=387 schema_validated=587 implemented_percent=34.1` - Current branch verification covers Go tests, compatibility scorecard, and inventory generation; `gofmt -l .` is clean. The forbidden internal keyword scan runs in CI with the configured keyword list. diff --git a/docs/SIMULATION_CAPACITY_BACKLOG.md b/docs/SIMULATION_CAPACITY_BACKLOG.md index 0b9d19d..df74e2f 100644 --- a/docs/SIMULATION_CAPACITY_BACKLOG.md +++ b/docs/SIMULATION_CAPACITY_BACKLOG.md @@ -36,7 +36,7 @@ Billtap currently has a stateful Stripe-like local subset with: - webhook retry, duplicate, delay, out-of-order, replay, historical replay, and delivery attempt evidence - diagnostic bundles, request traces, timeline evidence, and dashboard views -- OpenAPI inventory baseline: `195 / 587` implemented operations, `33.2%` +- OpenAPI inventory baseline: `200 / 587` implemented operations, `34.1%` ## P0 Regression-Paired Backlog diff --git a/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md b/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md index ab6c84e..a42352d 100644 --- a/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md +++ b/docs/STRIPE_API_COMPATIBILITY_ROADMAP.md @@ -63,7 +63,7 @@ The concrete expansion target is defined in `docs/STRIPE_COMPATIBILITY_90_TARGET.md`: - reach at least `90.0%` OpenAPI operation coverage at `L1+` -- move from the current `195 / 587` baseline to at least `529 / 587` +- move from the current `200 / 587` baseline to at least `529 / 587` - keep P0/P1 billing-lab families on deeper `L3-L6` gates where behavior matters - use broad `L1-L2` validation/fixture coverage for safe low-state and @@ -277,9 +277,9 @@ questions quickly. "generated_at": "2026-08-29T00:00:00Z", "summary": { "total_operations": 587, - "implemented_operations": 195, - "inventory_only_operations": 392, - "implemented_percent": 33.2, + "implemented_operations": 200, + "inventory_only_operations": 387, + "implemented_percent": 34.1, "families": [ { "family": "connect", diff --git a/docs/STRIPE_COMPATIBILITY_90_TARGET.md b/docs/STRIPE_COMPATIBILITY_90_TARGET.md index 4d85c61..c536309 100644 --- a/docs/STRIPE_COMPATIBILITY_90_TARGET.md +++ b/docs/STRIPE_COMPATIBILITY_90_TARGET.md @@ -11,8 +11,9 @@ claiming that every operation has deep payment-processing behavior. The 90% target is based on generated `stripe-api-inventory.json`: - **Overall target:** `summary.implemented_percent >= 90.0`. -- **Current baseline:** `195 / 587` operations, `33.2%`, using Stripe OpenAPI +- **Current baseline:** `200 / 587` operations, `34.1%`, using Stripe OpenAPI `2026-04-22.dahlia` (stripe/openapi tag `v2261`), re-measured on 2026-08-29. + All four P0 families are at 100%. - **Minimum target count:** `529 / 587` operations at `L1` or higher. - **Remaining inventory-only budget:** at most `58 / 587` operations at `L0`. @@ -59,13 +60,13 @@ Latest measured baseline from the Stripe OpenAPI `2026-04-22.dahlia` snapshot | --- | --- | ---: | ---: | ---: | ---: | --- | | P0 | webhooks | 7 | 7 | 100.0% | 7 | Expand connected-account routing, thin event fixtures, and replay evidence. | | P0 | checkout | 6 | 6 | 100.0% | 6 | Deepen SDK smoke and hosted-page parity evidence. | -| P0 | billing | 39 | 36 | 92.3% | 36 | Close the remaining invoice adjunct routes (`attach_payment`, single-line update, `migrate`). | +| P0 | billing | 39 | 39 | 100.0% | 36 | Deepen dunning, schedule, and credit-note scenario evidence. | | P0 | billing_portal | 5 | 5 | 100.0% | 5 | Deepen portal configuration fixtures and hosted portal rendering evidence. | | P1 | catalog | 54 | 28 | 51.9% | 49 | Add coupon, promotion code, tax-rate, and product/price search validation. | | P1 | customers | 31 | 12 | 38.7% | 28 | Add OpenAPI-backed validation, search/list parity, and payment source fixtures. | -| P1 | payments | 41 | 22 | 53.7% | 37 | Add PaymentIntent and SetupIntent create/confirm/capture/cancel state machines. | +| P1 | payments | 41 | 23 | 56.1% | 37 | Add PaymentIntent and SetupIntent create/confirm/capture/cancel state machines. | | P1 | connect | 53 | 53 | 100.0% | 48 | Deepen Connect SDK/adoption smoke, connected-account webhook routing, and v2 Core account inventory evidence. | -| P1 | payment_history | 30 | 13 | 43.3% | 27 | Add charge, refund, balance transaction, dispute, and payment history evidence. | +| P1 | payment_history | 30 | 14 | 46.7% | 27 | Add charge, refund, balance transaction, dispute, and payment history evidence. | | P3 | auxiliary | 321 | 13 | 4.0% | 289 | Keep inventory visible and add schema/fixture smoke only when adoption requires it. | ## PR Chunk Plan @@ -128,9 +129,16 @@ payment-method attach/detach. T10 also does not increase `summary.implemented_operations` by itself. It raises confidence and levels for already counted operations; new operation coverage must come from T3-T9. The planned T3-T9 delta is intentionally larger -than the `+334` operations needed to move the current `195 / 587` baseline to +than the `+329` operations needed to move the current `200 / 587` baseline to the `529 / 587` target. +The billing-completion wave on 2026-08-29 raised the generated inventory from +`195 / 587` (`33.2%`) to `200 / 587` (`34.1%`): single draft-invoice line +update, invoice `attach_payment`, subscription `migrate` (billing-mode +evidence), PaymentIntent metadata/description update, and credit-note lines. +Billing closes to `39 / 39`, so all four P0 families are at 100%; further +count growth now comes from P1 families and the auxiliary waves. + The P0 checkout/invoice wave on 2026-08-29 raised the generated inventory from `187 / 587` (`31.9%`) to `195 / 587` (`33.2%`): checkout session update and line_items listing, draft invoice update/delete and add_lines/update_lines/ diff --git a/internal/api/api.go b/internal/api/api.go index 1f2b867..15e1dfc 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -2274,6 +2274,15 @@ func (h *Handler) handleSubscription(w http.ResponseWriter, r *http.Request) { h.handleSubscriptionResume(w, r, subscriptionID) return } + if strings.HasSuffix(id, "/migrate") { + subscriptionID := strings.TrimSuffix(id, "/migrate") + if subscriptionID == "" || strings.Contains(subscriptionID, "/") { + h.notFound(w, r) + return + } + h.handleSubscriptionMigrate(w, r, subscriptionID) + return + } if id == "" || strings.Contains(id, "/") { h.notFound(w, r) return @@ -2442,6 +2451,41 @@ func (h *Handler) handleSubscription(w http.ResponseWriter, r *http.Request) { } } +// handleSubscriptionMigrate records a billing-mode migration request +// (POST /v1/subscriptions/{id}/migrate) as subscription metadata evidence. +// Flexible-billing proration recalculation itself is not modeled. +func (h *Handler) handleSubscriptionMigrate(w http.ResponseWriter, r *http.Request, subscriptionID string) { + if r.Method != http.MethodPost { + h.methodNotAllowed(w, r, "POST") + return + } + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateSubscriptionMigrate(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + subscription, err := h.billing.GetSubscription(r.Context(), subscriptionID) + if err != nil { + writeResult(w, nil, err) + return + } + metadata := map[string]string{"billtap_billing_mode": p.string("billing_mode[type]")} + if discounts := p.string("billing_mode[flexible][proration_discounts]"); discounts != "" { + metadata["billtap_proration_discounts"] = discounts + } + updated, err := h.billing.PatchSubscription(r.Context(), subscription.ID, billing.SubscriptionPatch{ + Metadata: metadata, + TimelineAction: "customer.subscription.migrated", + TimelineMessage: "Subscription billing-mode migration recorded", + TimelineSource: "stripe_compat_migrate", + }) + writeResult(w, h.stripeSubscription(r, updated), err) +} + func (h *Handler) handleSubscriptionResume(w http.ResponseWriter, r *http.Request, subscriptionID string) { if r.Method != http.MethodPost { h.methodNotAllowed(w, r, "POST") @@ -3827,6 +3871,52 @@ func (h *Handler) handleInvoice(w http.ResponseWriter, r *http.Request) { writeResult(w, stripeListFromRequest(r, payments), nil) return } + if len(parts) == 2 && parts[1] == "attach_payment" { + if r.Method != http.MethodPost { + h.methodNotAllowed(w, r, "POST") + return + } + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateInvoiceAttachPayment(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + invoice, err := h.billing.AttachInvoicePayment(r.Context(), id, p.string("payment_intent"), p.string("payment_record")) + writeResult(w, h.stripeInvoice(r.Context(), invoice), err) + return + } + if len(parts) == 3 && parts[1] == "lines" { + if r.Method != http.MethodPost { + h.methodNotAllowed(w, r, "POST") + return + } + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validateInvoiceLineItemUpdate(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + var amount *int64 + if p.has("amount") { + value := p.int64("amount") + amount = &value + } + var description *string + if p.has("description") { + value := p.string("description") + description = &value + } + invoice, err := h.billing.UpdateInvoiceLine(r.Context(), id, parts[2], amount, description, p.metadata()) + writeResult(w, h.stripeInvoice(r.Context(), invoice), err) + return + } if len(parts) == 2 && parts[1] == "add_lines" { if r.Method != http.MethodPost { h.methodNotAllowed(w, r, "POST") @@ -3872,7 +3962,7 @@ func (h *Handler) handleInvoice(w http.ResponseWriter, r *http.Request) { var invoice billing.Invoice for _, line := range patches { var err error - invoice, err = h.billing.UpdateInvoiceLine(r.Context(), id, line.id, line.amount, line.description) + invoice, err = h.billing.UpdateInvoiceLine(r.Context(), id, line.id, line.amount, line.description, nil) if err != nil { writeResult(w, nil, err) return @@ -4087,6 +4177,31 @@ func (h *Handler) handleCreditNote(w http.ResponseWriter, r *http.Request) { return } id, action, hasAction := strings.Cut(rest, "/") + if hasAction && action == "lines" { + if r.Method != http.MethodGet { + h.methodNotAllowed(w, r, "GET") + return + } + note, err := h.billing.GetCreditNote(r.Context(), id) + if err != nil { + writeResult(w, nil, err) + return + } + description := firstNonEmptyString(note.Memo, note.Reason, "Credit note") + data := []map[string]any{{ + "id": "cnli_" + note.ID, + "object": "credit_note_line_item", + "amount": -note.Amount, + "description": description, + "credit_note": note.ID, + "invoice": note.InvoiceID, + "quantity": 1, + "unit_amount": -note.Amount, + "livemode": false, + }} + writeJSON(w, http.StatusOK, stripeListFromRequest(r, data)) + return + } if hasAction && action == "void" { if r.Method != http.MethodPost { h.methodNotAllowed(w, r, "POST") @@ -4211,8 +4326,29 @@ func (h *Handler) handlePaymentIntent(w http.ResponseWriter, r *http.Request) { return } if !hasAction { + if r.Method == http.MethodPost { + p, err := parseParams(r) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := validatePaymentIntentUpdate(p); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + metadata := p.metadata() + if description := p.string("description"); description != "" { + if metadata == nil { + metadata = map[string]string{} + } + metadata["billtap_description"] = description + } + intent, err := h.billing.UpdatePaymentIntentDetails(r.Context(), id, metadata) + writeResult(w, stripePaymentIntent(intent), err) + return + } if r.Method != http.MethodGet { - h.methodNotAllowed(w, r, "GET") + h.methodNotAllowed(w, r, "GET, POST") return } paymentIntent, err := h.billing.GetPaymentIntent(r.Context(), id) diff --git a/internal/api/billing_completion_test.go b/internal/api/billing_completion_test.go new file mode 100644 index 0000000..e1344c9 --- /dev/null +++ b/internal/api/billing_completion_test.go @@ -0,0 +1,249 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/hckim/billtap/internal/billing" +) + +func setupDraftInvoiceWithLine(t *testing.T, handler http.Handler) (customer billing.Customer, invoiceID, lineID string) { + t.Helper() + customer = postForm[billing.Customer](t, handler, "/v1/customers", url.Values{"email": {"inv-final@example.test"}}) + invoice := postForm[struct { + ID string `json:"id"` + }](t, handler, "/v1/invoices", url.Values{ + "customer": {customer.ID}, + }) + line := postForm[struct { + ID string `json:"id"` + }](t, handler, "/v1/invoiceitems", url.Values{ + "customer": {customer.ID}, + "invoice": {invoice.ID}, + "amount": {"4000"}, + "currency": {"usd"}, + "description": {"Original"}, + }) + return customer, invoice.ID, line.ID +} + +func TestInvoiceSingleLineUpdate(t *testing.T) { + handler := newTestHandler(t) + _, invoiceID, lineID := setupDraftInvoiceWithLine(t, handler) + + // Unknown params are rejected. + status, body := postFormStatus(t, handler, "/v1/invoices/"+invoiceID+"/lines/"+lineID, url.Values{ + "quantity": {"2"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("unknown line update param status = %d body = %s, want 400", status, body) + } + // Missing lines are 404. + status, body = postFormStatus(t, handler, "/v1/invoices/"+invoiceID+"/lines/ii_missing", url.Values{ + "amount": {"1000"}, + }) + if status != http.StatusNotFound { + t.Fatalf("missing line update status = %d body = %s, want 404", status, body) + } + + updated := postForm[struct { + Subtotal int64 `json:"subtotal"` + }](t, handler, "/v1/invoices/"+invoiceID+"/lines/"+lineID, url.Values{ + "amount": {"2500"}, + "description": {"Revised"}, + "metadata[k]": {"v"}, + }) + if updated.Subtotal != 2500 { + t.Fatalf("single line update subtotal = %d, want 2500", updated.Subtotal) + } + lines := getJSON[struct { + Data []struct { + ID string `json:"id"` + Amount int64 `json:"amount"` + Description string `json:"description"` + } `json:"data"` + }](t, handler, "/v1/invoices/"+invoiceID+"/lines") + if len(lines.Data) != 1 || lines.Data[0].ID != lineID || lines.Data[0].Amount != 2500 || lines.Data[0].Description != "Revised" { + t.Fatalf("line after single update = %#v, want revised %s", lines.Data, lineID) + } + + // Non-draft invoices reject line edits. + _ = postJSON[map[string]json.RawMessage](t, handler, "/v1/invoices/"+invoiceID+"/finalize", map[string]string{}) + status, body = postFormStatus(t, handler, "/v1/invoices/"+invoiceID+"/lines/"+lineID, url.Values{ + "amount": {"1000"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("finalized invoice line update status = %d body = %s, want 400", status, body) + } +} + +func TestInvoiceAttachPayment(t *testing.T) { + handler := newTestHandler(t) + customer, invoiceID, _ := setupDraftInvoiceWithLine(t, handler) + + intent := postForm[struct { + ID string `json:"id"` + }](t, handler, "/v1/payment_intents", url.Values{ + "customer": {customer.ID}, + "amount": {"4000"}, + "currency": {"usd"}, + }) + + // payment_intent or payment_record is required. + status, body := postFormStatus(t, handler, "/v1/invoices/"+invoiceID+"/attach_payment", url.Values{}) + if status != http.StatusBadRequest { + t.Fatalf("attach_payment without params status = %d body = %s, want 400", status, body) + } + // Unknown intents are 404. + status, body = postFormStatus(t, handler, "/v1/invoices/"+invoiceID+"/attach_payment", url.Values{ + "payment_intent": {"pi_missing"}, + }) + if status != http.StatusNotFound { + t.Fatalf("attach_payment unknown intent status = %d body = %s, want 404", status, body) + } + + attached := postForm[struct { + Metadata map[string]string `json:"metadata"` + }](t, handler, "/v1/invoices/"+invoiceID+"/attach_payment", url.Values{ + "payment_intent": {intent.ID}, + }) + if attached.Metadata["billtap_attached_payment_intent"] != intent.ID { + t.Fatalf("attach_payment metadata = %#v, want recorded %s", attached.Metadata, intent.ID) + } +} + +func TestSubscriptionMigrate(t *testing.T) { + handler := newTestHandler(t) + customer := postForm[billing.Customer](t, handler, "/v1/customers", url.Values{"email": {"migrate@example.test"}}) + product := postForm[billing.Product](t, handler, "/v1/products", url.Values{"name": {"Migrate Plan"}}) + price := postForm[billing.Price](t, handler, "/v1/prices", url.Values{ + "product": {product.ID}, + "currency": {"usd"}, + "unit_amount": {"1900"}, + "recurring[interval]": {"month"}, + }) + subscription := postForm[prorationSubResponse](t, handler, "/v1/subscriptions", url.Values{ + "customer": {customer.ID}, + "items[0][price]": {price.ID}, + "items[0][quantity]": {"1"}, + }) + + // billing_mode[type] is required and enum-bound. + status, body := postFormStatus(t, handler, "/v1/subscriptions/"+subscription.ID+"/migrate", url.Values{}) + if status != http.StatusBadRequest { + t.Fatalf("migrate without billing_mode status = %d body = %s, want 400", status, body) + } + status, body = postFormStatus(t, handler, "/v1/subscriptions/"+subscription.ID+"/migrate", url.Values{ + "billing_mode[type]": {"legacy"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("migrate invalid billing_mode status = %d body = %s, want 400", status, body) + } + + migrated := postForm[struct { + ID string `json:"id"` + Metadata map[string]string `json:"metadata"` + }](t, handler, "/v1/subscriptions/"+subscription.ID+"/migrate", url.Values{ + "billing_mode[type]": {"flexible"}, + }) + if migrated.ID != subscription.ID || migrated.Metadata["billtap_billing_mode"] != "flexible" { + t.Fatalf("migrated subscription = %#v, want billing_mode=flexible evidence", migrated) + } +} + +func TestPaymentIntentUpdate(t *testing.T) { + handler := newTestHandler(t) + customer := postForm[billing.Customer](t, handler, "/v1/customers", url.Values{"email": {"pi-update@example.test"}}) + intent := postForm[struct { + ID string `json:"id"` + }](t, handler, "/v1/payment_intents", url.Values{ + "customer": {customer.ID}, + "amount": {"1200"}, + "currency": {"usd"}, + }) + + // Unknown params are rejected. + status, body := postFormStatus(t, handler, "/v1/payment_intents/"+intent.ID, url.Values{ + "amount": {"9999"}, + }) + if status != http.StatusBadRequest { + t.Fatalf("intent amount update status = %d body = %s, want 400", status, body) + } + + updated := postForm[struct { + Description string `json:"description"` + Metadata map[string]string `json:"metadata"` + }](t, handler, "/v1/payment_intents/"+intent.ID, url.Values{ + "description": {"Widget order"}, + "metadata[env]": {"ci"}, + }) + if updated.Description != "Widget order" { + t.Fatalf("updated description = %q, want Widget order", updated.Description) + } + if updated.Metadata["env"] != "ci" { + t.Fatalf("updated metadata = %#v, want env=ci", updated.Metadata) + } + fetched := getJSON[struct { + Description string `json:"description"` + }](t, handler, "/v1/payment_intents/"+intent.ID) + if fetched.Description != "Widget order" { + t.Fatalf("persisted description = %q, want Widget order", fetched.Description) + } +} + +func TestCreditNoteLines(t *testing.T) { + handler := newTestHandler(t) + customer := postForm[billing.Customer](t, handler, "/v1/customers", url.Values{"email": {"cn-lines@example.test"}}) + product := postForm[billing.Product](t, handler, "/v1/products", url.Values{"name": {"CN Plan"}}) + price := postForm[billing.Price](t, handler, "/v1/prices", url.Values{ + "product": {product.ID}, + "currency": {"usd"}, + "unit_amount": {"3000"}, + "recurring[interval]": {"month"}, + }) + session := postForm[billing.CheckoutSession](t, handler, "/v1/checkout/sessions", url.Values{ + "customer": {customer.ID}, + "line_items[0][price]": {price.ID}, + "line_items[0][quantity]": {"1"}, + "success_url": {"http://app.test/success"}, + "cancel_url": {"http://app.test/cancel"}, + }) + completion := postJSON[map[string]json.RawMessage](t, handler, "/api/checkout/sessions/"+session.ID+"/complete", map[string]string{"outcome": "payment_succeeded"}) + var completed billing.CheckoutSession + if err := json.Unmarshal(completion["session"], &completed); err != nil { + t.Fatalf("decode completed session: %v", err) + } + + note := postForm[struct { + ID string `json:"id"` + }](t, handler, "/v1/credit_notes", url.Values{ + "invoice": {completed.InvoiceID}, + "amount": {"1000"}, + "memo": {"Partial refund credit"}, + }) + lines := getJSON[struct { + Data []struct { + ID string `json:"id"` + Object string `json:"object"` + Amount int64 `json:"amount"` + Description string `json:"description"` + } `json:"data"` + }](t, handler, "/v1/credit_notes/"+note.ID+"/lines") + if len(lines.Data) != 1 { + t.Fatalf("credit note lines = %d, want 1", len(lines.Data)) + } + line := lines.Data[0] + if line.Object != "credit_note_line_item" || line.Amount != -1000 || line.Description != "Partial refund credit" { + t.Fatalf("credit note line = %#v, want negative 1000 with memo description", line) + } + + missingReq := httptest.NewRequest(http.MethodGet, "/v1/credit_notes/cn_missing/lines", nil) + missingRec := httptest.NewRecorder() + handler.ServeHTTP(missingRec, missingReq) + if missingRec.Code != http.StatusNotFound { + t.Fatalf("missing credit note lines status = %d body = %s, want 404", missingRec.Code, missingRec.Body.String()) + } +} diff --git a/internal/api/validation.go b/internal/api/validation.go index 3e4ee68..3eaa80a 100644 --- a/internal/api/validation.go +++ b/internal/api/validation.go @@ -56,6 +56,8 @@ var ( checkoutSessionLineItemQuantityRE = regexp.MustCompile(`^line_items\[(\d+)\]\[quantity\]$`) // Draft-invoice line mutation params (add_lines/update_lines/remove_lines). invoiceLineItemParamRE = regexp.MustCompile(`^line_items\[(\d+)\]\[(id|amount|description|currency)\]$`) + // Subscription migrate billing_mode params. + subscriptionMigrateParamRE = regexp.MustCompile(`^billing_mode\[(type|flexible)\](\[proration_discounts\])?$`) ) var stripePaymentMethodTypes = []string{ @@ -1617,6 +1619,38 @@ func validateInvoiceUpdate(p params) error { }) } +func validateInvoiceAttachPayment(p params) error { + return p.validate(paramSpec{ + Allowed: []string{"payment_intent", "payment_record"}, + RequiredAny: [][]string{{"payment_intent", "payment_record"}}, + }) +} + +func validateInvoiceLineItemUpdate(p params) error { + return p.validate(paramSpec{ + Allowed: []string{"amount", "description"}, + AllowMetadata: true, + }) +} + +func validateSubscriptionMigrate(p params) error { + return p.validate(paramSpec{ + AllowedRegex: []*regexp.Regexp{subscriptionMigrateParamRE}, + Required: []string{"billing_mode[type]"}, + EnumParams: map[string][]string{ + "billing_mode[type]": {"flexible"}, + "billing_mode[flexible][proration_discounts]": {"included", "itemized"}, + }, + }) +} + +func validatePaymentIntentUpdate(p params) error { + return p.validate(paramSpec{ + Allowed: []string{"description"}, + AllowMetadata: true, + }) +} + func validateInvoiceAddLines(p params) error { return p.validate(paramSpec{ AllowedRegex: []*regexp.Regexp{invoiceLineItemParamRE}, diff --git a/internal/billing/service.go b/internal/billing/service.go index 67d8320..d827ba1 100644 --- a/internal/billing/service.go +++ b/internal/billing/service.go @@ -1047,9 +1047,10 @@ func (s *Service) findInvoiceLine(ctx context.Context, invoice Invoice, lineID s } // UpdateInvoiceLine patches one attached draft-invoice line -// (POST /v1/invoices/{id}/update_lines): amount and description updates -// recompute subtotal/total/amount_due. -func (s *Service) UpdateInvoiceLine(ctx context.Context, invoiceID string, lineID string, amount *int64, description *string) (Invoice, error) { +// (POST /v1/invoices/{id}/update_lines and /lines/{line_item_id}): +// amount, description, and metadata updates recompute +// subtotal/total/amount_due. +func (s *Service) UpdateInvoiceLine(ctx context.Context, invoiceID string, lineID string, amount *int64, description *string, metadata map[string]string) (Invoice, error) { invoice, err := s.requireDraftInvoice(ctx, invoiceID) if err != nil { return Invoice{}, err @@ -1066,6 +1067,19 @@ func (s *Service) UpdateInvoiceLine(ctx context.Context, invoiceID string, lineI if description != nil { item.Description = *description } + if metadata != nil { + item.Metadata = copyMap(item.Metadata) + if item.Metadata == nil { + item.Metadata = map[string]string{} + } + for key, value := range metadata { + if value == "" { + delete(item.Metadata, key) + } else { + item.Metadata[key] = value + } + } + } _, updated, err := s.repo.UpdateInvoiceItem(ctx, item, invoice, []TimelineEntry{billingTimelineEntry( "invoiceline_updated_"+item.ID+"_"+at.Format(time.RFC3339Nano), "invoice.updated", @@ -1083,6 +1097,74 @@ func (s *Service) UpdateInvoiceLine(ctx context.Context, invoiceID string, lineI return updated, err } +// AttachInvoicePayment records a PaymentIntent (or payment record) attachment +// on a draft invoice (POST /v1/invoices/{id}/attach_payment) as evidence; +// collection itself still runs through finalize/pay. +func (s *Service) AttachInvoicePayment(ctx context.Context, invoiceID string, paymentIntentID string, paymentRecord string) (Invoice, error) { + invoice, err := s.requireDraftInvoice(ctx, invoiceID) + if err != nil { + return Invoice{}, err + } + if strings.TrimSpace(paymentIntentID) != "" { + intent, err := s.repo.GetPaymentIntent(ctx, paymentIntentID) + if err != nil { + return Invoice{}, err + } + if intent.CustomerID != invoice.CustomerID { + return Invoice{}, fmt.Errorf("%w: payment intent customer must match invoice customer", ErrInvalidInput) + } + } + at := s.now() + invoice.Metadata = copyMap(invoice.Metadata) + if invoice.Metadata == nil { + invoice.Metadata = map[string]string{} + } + if strings.TrimSpace(paymentIntentID) != "" { + invoice.Metadata["billtap_attached_payment_intent"] = strings.TrimSpace(paymentIntentID) + } + if strings.TrimSpace(paymentRecord) != "" { + invoice.Metadata["billtap_attached_payment_record"] = strings.TrimSpace(paymentRecord) + } + return s.repo.UpdateInvoice(ctx, invoice, []TimelineEntry{billingTimelineEntry( + "invoice_payment_attached_"+invoice.ID+"_"+at.Format(time.RFC3339Nano), + "invoice.updated", + "Invoice payment attached", + ObjectInvoice, + invoice.ID, + invoice.CustomerID, + "", + invoice.SubscriptionID, + invoice.ID, + invoice.PaymentIntentID, + map[string]string{"source": "invoice.attach_payment"}, + at, + )}) +} + +// UpdatePaymentIntentDetails merges metadata on a PaymentIntent +// (POST /v1/payment_intents/{id}); amount and status stay immutable. +func (s *Service) UpdatePaymentIntentDetails(ctx context.Context, intentID string, metadata map[string]string) (PaymentIntent, error) { + intent, err := s.repo.GetPaymentIntent(ctx, intentID) + if err != nil { + return PaymentIntent{}, err + } + if metadata == nil { + return intent, nil + } + intent.Metadata = copyMap(intent.Metadata) + if intent.Metadata == nil { + intent.Metadata = map[string]string{} + } + for key, value := range metadata { + if value == "" { + delete(intent.Metadata, key) + } else { + intent.Metadata[key] = value + } + } + return s.repo.UpdatePaymentIntent(ctx, intent, nil) +} + // RemoveInvoiceLines detaches lines from a draft invoice // (POST /v1/invoices/{id}/remove_lines), recomputing totals per removed line. func (s *Service) RemoveInvoiceLines(ctx context.Context, invoiceID string, lineIDs []string) (Invoice, error) { diff --git a/internal/stripecompat/registry.go b/internal/stripecompat/registry.go index a8823ac..5cc68ca 100644 --- a/internal/stripecompat/registry.go +++ b/internal/stripecompat/registry.go @@ -280,6 +280,9 @@ func DefaultClaims() []Claim { add(http.MethodPost, "/v1/invoices/{id}/add_lines", Claim{Level: "L3", Stateful: true, Risks: []string{"draft invoices only; amount/description/currency lines through the invoice-item path; price_data and tax lines are not modeled"}}) add(http.MethodPost, "/v1/invoices/{id}/update_lines", Claim{Level: "L3", Stateful: true, Risks: []string{"draft invoices only; amount/description updates recompute subtotal/total/amount_due"}}) add(http.MethodPost, "/v1/invoices/{id}/remove_lines", Claim{Level: "L3", Stateful: true, Risks: []string{"draft invoices only; removing every line leaves a zero-amount draft"}}) + add(http.MethodPost, "/v1/invoices/{id}/lines/{line_item_id}", Claim{Level: "L3", Stateful: true, Risks: []string{"draft invoices only; amount/description/metadata updates recompute subtotal/total/amount_due; pricing and tax line edits are not modeled"}}) + add(http.MethodPost, "/v1/invoices/{id}/attach_payment", Claim{Level: "L3", Stateful: true, Risks: []string{"draft invoices only; records the attached PaymentIntent or payment record as invoice evidence; collection still runs through finalize/pay"}}) + add(http.MethodPost, "/v1/subscriptions/{id}/migrate", Claim{Level: "L3", Stateful: true, Risks: []string{"records billing_mode=flexible as subscription metadata evidence; flexible-billing proration recalculation is not modeled"}}) add(http.MethodGet, "/v1/invoices/{id}/lines", Claim{Level: "L2", Stateful: true, Risks: []string{"returns local invoice items for manual one-time invoices only"}}) add(http.MethodGet, "/v1/invoices/{id}/payments", Claim{Level: "L2", Stateful: true, Risks: []string{"returns local invoice payment evidence only"}}) add(http.MethodPost, "/v1/invoices/create_preview", Claim{Level: "L3", Stateful: true, Risks: []string{"local next-period upcoming when no item overrides (with pending create_prorations + default_tax_rates/automatic_tax); item overrides keep subscription-update proration; pending invoice items and full multi-item Stripe parity are not modeled"}}) @@ -298,6 +301,7 @@ func DefaultClaims() []Claim { add(http.MethodPost, "/v1/credit_notes", Claim{Level: "L2", Stateful: true, WebhookEvents: []string{"credit_note.created", "credit_note.voided"}, Risks: []string{"local credit note evidence only; line/tax/customer-balance math is not modeled"}}) add(http.MethodGet, "/v1/credit_notes/{id}", Claim{Level: "L2", Stateful: true}) add(http.MethodPost, "/v1/credit_notes/{id}/void", Claim{Level: "L2", Stateful: true, WebhookEvents: []string{"credit_note.voided"}}) + add(http.MethodGet, "/v1/credit_notes/{id}/lines", Claim{Level: "L2", Stateful: true, Risks: []string{"returns one synthesized line derived from the stored credit note amount and memo/reason; stored line-item breakdowns are not modeled"}}) add(http.MethodGet, "/v1/payment_intents", statefulL3) add(http.MethodPost, "/v1/payment_intents", Claim{Level: "L3", Stateful: true, ScorecardCases: []string{"payment_intents.create.confirm.succeeds", "payment_intents.confirm.card_decline"}, Risks: []string{"local state machine only; no card processing or full PaymentIntent parameter parity"}}) @@ -305,6 +309,7 @@ func DefaultClaims() []Claim { add(http.MethodPost, "/v1/payment_intents/{id}/confirm", Claim{Level: "L3", Stateful: true, ScorecardCases: []string{"payment_intents.confirm.card_decline"}, Risks: []string{"local deterministic outcome aliases only"}}) add(http.MethodPost, "/v1/payment_intents/{id}/capture", Claim{Level: "L3", Stateful: true, Risks: []string{"local capture marks the intent succeeded; partial capture accounting is not modeled"}}) add(http.MethodPost, "/v1/payment_intents/{id}/cancel", Claim{Level: "L3", Stateful: true}) + add(http.MethodPost, "/v1/payment_intents/{id}", Claim{Level: "L3", Stateful: true, Risks: []string{"metadata merge plus evidenced description only; amount, currency, and status stay immutable"}}) add(http.MethodGet, "/v1/payment_intents/search", Claim{Level: "L3", Stateful: true, Risks: []string{"supports a measured PaymentIntent search subset for id, customer, status, and metadata equality clauses joined by AND"}}) add(http.MethodGet, "/v1/setup_intents", statefulL3) add(http.MethodPost, "/v1/setup_intents", Claim{Level: "L3", Stateful: true, ScorecardCases: []string{"setup_intents.create.confirm.succeeds"}, Risks: []string{"local state machine only; mandates and full SCA flows are not modeled"}}) diff --git a/internal/stripecompat/registry_test.go b/internal/stripecompat/registry_test.go index bc7c15f..471dd3d 100644 --- a/internal/stripecompat/registry_test.go +++ b/internal/stripecompat/registry_test.go @@ -8,8 +8,8 @@ import ( func TestDefaultRegistryContainsCurrentPublicClaims(t *testing.T) { registry := DefaultRegistry() claims := registry.Claims() - if len(claims) != 201 { - t.Fatalf("default claims = %d, want 201", len(claims)) + if len(claims) != 206 { + t.Fatalf("default claims = %d, want 206", len(claims)) } checkout, ok := registry.Lookup(http.MethodPost, "/v1/checkout/sessions") From d63093fc97a4d7cbf5444b167afe223061caa4eb Mon Sep 17 00:00:00 2001 From: midagedev Date: Sat, 29 Aug 2026 17:20:34 +0900 Subject: [PATCH 5/5] Add CI/license/GHCR badges and a social preview image --- README.md | 4 ++++ docs/assets/social-preview.png | Bin 0 -> 215703 bytes 2 files changed, 4 insertions(+) create mode 100644 docs/assets/social-preview.png diff --git a/README.md b/README.md index 7b14e73..e4e8654 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Billtap +[![CI](https://github.com/midagedev/billtap/actions/workflows/ci.yml/badge.svg)](https://github.com/midagedev/billtap/actions/workflows/ci.yml) +[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) +[![GHCR image](https://img.shields.io/badge/ghcr-billtap-2496ED?logo=docker&logoColor=white)](https://github.com/midagedev/billtap/pkgs/container/billtap) + Full-stack Stripe-style billing sandbox for local development, CI scenarios, and controlled staging checks. diff --git a/docs/assets/social-preview.png b/docs/assets/social-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..8b81a5e0b689f1dd5882fe3c3b9a71b62b895e8f GIT binary patch literal 215703 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�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%0Dy6d~yAn==MZY8cb^30U;+b@CXRnJ)TOQnvTA< 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