Skip to content

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.v1 replay 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.md
  • docs/architecture/legacy-api-map.md
  • docs/agents/service-tasks/payment-service.md
  • docs/agents/handoffs/SVC-019-sepay-legacy-response-parity.md
  • docs/agents/service-tasks/billing-service.md
  • docs/agents/service-tasks/wallet-service.md
  • docs/agents/service-tasks/monetization-broker-transport.md
  • docs/agents/service-agent-next-slice-queue.json
  • docs/agents/service-agent-next-slice-closeout-ledger.json
  • docs/agents/agent-handoff-format.md
  • docs/api/payment-service.md
  • docs/qa/monetization-broker-live-smoke.md
  • contracts/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.md
  • services/payment-service/internal/usecase/payment.go
  • services/payment-service/internal/usecase/payment_reconcile_test.go
  • services/payment-service/internal/repository/memory.go
  • services/payment-service/internal/repository/postgres.go
  • services/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.reconciled is appended and published only for the first idempotent reconcile outcome; a replay returns only payment.webhook.replayed and 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.v1 outbox 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.v1 event; 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 55440 with 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 TestPostgresPaymentWebhookReplayIntegration was skipped because PAYMENT_SERVICE_POSTGRES_TEST_DATABASE_URL was 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 event payevt_1d90ead9ba3959005b0950bf and top-up event payevt_ef6a21ace82f7f84a9a89de7 both reached DELIVERED through NATS JetStream. Billing emitted and delivered the linked activation and entitlement events; usage allowed credits_monthly; wallet reached exactly 1977 credits from the 1200 subscription grant plus 777 top-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 by sourcePaymentEventId=payevt_1d90ead9ba3959005b0950bf, and the wallet remained at 1977 credits.
  • Command: make test-monetization-event-chain test-monetization-routes test-monetization-broker-transport and GOTOOLCHAIN=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; TestReconcilePaymentOrderReplayDoesNotAppendSecondReconciledEvent covers replay-safe reconcile event state.
  • paid-event replay proof: payment event contract tests verify payment.order.paid.v1 payload 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.v1 and 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-payment and /api/wallet/webhooks/sepay remain legacy-proxied by default.
  • Native route: /v1/provider-webhooks/sepay plus service-local /v1/legacy/hooks/sepay-payment and /v1/legacy/wallet/webhooks/sepay remain 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_20260713 records.
  • Validation report: docker compose -p hoctapaz-payment-proof-20260713 -f deploy/docker-compose.yml down -v --remove-orphans completed after the proof; no project container, volume, or listener remained on 5433, 8096-8099, or 4222. 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-service or 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.

Go-platform documentation is generated from repository Markdown.