Appearance
Payment Service
payment-service owns payment orders, provider transactions, provider webhooks, reconciliation, and webhook idempotency for the Go monetization stack.
Public /api/hooks/sepay-payment, /api/wallet/webhooks/sepay, and wallet or subscription payment pages remain legacy-proxied. The native /v1/* endpoints are for service tests, migration tooling, and later gateway adapters only.
HOCTAPAZ v2 production has no legacy fallback. Its strict production route table intentionally excludes both public SePay callback paths, so they return 404 until a scoped SePay credential is stored in Kubernetes, configured at the provider, and promoted through a reviewed route change. Local and isolated candidate route tables remain available for provider-auth rehearsal.
Boundary
Owns:
- Payment order state and provider reference codes.
- Provider webhook receipts and replay/idempotency keys.
- Provider transactions and manual reconciliation records.
- Payment outcome events for downstream billing/wallet consumers.
Does not own:
- Wallet balance or AZ Credit ledger entries.
- Subscription activation, plan entitlements, or invoices.
- Usage/quota decisions.
- IAM/profile data.
Native Routes
| Method | Path | Purpose |
|---|---|---|
POST | /v1/payment-orders | Create a payment order for SUBSCRIPTION, WALLET_TOP_UP, or OTHER. |
GET | /v1/payment-orders/{orderId} | Read payment order state. |
GET | /v1/payment-orders/{orderId}/transactions | List provider transactions attached to an order. |
POST | /v1/payment-orders/{orderId}/reconcile | Add manual provider evidence and recompute payment outcome. |
POST | /v1/provider-webhooks/{provider} | Record a provider webhook idempotently. |
POST | /v1/legacy/hooks/sepay-payment | Rehearse the legacy SePay payment-hook payload and response envelope. |
POST | /v1/legacy/wallet/webhooks/sepay | Rehearse the legacy SePay wallet-webhook payload and response envelope. |
provider currently accepts sepay.
The /v1/legacy/* endpoints are not public route promotions. They normalize the old flat bank-transfer and nested payment-gateway SePay payloads into the native idempotent webhook path. Wallet, unmatched, replay, and review outcomes return the old { "success": true } acknowledgment without native internals; subscription payment-order matches include a compatibility data object. Payment-service still does not activate subscriptions or credit wallets.
Webhook Provider Auth
Native SePay webhook ingestion can require provider authentication by setting SEPAY_WEBHOOK_API_KEY and/or SEPAY_WEBHOOK_SECRET. When configured, POST /v1/provider-webhooks/sepay rejects requests without X-SePay-Webhook-Api-Key or without a valid X-SePay-Signature: sha256=<hex hmac-sha256 raw body> signature. Public legacy webhook routes still remain legacy-proxied until gateway parity, provider-auth, browser/runtime, and rollback evidence exist.
If SePay sends X-SePay-Timestamp, the signature payload is <timestamp>.<raw body>, matching the legacy webhook helper. Set SEPAY_WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS to require the timestamp and reject stale or future-dated requests outside the configured tolerance window. Leave it unset for local replay compatibility.
Webhook Idempotency
Webhook calls require an idempotencyKey in the body or one of these headers:
Idempotency-KeyX-Idempotency-KeyX-SePay-Webhook-IdX-SePay-Request-Id
Replays with the same provider/idempotency key return replay: true and do not create another transaction. Provider transaction IDs are also de-duplicated by the service store.
Events
The foundation records internal payment events such as:
payment.order.createdpayment.order.paid.v1payment.order.needs_reviewpayment.webhook.unmatchedpayment.webhook.replayedpayment.order.reconciled
These events are the integration point for future billing-service and wallet-service agents. Payment success must be consumed by those owner services; payment-service does not activate subscriptions or credit wallets.
When BILLING_SERVICE_URL/BILLING_SERVICE_BASE_URL and WALLET_SERVICE_URL/WALLET_SERVICE_BASE_URL are configured, payment-service stores payment.order.paid.v1 rows in the payment event outbox and dispatches due envelopes to each service at /v1/events/payment-order-paid. The current sink is internal HTTP so service-route rehearsal stays explicit; the durable outbox owns retry/dead-letter state until a shared broker transport is selected.
Each HTTP delivery identifies payment-service with X-Internal-Service and adds X-Internal-Token from PAYMENT_INTERNAL_SERVICE_TOKEN or INTERNAL_SERVICE_TOKEN. If an HTTP target is configured without either token, the publisher is disabled rather than sending an unauthenticated paid event; the durable outbox remains the retry/dead-letter source once a valid publisher is configured.
Set MONETIZATION_EVENT_TRANSPORT=nats or PAYMENT_EVENT_TRANSPORT=nats to switch the payment outbox dispatcher to NATS JetStream. The publisher uses NATS_URL or MONETIZATION_NATS_URL, stream MONETIZATION by default, subject monetization.payment.order.paid.v1, and the payment outbox event id as the broker message id. Keep the default http mode until billing/wallet broker subscribers, live smoke evidence, route rehearsal, and rollback notes exist.
Operator replay/dead-letter handling uses tools/monetization-outbox/cmd/monetization-outbox. The payment outbox event id is also the outbox row id and JetStream message id. list, list-dead, and inspect read only the payment-service database; replay --confirm resets the payment outbox row to PENDING and lets the existing dispatcher republish the same event through HTTP fallback or NATS.
When AUDIT_SERVICE_URL or AUDIT_SERVICE_BASE_URL is configured, payment-service also projects persisted payment events to audit-service /v1/audit-events as best-effort audit rows. The sink uses sourceService=payment-service, sourceEventId=<payment event id>, category=DOMAIN, severity=INFO, metadataSchemaVersion=1, retentionPolicy=payment-events, and createdAt -> occurredAt. It stays separate from the billing/wallet outbox so audit-service failures do not affect payment fanout retries or replay behavior.
Verification
bash
GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/... -count=1
GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/internal/eventfanout -count=1The current focused tests cover paid webhook replay, amount mismatch review, unmatched provider evidence, legacy flat bank-transfer parsing, nested payment-gateway subscription parsing, legacy success envelopes, and provider auth enforcement on the rehearsal endpoints.
Optional Postgres repository integration:
bash
PAYMENT_SERVICE_POSTGRES_TEST_DATABASE_URL='postgres://hoctapaz:hoctapaz@localhost:5433/hoctapaz_platform?sslmode=disable' \
GOTOOLCHAIN=go1.25.11 go test ./services/payment-service/internal/repository \
-run TestPostgresPaymentWebhookReplayIntegration -count=1 -v