fullstackhero/dotnet-starter-kit · error · InvalidOperationException
Paid invoices cannot be voided.
Error message
Paid invoices cannot be voided.
What it means
Invoice.Void throws InvalidOperationException when the invoice is already Paid, because a paid invoice represents collected money and must not be voided. Re-voiding an already-Void invoice is allowed and idempotent.
Solutions
- Check invoice.Status != InvoiceStatus.Paid before calling Void().
- For paid invoices that must be cancelled, implement a refund/credit-note flow instead of voiding.
- Disable the Void action in the UI for paid invoices.
- Handle the InvalidOperationException as a user-facing 'cannot cancel paid invoice' message.
Example fix
// before
invoice.Void(reason); // throws if paid
// after
if (invoice.Status != InvoiceStatus.Paid)
{
invoice.Void(reason);
} Defensive patterns
Strategy: validation
Validate before calling
if (invoice.Status is InvoiceStatus.Paid or InvoiceStatus.Void) return; // nothing to void / not voidable
Try / catch
try { invoice.Void(reason); } catch (InvalidOperationException) { // route to refund/credit-note flow for paid invoices } Prevention
- Disable Void buttons for paid invoices
- Treat paid-invoice cancellation as a refund workflow, not a void
- Serialize cancellation and payment jobs per invoice
When it happens
Trigger: Calling Void() on an invoice whose Status is InvoiceStatus.Paid.
Common situations: Cancellation flows that unconditionally void invoices after payment succeeded; race between a payment job and a cancellation job; UI not disabling Void for paid invoices.
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
- Cannot mark invoice as paid from status
- 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/842ffc749b3a34d3.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Domain/Invoice.cs:150
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;
if (!string.IsNullOrWhiteSpace(reason))
{
Notes = string.IsNullOrWhiteSpace(Notes) ? reason : $"{Notes}; Voided: {reason}";
}
}
public void SetNotes(string? notes)
{
Notes = notes;
}View on GitHub (pinned to 3f2959e683)