Skip to content

[BUG]: Voided invoices and terminated wallets remain mutable after terminal state transitions #5869

Description

@avisprince

Describe the bug
Several resources can be modified after reaching a terminal state (voided or terminated) that should make them immutable. The API accepts mutations that change payment status, add metadata, and update fields on resources that are supposed to be frozen.

  1. Payment status changeable on voided invoices — After voiding an invoice, PUT /api/v1/invoices/{id} with payment_status: "succeeded" returns 200. The same works with "failed". This triggers webhook delivery (invoice.payment_status_updated) on a cancelled invoice. Note: void-again and retry-payment are correctly blocked with 405.

  2. Terminated wallets can be updated — After terminating a wallet via DELETE, PUT /api/v1/wallets/{id} with name, priority, or expiration_at changes all return 200. Note: topping up a terminated wallet with paid_credits or granted_credits correctly returns 422.

  3. Metadata writable on voided invoices — Adding metadata to a voided invoice via PUT returns 200. No state check on metadata updates.

To Reproduce
Environment:

  • Lago API (getlago/api:v1.48.1)
  • PostgreSQL 15 with pg_partman
  • Redis
  • Env vars: LAGO_DISABLE_SEGMENT=true, LAGO_DISABLE_WALLET_REFRESH=true

Setup:

Organization with API key, a customer, and an add-on (for one-off invoices).

Control — voided invoice blocks void-again and retry:

Create a one-off invoice and void it:

POST /api/v1/invoices/{invoice_id}/void
Authorization: Bearer {api_key}

Returns 200. Then:

POST /api/v1/invoices/{invoice_id}/void
Authorization: Bearer {api_key}

Returns 405. Correctly rejects double-void.

POST /api/v1/invoices/{invoice_id}/retry_payment
Authorization: Bearer {api_key}

Returns 405. Correctly rejects retry on voided invoice.

Bug 1 — payment status updated on voided invoice:

PUT /api/v1/invoices/{invoice_id}
Authorization: Bearer {api_key}
Content-Type: application/json

{
  "invoice": {
    "payment_status": "succeeded"
  }
}

Returns 200. The invoice is now voided with payment_status: succeeded.

Bug 2 — terminated wallet updated:

Create and terminate a wallet:

DELETE /api/v1/wallets/{wallet_id}
Authorization: Bearer {api_key}

Returns 200. Then:

PUT /api/v1/wallets/{wallet_id}
Authorization: Bearer {api_key}
Content-Type: application/json

{
  "wallet": {
    "name": "Zombie Wallet Name"
  }
}

Returns 200. Also works for priority and expiration_at.

Control — terminated wallet balance is correctly protected:

POST /api/v1/wallet_transactions
Authorization: Bearer {api_key}
Content-Type: application/json

{
  "wallet_transaction": {
    "wallet_id": "{wallet_id}",
    "paid_credits": "50.0"
  }
}

Returns 422. Correctly rejects top-up on terminated wallet.

Bug 3 — metadata added to voided invoice:

PUT /api/v1/invoices/{invoice_id}
Authorization: Bearer {api_key}
Content-Type: application/json

{
  "invoice": {
    "metadata": [
      {
        "key": "ghost-key",
        "value": "ghost-value"
      }
    ]
  }
}

Returns 200.

Reproducing with Dokkimi:

Dokkimi is an open source testing tool that stands up isolated Docker environments from declarative YAML definitions — services, databases, seed data, test steps, and assertions all in one file. The definition for this bug is in dokkimi/dokkimi-in-the-wild/.dokkimi/lagodokkimi run reproduces it from scratch.

Expected behavior

  • Voided invoices should reject all mutations via the update endpoint — payment status, metadata, and any other field. The 405 enforcement on void-again and retry-payment should extend to the PUT endpoint.
  • Terminated wallets should reject all mutations. The 422 enforcement on balance top-ups should extend to name, priority, and expiration updates.

Screenshots
N/A

Additional context

  • A voided invoice marked as succeeded triggers webhook delivery and payment processing jobs, potentially causing reconciliation errors
  • Terminated wallets with mutable priority can affect wallet selection logic for customers with multiple wallets
  • Metadata added to voided invoices modifies the audit trail of a cancelled document, which may violate compliance requirements for immutable financial records

Version

  • Lago: v1.48.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions