feat(billing): WhatsApp prepaid wallet + top-up requests (Phase 1)#1315
Merged
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ique index Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GetOrCreateWalletAsync, CreateTopupInvoiceAsync to IBillingService/BillingService.
MarkInvoicePaidAsync now credits the tenant wallet and completes the TopupRequest in the
same unit of work when Purpose == Topup. Invoice.CreateTopupDraft convenience factory
added. Invoice number scheme: TOP-{yyyyMM}-{tenantToken}-{requestSuffix} ensures
collision-safety for multiple top-ups in the same month.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…llet ledger Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the three dashboard-facing wallet endpoints under api/v1/billing: GET /wallet/me, POST /wallet/topup-requests, GET /wallet/topup-requests/me. Handlers root-gate to caller tenant via IMultiTenantContextAccessor; BillingDbContext is not EF-filtered so every handler explicitly filters by callerTenantId. Validators enforce Amount bounds and pagination invariants. Integration tests confirm happy-path and cross-tenant isolation (Tenant B cannot see Tenant A's requests). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds three admin-facing endpoints under api/v1/billing/wallet/topup-requests:
- GET (root cross-tenant list, non-root scoped to own tenant)
- POST /{id}/approve (creates+issues Topup invoice via IBillingService)
- POST /{id}/reject (transitions request to Rejected)
Root-gating mirrors GetInvoicesQueryHandler. Approve/reject require
BillingPermissions.Manage and use .WithIdempotency(). All handlers are
public sealed with ValueTask<T> and ConfigureAwait(false). Three validators
added (Architecture.Tests requires validator per command/paginated-query).
Architecture.Tests extended with Approve/Reject as valid endpoint verb prefixes.
4 integration tests (approve happy-path, reject happy-path, cross-tenant isolation,
root tenantId filter) + 12 unit validator tests — all green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add an explicit Pending-state guard in RejectTopupRequestCommandHandler that throws CustomException(HttpStatusCode.Conflict) before delegating to the domain Reject() method. Without the guard the domain's InvalidOperationException fell through to the global 500 branch. Add integration test Reject_of_already_rejected_request_returns_409_Conflict to TopupApprovalTests that asserts a second reject on the same request returns 409 Conflict. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Top-up wallet credit on invoice-paid was exactly-once only via an in-process status check; two concurrent MarkInvoicePaid calls on the same top-up invoice could both credit and commit. Add a unique partial index on WalletTransactions(ReferenceId) filtered to Kind=Topup so a second ledger row for the same top-up request cannot be inserted -- the concurrent second transaction violates the constraint and rolls back, leaving exactly one credit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds the funding half of a prepaid WhatsApp messaging wallet (Phase 1). We act as the WhatsApp BSP (Meta bills us; clinics top up a prepaid money wallet with us).
Lifecycle: clinic submits a top-up request (dashboard) → operator sees it (admin → Billing → Top-ups) → Approve generates + issues an invoice (clinic emailed) → clinic pays offline → operator marks the invoice Paid → wallet is auto-credited via an append-only ledger and the request is Completed. Operators can also Reject a pending request.
Backend (Billing module)
Wallet(1/tenant),WalletTransaction(append-only signed ledger; balance = Σ ledger),TopupRequest(state machine Pending→Invoiced→Completed / Pending→Rejected).InvoicePurpose.Topup; the invoice period-unique index is filtered to exclude Topup so multiple top-ups per month are allowed.ReferenceIdfor Topup rows.GET /wallet/me,POST /wallet/topup-requests,GET /wallet/topup-requests/me. Operator endpoints:GET /wallet/topup-requests(root cross-tenant),POST .../{id}/approve,POST .../{id}/reject. Permissions:Billing.Viewto view/request,Billing.Manageto approve/reject.BillingDbContextisn't tenant-filtered); one EF migration.Frontend
Tests
Notes
Debit/MessageChargepaths exist but are unused.docs/whatsapp-wallet-topupin the separate docs repo (not yet pushed).🤖 Generated with Claude Code