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

  1. Check invoice.Status != InvoiceStatus.Paid before calling Void().
  2. For paid invoices that must be cancelled, implement a refund/credit-note flow instead of voiding.
  3. Disable the Void action in the UI for paid invoices.
  4. 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

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


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)