Appearance
Wallet Service
wallet-service owns AZ Credit wallets, balances, and immutable ledger entries. It is the source of truth for credit grants, debits, refunds, holds, releases, and admin balance adjustments.
Public /api/wallet*, /api/admin/wallet*, and /wallet?tab=billing routes remain legacy-proxied. Native /v1/* routes are for service tests, migration tooling, and future gateway adapters only.
Gateway Rehearsal
The first non-default gateway rehearsal for the wallet admin review queue is:
deploy/gateway/routes.wallet-admin-review-native-example.jsondeploy/gateway/routes.wallet-admin-review-native-localhost-example.json
Both route only GET /api/admin/wallet/reviews to wallet-service /v1/admin/wallet/reviews with ADMIN auth. POST, nested admin wallet routes, public /api/wallet*, and the default deploy/gateway/routes.json table stay legacy-proxied. Rollback is to stop using either rehearsal file and run the gateway with the default route table.
Boundary
Owns:
- Account wallet rows scoped by optional organization.
- Available and held AZ Credit balances.
- Append-only wallet ledger entries.
- Idempotent credit, debit, hold, release, refund, sale, and adjustment entries.
- Optional best-effort audit projection for successful ledger entries using
sourceService=wallet-serviceandsourceEventId=<ledger entry id>.
Does not own:
- SePay provider webhooks or provider reconciliation. Those belong to
payment-service. - Subscription lifecycle, plan entitlements, and invoices. Those belong to
billing-service. - Feature gates and quota policy. Those belong to
usage-service.
Broker Subscriber
When WALLET_EVENT_TRANSPORT=nats or MONETIZATION_EVENT_TRANSPORT=nats is configured, wallet-service starts durable NATS JetStream subscribers for monetization.payment.order.paid.v1 and monetization.billing.subscription.activated.v1. The broker handlers reuse the same /v1/events/* usecases and idempotency keys as HTTP fallback, so broker redelivery, HTTP replay, and manual replay converge on the same wallet ledger rows. Default transport remains HTTP until broker runtime proof and route rollback evidence are complete.
Native Routes
| Method | Path | Purpose |
|---|---|---|
POST | /v1/wallets/ensure | Create or return an account wallet. |
GET | /v1/wallets/by-account/{accountId} | Read wallet by account and optional organizationId. |
GET | /v1/wallets/{walletId} | Read wallet balance. |
GET | /v1/wallets/{walletId}/ledger | List immutable ledger entries. |
POST | /v1/wallets/{walletId}/ledger-entries | Apply an idempotent balance movement. |
POST | /v1/wallets/{walletId}/adjustments | Apply an admin credit/debit adjustment. |
POST | /v1/top-up-requests | Create an internal top-up review request. |
POST | /v1/top-up-requests/{topUpRequestId}/approve | Approve a reviewable top-up and credit AZ Credits once. |
POST | /v1/top-up-requests/{topUpRequestId}/reject | Reject a reviewable top-up without ledger movement. |
POST | /v1/withdrawal-requests | Create a withdrawal request and hold AZ Credits. |
POST | /v1/withdrawal-requests/{withdrawalRequestId}/mark-paid | Mark a reviewable withdrawal paid and debit held credits once. |
POST | /v1/withdrawal-requests/{withdrawalRequestId}/reject | Reject a reviewable withdrawal and release held credits once. |
GET | /v1/admin/wallet/reviews | List wallet-owned top-up and withdrawal review queue items for admin aggregation. |
POST | /v1/events/payment-order-paid | Consume a payment.order.paid.v1 top-up envelope from payment-service. |
POST | /v1/events/billing-subscription-activated | Consume a billing.subscription.activated.v1 subscription credit grant from billing-service. |
The two HTTP event routes are internal-only. Paid top-ups require X-Internal-Service: payment-service; subscription grants require X-Internal-Service: billing-service; both require X-Internal-Token matched against WALLET_INTERNAL_SERVICE_TOKEN or INTERNAL_SERVICE_TOKEN. Missing or wrong credentials return 401 before a ledger mutation. A missing receiver token returns 503 and trusts no event.
Idempotency
Every balance movement requires an idempotencyKey. Replays for the same wallet/idempotency key return replay: true, the existing ledger entry, and do not update balances a second time.
POST /v1/events/payment-order-paid only credits purpose=WALLET_TOP_UP events. It requires explicit amountCredits, creates or loads the account wallet, and keys the top-up ledger replay on the payment event id.
POST /v1/events/billing-subscription-activated credits SUBSCRIPTION_CREDIT using the billing event monthlyCredits amount. It creates or loads the account wallet and keys replay on the source-scoped billing event identity, so redelivery of the same activation event returns the original ledger row without moving the balance twice. Positive monthlyCredits grants must include sourcePaymentEventId; zero-credit events are ignored without creating wallets or ledger entries.
Top-up review only allows approval/rejection from PENDING_REVIEW or NEEDS_REVIEW. Approval uses topup:<requestId>:approved; replaying an already approved request returns the original ledger row. Rejected, cancelled, and expired top-ups are terminal and cannot be reviewed again.
Withdrawal review only allows paid/rejected transitions from PENDING_REVIEW. Creating a withdrawal holds credits with withdrawal:<requestId>:hold; marking paid uses withdrawal:<requestId>:paid; rejecting uses withdrawal:<requestId>:rejected and releases held credits. Paid withdrawals can be replayed safely, while rejected/cancelled/paid terminal states cannot be reviewed through the opposite action.
GET /v1/admin/wallet/reviews is the wallet-owned read side for admin review summaries. It combines top-up and withdrawal requests into one page and supports kind, status, accountId, organizationId, limit, and offset filters. Admin-service may aggregate this endpoint later, but it must not become the source of truth for wallet review state.
When AUDIT_SERVICE_URL or AUDIT_SERVICE_BASE_URL is configured, successful ledger entries are also posted to audit-service /v1/audit-events best-effort with category=DOMAIN, severity=INFO, outcome=SUCCESS, metadataSchemaVersion=1, retentionPolicy=wallet-ledger, and createdAt -> occurredAt. Publish failures are ignored so wallet balance movements remain available.
Verification
bash
GOTOOLCHAIN=go1.25.11 go test ./services/wallet-service/... -count=1Optional Postgres repository integration:
bash
WALLET_SERVICE_POSTGRES_TEST_DATABASE_URL='postgres://hoctapaz:hoctapaz@localhost:5433/hoctapaz_platform?sslmode=disable' \
GOTOOLCHAIN=go1.25.11 go test ./services/wallet-service/internal/repository \
-run TestPostgresWalletLedgerReplayIntegration -count=1 -v