Appearance
Handoff: SVC-016-subscription-entitlement-replay-proof billing-service subscription entitlement replay proof
Scope
- Service/area:
billing-service - Task pack:
docs/agents/service-tasks/billing-service.md - Goal: accept the bounded
SVC-016next slice by proving subscription/payment replay safety, entitlement event idempotency, and route rollback notes before public subscription route promotion. - Non-goals: default gateway route promotion, public subscription page cutover, provider webhook ingestion inside billing, wallet ledger writes inside billing, usage counter ownership inside billing, prorated upgrade/downgrade policy, or legacy source edits under
/Users/velikho/Desktop/WORKING/HOCTAPAZ/.
Docs Read
go-platform/docs/agents/service-tasks/billing-service.mdgo-platform/docs/agents/handoffs/SVC-016-renewal-mutation-no-proration.mdgo-platform/docs/agents/service-agent-next-slice-queue.jsongo-platform/docs/agents/service-agent-next-slice-closeout-ledger.jsongo-platform/docs/agents/agent-handoff-format.mdgo-platform/docs/agents/service-tasks/payment-service.mdgo-platform/docs/agents/service-tasks/wallet-service.mdgo-platform/docs/agents/service-tasks/usage-service.mdgo-platform/docs/agents/service-tasks/monetization-broker-transport.mdgo-platform/contracts/events/README.mdgo-platform/docs/api/billing-service.mdgo-platform/services/billing-service/README.md
Legacy Evidence Read
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/wallet/az-credit.service.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/wallet/az-credit-business.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/wallet/az-credit.schemas.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/wallet/az-credit.controller.ts- Legacy AZ package/subscription ownership is split in Go so billing owns plans/subscriptions/invoices/entitlement events, payment-service owns provider payment evidence, usage-service owns quota/entitlement consumption, and wallet-service owns AZ Credit ledger movement.
- Legacy source was read-only evidence only; no file under
/Users/velikho/Desktop/WORKING/HOCTAPAZ/was edited.
Files Changed
- Disposable Postgres runner follow-up:
go-platform/scripts/test/billing-subscription-replay-postgres-isolated.shgo-platform/services/billing-service/README.mdgo-platform/docs/agents/service-tasks/billing-service.mdgo-platform/docs/agents/handoffs/SVC-016-subscription-entitlement-replay-proof.md
- Current audit changes:
go-platform/services/billing-service/internal/usecase/billing.gogo-platform/services/billing-service/internal/usecase/replay_recovery_test.gogo-platform/docs/agents/handoffs/SVC-016-subscription-entitlement-replay-proof.md
go-platform/docs/agents/service-agent-next-slice-closeout-ledger.jsongo-platform/docs/agents/service-agent-next-slice-closeout-ledger.mdgo-platform/docs/agents/service-agent-accepted-handoffs.mdgo-platform/docs/qa/service-agent-goal-completion-audit.mdgo-platform/docs/qa/service-agent-readiness-smoke.md
Behavior Implemented
- The original proof handoff did not change runtime code. This audit fixed the post-commit failure window where subscription/payment mutation succeeded but billing
AppendEventsfailed, leaving a later idempotent replay unable to restore missing downstream outbox rows. - Payment replay now reuses existing activation events by idempotency key and appends only missing
billing.subscription.activated.v1andbilling.entitlements.updated.v1rows. A successful replay does not duplicate wallet credit or usage entitlement events. - Zero-price subscription creation and renewal replay now use the same missing-event repair behavior for entitlement rows. Paid renewal still waits for payment-service evidence before emitting wallet/usage events.
- Existing billing-service payment activation remains replay-safe by payment idempotency/source event ids and emits each billing subscription/entitlement outbox row once, repairing only missing rows on a later replay.
- Existing renewal mutation remains no-proration and idempotent: paid renewals create open invoices pending payment-service activation, while zero-price renewals emit only usage entitlement events.
- Existing monetization event contracts keep
payment.order.paid.v1,billing.subscription.activated.v1, andbilling.entitlements.updated.v1as the cross-service chain. Usage-service consumes entitlement updates; wallet-service consumes payment-backed subscription activation for credit grants. - Required evidence covered: entitlement event replay proof, idempotency proof, subscription route rollback notes.
Tests And Verification
Current audit verification:
Command:
GOTOOLCHAIN=go1.25.11 go test ./services/billing-service/internal/usecase -run 'Test(PaymentOrderPaidReplayRepairsMissingBillingOutbox|ZeroPriceRenewalReplayRepairsMissingEntitlementOutbox)' -count=1 -vResult: passed.
Evidence: a forced append failure after the subscription mutation was recovered by replay; payment replay restored both activation and entitlement outbox rows, and zero-price renewal replay restored the missing entitlement row without duplicating the existing create event.
Command:
GOTOOLCHAIN=go1.25.11 go test ./services/billing-service/... -count=1Result: passed.
Command:
GOTOOLCHAIN=go1.25.11 go test -race ./services/billing-service/internal/usecase ./services/billing-service/internal/eventfanout -count=1Result: passed.
Command:
BILLING_SERVICE_POSTGRES_TEST_DATABASE_URL='postgres://hoctapaz:hoctapaz@localhost:5433/hoctapaz_platform?sslmode=disable' GOTOOLCHAIN=go1.25.11 go test ./services/billing-service/internal/repository -run TestPostgresBillingSubscriptionReplayIntegration -count=1 -vResult: passed against local Postgres
5433; the test used an isolated schema and dropped it during cleanup.Command:
make test-monetization-event-chainResult: passed.
Command:
make test-monetization-routesResult: passed; route guard self-test passed and no default route table changed.
Command:
make test-monetization-broker-transportResult: passed; broker transport coverage and
MONETIZATION_BROKER_SMOKE_SELF_TEST=1passed. This is self-test evidence, not live NATS runtime evidence.Command:
MONETIZATION_EVENT_TRANSPORT=nats MONETIZATION_BROKER_SMOKE_CONFIRM=broker-runtime make test-monetization-broker-liveResult: passed against the local Compose NATS and monetization services. The smoke delivered subscription payment and wallet top-up events, observed
1200subscription credits plus777top-up credits, and verified final wallet credits1977.Runtime note: the Compose services were already running from older images, so this local smoke proves the broker/consumer handoff but does not prove that the new working-tree replay-repair code was loaded into those containers.
Command:
GOTOOLCHAIN=go1.25.11 go test ./services/billing-service/...Result: passed.
Evidence: billing-service cmd, auditsink, domain, eventfanout, HTTP, repository, and usecase packages passed.
Command:
make test-monetization-event-chainResult: passed.
Evidence: event-chain guard verified event contracts and ran payment-service, billing-service, wallet-service, and usage-service tests covering subscription payment routing, billing activation events, wallet top-up routing, and usage entitlement updates.
Command:
make test-monetization-broker-transportResult: passed.
Evidence: broker transport coverage passed, live-smoke self-test passed, focused eventfanout packages passed, and monetization outbox tool tests passed.
Command:
BILLING_SERVICE_POSTGRES_TEST_DATABASE_URL=postgres://... GOTOOLCHAIN=go1.25.11 go test ./services/billing-service/internal/repository -run TestPostgresBillingSubscriptionReplayIntegration -count=1Result: passed with an ephemeral local Postgres instance.
Evidence: the actual run used a temporary
initdb/pg_ctlPostgres server andBILLING_SERVICE_POSTGRES_TEST_DATABASE_URL=postgres://127.0.0.1:<temp-port>/postgres?sslmode=disable;TestPostgresBillingSubscriptionReplayIntegrationpassed and the Postgres data directory was stopped and removed by the command trap.workflowVerification:
GOTOOLCHAIN=go1.25.11 go test ./services/billing-service/...make test-monetization-event-chainmake test-monetization-broker-transportBILLING_SERVICE_POSTGRES_TEST_DATABASE_URL=postgres://... GOTOOLCHAIN=go1.25.11 go test ./services/billing-service/internal/repository -run TestPostgresBillingSubscriptionReplayIntegration -count=1
requiredVerification:
GOTOOLCHAIN=go1.25.11 go test ./services/billing-service/...make test-monetization-event-chainmake test-monetization-broker-transportBILLING_SERVICE_POSTGRES_TEST_DATABASE_URL=postgres://... GOTOOLCHAIN=go1.25.11 go test ./services/billing-service/internal/repository -run TestPostgresBillingSubscriptionReplayIntegration -count=1
Current Working-Tree Re-verification
On 2026-07-13, the current worktree passed the billing package suite, make test-monetization-event-chain, and make test-monetization-broker-transport. The focused replay-recovery tests and go test -race for billing usecase/eventfanout also passed.
TestPostgresBillingSubscriptionReplayIntegration was rerun against a fresh Compose Postgres project on 127.0.0.1:5433; it created and dropped its own schema. The Compose project and volume were removed after the test. No default gateway route, public subscription route, legacy source, or persistent shared database was used.
Disposable Postgres Runner Re-verification
On 2026-07-14, the opt-in scripts/test/billing-subscription-replay-postgres-isolated.sh runner passed both focused repository tests with one grouped regex: TestPostgresBillingSubscriptionReplayIntegration and TestPostgresBillingPaymentReplayRepairsMissingOutboxIntegration.
- Command:
env BILLING_SUBSCRIPTION_REPLAY_POSTGRES_CONFIRM=disposable-postgres ./scripts/test/billing-subscription-replay-postgres-isolated.sh - Result: passed against
postgres:16-alpinepublished on a Docker-assigned127.0.0.1port afterpg_isreadysucceeded. - Lifecycle evidence: the runner supplied its own test-only DSN, unset
DATABASE_URL, and its exit trap removed the named container; a post-rundocker ps -afilter returned no matching containers. - Subscription replay evidence: migrations, plan upsert, subscription create and replay, payment activation and replay, entitlement snapshot, renewal, and renewal replay passed in an isolated schema.
- Missing-outbox repair evidence: an injected Postgres trigger made the first activation outbox append fail after subscription activation. Replaying the same payment event repaired exactly the activation and entitlement events plus their two outbox rows; the final replay added no rows.
This is repository-level proof against one disposable local Postgres process. It does not use or validate a configured/shared service database, start the billing HTTP service, deliver outbox events to usage-service or wallet-service, exercise provider webhooks or a live broker, change gateway/default routes, or prove production migrations, traffic, observability, and rollback behavior.
Current Recheck (2026-07-15)
The current worktree reran both disposable PostgreSQL runners. Billing passed TestPostgresBillingSubscriptionReplayIntegration and TestPostgresBillingPaymentReplayRepairsMissingOutboxIntegration on a new Docker-assigned loopback port. Usage independently passed TestPostgresUsageConsumeReplayIntegration and TestPostgresUsageEntitlementSnapshotsSerializeAndRejectStaleOverwrite on its own disposable database. make test-monetization-event-chain, make test-monetization-broker-transport, and make test-monetization-routes also passed. Post-run docker ps -a found no matching disposable containers, and deploy/gateway/routes.json was unchanged.
This remains local replay/contract evidence only; it does not promote public subscription routes, validate a provider webhook, or prove a deployed broker, browser flow, production migration, or rollback.
Route State
- Legacy route: public
/api/wallet*,/api/admin/wallet*, subscription pages, provider hooks, and/wallet?tab=billingremain legacy-proxied by default. - Native route: billing-service native
/v1/subscriptions*,/v1/invoices*,/v1/events/payment-order-paid, and monetization outbox/eventfanout routes remain internal/service routes. - Gateway state: no default gateway route table changed and no public subscription route was promoted.
- Route default state: subscription route promotion requires replay/idempotency proof before public cutover.
- Rollback: keep
deploy/gateway/routes.jsonas the active default route table. If a future non-default billing route rehearsal is added, rollback must returnGATEWAY_ROUTE_TABLEtodeploy/gateway/routes.jsonor remove the native subscription entries, then rerun monetization route guards plus live/browser smoke before promotion review.
Data Notes
- Tables touched: temporary schemas only inside disposable Postgres instances; the 2026-07-14 runner did not connect to a configured/shared database.
- Migration run: billing-service migrations were applied inside an isolated temporary Postgres schema by
TestPostgresBillingSubscriptionReplayIntegration. - Validation report: command output from the temporary Postgres run showed
TestPostgresBillingSubscriptionReplayIntegrationpassed; no live production-like billing data was migrated. - Replay proof: create subscription, replay create, payment activation, replay payment activation, entitlement snapshot, renewal, and replay renewal all passed on Postgres.
Risks / Blockers
- Public subscription route promotion remains blocked on route-specific live/browser proof and rollback evidence.
- Paid renewal wallet credits still require payment-service paid events and wallet-service consumption of
billing.subscription.activated.v1. - Usage entitlement replay is proven through focused event-chain tests and billing Postgres replay, but not against a long-running shared broker.
- The local NATS smoke passed, but production broker observability, public subscription route rehearsal, browser evidence, and rollback proof remain open. The smoke used disposable account and organization ids; native monetization APIs do not provide teardown endpoints.
- Prorated upgrades, downgrades, credit carry-over, and partial-period billing remain unsupported by design.
Next Step
- Add a non-default public subscription route rehearsal only after payment-service, wallet-service, usage-service, and gateway live/browser rollback proof are ready.