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

  1. Issue top-ups in the same currency as the tenant's existing wallet, or create the top-up after checking wallet.Currency.
  2. If a different currency is genuinely needed, support multi-wallet-per-tenant or implement explicit FX conversion before crediting.
  3. Reject/void the mismatched invoice and re-issue it in the wallet's currency.
  4. 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

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


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)