fullstackhero/dotnet-starter-kit · error · InvalidOperationException
Cannot mark invoice as paid from status
Error message
Cannot mark invoice as paid from status {Status}. What it means
Invoice.MarkPaid enforces the state machine: an invoice can only transition to Paid from Issued. Calling MarkPaid when Status is Draft, Void, or any other non-Issued state throws InvalidOperationException. Marking an already-Paid invoice is silently idempotent.
Solutions
- Call Issue() before MarkPaid so the invoice reaches Issued state first.
- Guard with a status check before invoking MarkPaid.
- If voided, do not mark paid — issue a corrected invoice instead.
- Reload the invoice from the database to get its current status before transitioning.
Example fix
// before
invoice.MarkPaid(); // throws from Draft
// after
invoice.AddLineItem(...);
invoice.Issue();
if (invoice.Status == InvoiceStatus.Issued)
{
invoice.MarkPaid();
} Defensive patterns
Strategy: validation
Validate before calling
if (invoice.Status is not InvoiceStatus.Issued and not InvoiceStatus.Paid) throw new InvalidOperationException($"Cannot mark paid from {invoice.Status}"); Try / catch
try { invoice.MarkPaid(); } catch (InvalidOperationException ex) { logger.LogWarning(ex, "Invalid invoice transition"); } Prevention
- Always Issue() before MarkPaid()
- Re-read the entity from the DB before state transitions in long-running flows
- Make payment handlers idempotent (skip when already Paid)
When it happens
Trigger: Calling MarkPaid on a Draft invoice (never issued); on a Voided invoice; or after a concurrent handler already moved it past Issued.
Common situations: Payment webhooks firing for invoices that were voided between order and payment; batch payment jobs processing stale invoice snapshots; tests reusing a saved entity instance.
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
- Paid invoices cannot be voided.
- Operation requires invoice status
- Top-up request must be
- Insufficient wallet balance.
- Top-up request cannot be rejected because it is (only…
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/dbe3b8bc1dac520d.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Domain/Invoice.cs:140
public void Issue(DateTime? dueAtUtc = null)
{
RequireStatus(InvoiceStatus.Draft);
Status = InvoiceStatus.Issued;
IssuedAtUtc = DateTime.UtcNow;
DueAtUtc = dueAtUtc is null
? IssuedAtUtc.Value.AddDays(14)
: DateTime.SpecifyKind(dueAtUtc.Value, DateTimeKind.Utc);
}
public void MarkPaid()
{
if (Status is InvoiceStatus.Paid)
{
return;
}
if (Status is not InvoiceStatus.Issued)
{
throw new InvalidOperationException($"Cannot mark invoice as paid from status {Status}.");
}
Status = InvoiceStatus.Paid;
PaidAtUtc = DateTime.UtcNow;
}
public void Void(string? reason = null)
{
if (Status is InvoiceStatus.Paid)
{
throw new InvalidOperationException("Paid invoices cannot be voided.");
}
if (Status is InvoiceStatus.Void)
{
// Idempotent: re-voiding must not re-stamp VoidedAtUtc or append the reason again.
return;
}
Status = InvoiceStatus.Void;
VoidedAtUtc = DateTime.UtcNow;View on GitHub (pinned to 3f2959e683)