Busflow Docs

Internal documentation portal

Skip to content

Storno Workflow — Invoice Cancellation Service

NestJS Domain Service implementing GoBD-compliant invoice cancellation (Storno) and optional reissuance. Part of the Commerce Bounded Context.

Purpose

GoBD prohibits deleting or silently editing issued invoices. The InvoiceCancellationService enforces a formal cancel → counter-document → optional reissue workflow.

CAUTION

GoBD-Pflicht: VOIDED bedeutet nicht, dass die ursprüngliche Invoice-Zeile modifiziert wird. Ein Storno erfordert zwingend die Erstellung eines neuen, gegenläufigen Buchungsdokuments (Stornorechnung). Das Ursprungsdokument bleibt unverändert im System.

Lifecycle

mermaid
stateDiagram-v2
    [*] --> ISSUED
    ISSUED --> CANCELLED: cancelInvoice()
    note right of CANCELLED: Original invoice remains\nunchanged. A new\nStornorechnung is created.
    CANCELLED --> [*]: No replacement needed
    CANCELLED --> DRAFT: reissueInvoice()
    DRAFT --> ISSUED: New corrected invoice issued

API Surface (Hasura Actions)

cancelInvoice

graphql
mutation CancelInvoice($invoiceId: uuid!, $reason: String!) {
  cancelInvoice(invoice_id: $invoiceId, reason: $reason) {
    cancellation_id
    storno_invoice_id
  }
}

Side effects:

  1. Does NOT modify the original invoices row — it remains in ISSUED status with a cancelled: true flag
  2. Creates a new Stornorechnung (counter-invoice) with negative amounts mirroring the original
  3. Creates invoice_cancellations record linking cancelled_invoice_idstorno_invoice_id with reason and timestamp
  4. Writes commerce.change_events entries via AuditTrailService (scope: GOBD, entity_type: 'invoice', entity_id set to both original and storno invoices) — see ADR-019

reissueInvoice

graphql
mutation ReissueInvoice($cancellationId: uuid!) {
  reissueInvoice(cancellation_id: $cancellationId) {
    new_invoice_id
  }
}

Side effects:

  1. Creates a new invoices record with status DRAFT and corrected amounts
  2. Links via invoice_cancellations.replacement_invoice_id

Validation Guards

GuardCheckError
Period LockPeriodLockService.validateMutation(tenantId, invoice.issue_date)PeriodLockedException
Statusinvoice.status must be 'ISSUED'InvalidInvoiceStatusException
Duplicate StornoNo existing cancellation for this invoice_idAlreadyCancelledException

Audit Trail

Every cancelInvoice and reissueInvoice call writes to commerce.change_events via the shared AuditTrailService (ADR-019) with:

  • scope: 'GOBD'
  • entity_type: 'invoice', entity_id pointing to the affected invoice
  • old_values captured via SELECT ... FOR UPDATE, new_values capturing the status change

Internal documentation — Busflow