Appearance
Handoff: SVC-019-payment-webhook-reconcile-proof payment-service webhook reconcile proof
Scope
- Service/area:
payment-service - Goal: prove provider webhook/reconcile idempotency,
payment.order.paid.v1replay behavior, and downstream billing/wallet effects without moving subscription or wallet ownership into payment-service. - Non-goals: public/default hook promotion, browser proof, gateway route changes, billing subscription mutation, wallet balance/ledger mutation, provider secret management, live provider calls, or changes to legacy source.
Docs Read
docs/architecture/service-responsibility-matrix.mddocs/architecture/legacy-api-map.mddocs/agents/service-tasks/payment-service.mddocs/agents/handoffs/SVC-019-sepay-legacy-response-parity.mddocs/agents/service-tasks/billing-service.mddocs/agents/service-tasks/wallet-service.mddocs/agents/service-tasks/monetization-broker-transport.mddocs/agents/service-agent-next-slice-queue.jsondocs/agents/service-agent-next-slice-closeout-ledger.jsondocs/agents/agent-handoff-format.mddocs/api/payment-service.mddocs/qa/monetization-broker-live-smoke.mdcontracts/events/README.md
Legacy Evidence Read
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/wallet/wallet.utils.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/wallet/wallet.controller.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/wallet/wallet.service.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/wallet/wallet-sepay-webhook.spec.ts/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/prisma/schema.prisma- Legacy source was read-only evidence; no files under
/Users/velikho/Desktop/WORKING/HOCTAPAZ/were changed.
Files Changed
docs/agents/handoffs/SVC-019-payment-webhook-reconcile-proof.mdservices/payment-service/internal/usecase/payment.goservices/payment-service/internal/usecase/payment_reconcile_test.goservices/payment-service/internal/repository/memory.goservices/payment-service/internal/repository/postgres.goservices/payment-service/internal/repository/postgres_integration_test.go- No payment-service migration, OpenAPI, gateway route, billing/wallet code, or legacy file was changed by this proof handoff.
Behavior Implemented
- Fixed reconcile replay handling so
payment.order.reconciledis appended and published only for the first idempotent reconcile outcome; a replay returns onlypayment.webhook.replayedand cannot create a second reconcile audit event. - Added a focused regression test covering the first reconcile, the same-key replay, and exactly one paid/reconciled event. The existing foundation still normalizes authenticated SePay webhooks, persists provider transactions by idempotent keys, emits
payment.order.paid.v1, and dispatches through a durable payment outbox. Billing-service and wallet-service remain the owners of subscription and wallet mutations. - Strengthened the matched webhook write boundary: provider webhook, transaction, order status, paid/review event, and
payment.order.paid.v1outbox now share one owner-store transaction. Serialization failure after webhook construction rolls every row back, and a later provider retry completes the effect once. Existing partially stored matched webhook rows can also resume from the retry input when no transaction exists; completed transactions still take the replay-only path.
Tests And Verification
- Command:
GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/... - Result: passed with
-count=1. - Evidence: payment-service auditsink, eventfanout, HTTP, repository, and use-case packages passed.
- Command:
make test-monetization-event-chain test-monetization-routes test-monetization-broker-transport - Result: passed.
- Evidence: event-chain, route guard, broker transport, all monetization eventfanout packages, and outbox tool tests passed; wallet route and broker self-tests also passed.
- Command:
GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/internal/usecase -run TestReconcilePaymentOrderReplayDoesNotAppendSecondReconciledEvent -count=1 -v - Result: passed.
- Evidence: first reconcile emitted one paid and one reconciled event; same-key replay returned one replay event and emitted no second reconciled event.
- Command:
GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/internal/eventfanout ./services/payment-service/internal/auditsink -count=1 -v - Result: passed.
- Evidence: fanout only forwards the allowlisted
payment.order.paid.v1event; audit projection removes sensitive metadata keys such as secrets and tokens. - Command:
GOTOOLCHAIN=go1.25.11 go test -race ./services/payment-service/internal/usecase -run 'TestReconcilePaymentOrderReplayDoesNotAppendSecondReconciledEvent|TestPaymentAuditPublisherIsBestEffortAndPaidReplaySafe|TestRecordProviderWebhookEmitsVersionedPaidEvent' -count=1 - Result: passed.
- Evidence: replay/reconcile, paid-event contract, and audit best-effort paths passed under the race detector.
- Command:
PAYMENT_SERVICE_POSTGRES_TEST_DATABASE_URL=postgres://... GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/internal/repository -run TestPostgresPaymentWebhookReplayIntegration -count=1 -v - Result: passed against a temporary local Postgres cluster on port
55440with an isolated test schema; the cluster and temporary directory were removed after the run. - Actual command used:
PAYMENT_SERVICE_POSTGRES_TEST_DATABASE_URL=postgres://hoctapaz@localhost:55440/postgres?sslmode=disable GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/internal/repository -run TestPostgresPaymentWebhookReplayIntegration -count=1 -v. - Evidence: the integration test persisted one approved SePay webhook, replayed it, and verified one provider transaction remained.
- Current command:
GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/internal/repository -run TestPostgresPaymentWebhookReplayIntegration -count=1 -v - Current result: package passed, but
TestPostgresPaymentWebhookReplayIntegrationwas skipped becausePAYMENT_SERVICE_POSTGRES_TEST_DATABASE_URLwas not set; this is self-test evidence, not live database evidence.
Atomic Outcome Reverification (2026-07-14)
- Command:
PAYMENT_SERVICE_POSTGRES_TEST_DATABASE_URL=postgres://127.0.0.1:ephemeral-port/payment?sslmode=disable GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/internal/repository -run '^(TestPostgresPaymentWebhookReplayIntegration|TestPostgresPaymentWebhookOutcomeAtomicRollbackAndRetry)$' -count=1 -v. - Result: passed in a fresh local PostgreSQL cluster and a test-created schema; the cluster was stopped and removed after the test.
- Evidence: deliberately failing paid-event JSON serialization left zero webhook, transaction, event, and outbox rows. The identical retry then created exactly one of each; another replay returned the existing transaction without a second paid outbox item.
Current Reverification (2026-07-13)
- Command:
PAYMENT_SERVICE_POSTGRES_TEST_DATABASE_URL='postgres://hoctapaz_payment:[email protected]:5433/hoctapaz_payment_db?sslmode=disable' GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/internal/repository -run TestPostgresPaymentWebhookReplayIntegration -count=1 -v. - Result: passed against
hoctapaz-payment-proof-20260713, using a test-created schema in the isolated payment database. The first SePay webhook and its duplicate left one persisted provider transaction. - Command:
MONETIZATION_EVENT_TRANSPORT=nats MONETIZATION_BROKER_SMOKE_CONFIRM=broker-runtime ... bash scripts/test/monetization-broker-live-smoke.sh. - Result: passed with artifact
output/qa/monetization-broker-live-smoke-svc019-20260713/. The native payment subscription eventpayevt_1d90ead9ba3959005b0950bfand top-up eventpayevt_ef6a21ace82f7f84a9a89de7both reachedDELIVEREDthrough NATS JetStream. Billing emitted and delivered the linked activation and entitlement events; usage allowedcredits_monthly; wallet reached exactly1977credits from the1200subscription grant plus777top-up. - Runtime replay: reposting the exact subscription SePay body returned only
payment.webhook.replayed; the payment order still had one transaction, billing still had one activation event keyed bysourcePaymentEventId=payevt_1d90ead9ba3959005b0950bf, and the wallet remained at1977credits. - Command:
make test-monetization-event-chain test-monetization-routes test-monetization-broker-transportandGOTOOLCHAIN=go1.25.11 go test -race ./services/payment-service/internal/usecase ./services/payment-service/internal/eventfanout -count=1. - Result: passed. This rechecks the payment/billing/wallet/usage contract chain, route guard, broker transport and replay/outbox behavior under the race detector.
Required Evidence
- webhook/reconcile idempotency proof: provider webhook tests and the Postgres integration cover authenticated webhook handling, reference/amount matching, replay detection, and one persisted provider transaction after duplicate delivery;
TestReconcilePaymentOrderReplayDoesNotAppendSecondReconciledEventcovers replay-safe reconcile event state. - paid-event replay proof: payment event contract tests verify
payment.order.paid.v1payload fields including purpose, account, organization, provider transaction, VND amount, credit amount, and idempotency key; outbox tests cover delivered, retry, and dead-letter transitions. - ownership and sensitive-data proof: payment fanout allowlists only
payment.order.paid.v1and never sends review/replay/reconcile events as entitlement commands; billing/wallet/usage remain downstream owners. Audit metadata recursively removes secret/token/credential fields, while the paid event envelope contains only the payment contract fields. - downstream billing/wallet proof: fresh live NATS artifact
output/qa/monetization-broker-live-smoke-svc019-20260713/records delivered subscription/top-up payment events, billing activation and entitlement events, usage allowance, final wallet credits, and a duplicate-webhook check that leaves only one downstream activation. Current package/event-chain/broker tests also verify billing and wallet subscribers reuse replay-safe consumer paths.
Route State
- Legacy route: public
/api/hooks/sepay-paymentand/api/wallet/webhooks/sepayremain legacy-proxied by default. - Native route:
/v1/provider-webhooks/sepayplus service-local/v1/legacy/hooks/sepay-paymentand/v1/legacy/wallet/webhooks/sepayremain rehearsal/native surfaces. - Gateway state:
public hook cutover requires idempotency, paid-event replay, downstream billing/wallet proof, and rollback evidence; no gateway route was changed or promoted. - Delegated gateway scope: none; payment-service does not own gateway route changes.
- Rollback: keep
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.json; if a rehearsal regresses, roll back the service-local adapter or outbox delivery configuration without changing public route state.
Data Notes
- Tables touched: only the named Compose project's disposable payment, billing, wallet, and usage databases; no shared or legacy tables.
- Migration run: payment migrations were applied by the integration harness to its isolated schema. The live NATS smoke used the project's owner-service migrations and disposable
acct_svc019_payment_20260713/org_svc019_payment_20260713records. - Validation report:
docker compose -p hoctapaz-payment-proof-20260713 -f deploy/docker-compose.yml down -v --remove-orphanscompleted after the proof; no project container, volume, or listener remained on5433,8096-8099, or4222. No default Gateway route was modified.
Risks / Blockers
- The 2026-07-13 broker artifact is a fresh local Compose proof, not provider production traffic. Provider credentials and the public legacy hook still require separate reviewed evidence.
- Payment outbox delivery remains at-least-once. The runtime duplicate test and downstream idempotent consumers prove this tested duplicate path, but production monitoring and dead-letter operation remain required.
- Public hook promotion still needs a reviewed gateway rehearsal, current provider/runtime or browser/caller proof, and rollback evidence.
- Payment-service must not activate subscriptions or mutate wallet balances directly; downstream event consumers remain the source of those effects.
- This bounded handoff does not complete
payment-serviceor the broader service rollout goal.
Next Step
- Run a fresh non-default gateway/provider rehearsal for both legacy SePay hook shapes, attach request/response and rollback artifacts, then review public hook promotion only after downstream replay evidence remains green.