fullstackhero/dotnet-starter-kit · error · CustomException
Cannot credit a top-up to a wallet.
Error message
Cannot credit a {invoice.Currency} top-up to a {wallet.Currency} wallet. What it means
When a top-up invoice is marked paid, the service credits the tenant's wallet in the invoice's currency. If the tenant already has a wallet in a different currency, crediting would mix currencies, so CustomException with HTTP 409 Conflict is thrown.
Solutions
- Issue top-ups in the same currency as the tenant's existing wallet, or create the top-up after checking wallet.Currency.
- If a different currency is genuinely needed, support multi-wallet-per-tenant or implement explicit FX conversion before crediting.
- Reject/void the mismatched invoice and re-issue it in the wallet's currency.
- Validate currency at top-up request creation time so the mismatch never reaches payment.
Example fix
// before var invoice = Invoice.CreateTopupDraft(tenantId, "EUR", ...); // wallet is USD // after var wallet = await db.Wallets.SingleAsync(w => w.TenantId == tenantId); var invoice = Invoice.CreateTopupDraft(tenantId, wallet.Currency, ...);
Defensive patterns
Strategy: validation
Validate before calling
var wallet = await db.Wallets.SingleOrDefaultAsync(w => w.TenantId == tenantId);
if (wallet is not null && wallet.Currency != invoiceCurrency)
throw new CustomException("Currency mismatch", null, HttpStatusCode.Conflict); Type guard
bool currencyMatches(Wallet? w, string currency) => w is null || string.Equals(w.Currency, currency, StringComparison.Ordinal);
Try / catch
try { await billing.MarkInvoicePaidAsync(invoiceId, ct); }
catch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict && ex.Message.Contains("wallet")) { /* re-issue invoice in wallet currency */ } Prevention
- Create top-up requests in the wallet's currency; validate at request creation.
- Support multi-wallets or explicit FX conversion for multi-currency tenants.
- Never change a tenant's default currency without a migration plan.
When it happens
Trigger: Paying a top-up invoice whose Currency differs from the tenant's existing wallet.Currency — e.g. tenant opened a USD wallet, then an EUR top-up is paid; a plan/config change altered the top-up currency.
Common situations: Multi-currency tenants issuing top-ups in a currency other than their wallet; configuration change of default currency mid-life; importing invoices from another environment with different currencies.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Top-up request cannot be rejected because it is (only…
- Cannot mark invoice as paid from status
- Paid invoices cannot be voided.
- Operation requires invoice status
- Top-up request must be
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/15ddd7216386076b.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Services/BillingService.cs:261
{
var topupRequest = await _db.TopupRequests
.FirstOrDefaultAsync(r => r.InvoiceId == invoice.Id, cancellationToken)
.ConfigureAwait(false);
if (topupRequest is { Status: TopupRequestStatus.Invoiced })
{
var wallet = await _db.Wallets
.FirstOrDefaultAsync(w => w.TenantId == invoice.TenantId, cancellationToken)
.ConfigureAwait(false);
if (wallet is null)
{
wallet = Wallet.Create(invoice.TenantId, invoice.Currency);
_db.Wallets.Add(wallet);
}
else if (!string.Equals(wallet.Currency, invoice.Currency, StringComparison.Ordinal))
{
throw new CustomException(
$"Cannot credit a {invoice.Currency} top-up to a {wallet.Currency} wallet.",
errors: null,
HttpStatusCode.Conflict);
}
wallet.Credit(
invoice.SubtotalAmount,
WalletTransactionKind.Topup,
"WhatsApp wallet top-up",
topupRequest.Id.ToString());
topupRequest.MarkCompleted();
}
}
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
View on GitHub (pinned to 3f2959e683)