Skip to content

PRD: Invoice adjustment & cancellation flows

ModuleTax & Invoice (CORE-10)PRD IDPRD-INV-003
StatusShippedOwnerTax & Invoice squad
Date2026-06-15Versionv1.0
Packages@nx/invoice · @nx/core · @nx/iiapiURDINV

TL;DR

Once an e-invoice is issued, it cannot be edited in place - Vietnamese law requires a correction or a cancellation, each carrying a reason and linked to the original. This increment makes both first-class. An adjustment creates a new correction invoice that points back to the original and is issued asynchronously through the provider, referencing the original issuance so the tax authority ties them together. A cancellation marks an invoice cancelled with a required reason - and for an already-issued invoice it also calls the provider to cancel it - guarded so it can never collide with an in-flight issuance and idempotent so a repeated request is harmless. Every adjustment and cancellation writes an immutable audit entry and pushes a live update to connected clients.

1. Context & Problem

The invoice lifecycle & issuance increment (PRD-INV-001) turns a paid order into an issued, numbered e-invoice and records every event immutably. It establishes that an issued invoice can be adjusted, replaced, or cancelled with a reason - but it specifies that only as a single line. The mechanics of how a correction is created, issued, and tied to its original, and how a cancellation stays safe against the asynchronous issuance pipeline, were left for this increment.

That gap matters because correction and cancellation are exactly where compliance is easy to get wrong. A correction must reference the original issuance so the provider and the tax authority link them; it must not be issued twice if a job is retried; and it must not be attempted against an invoice that was never successfully issued. A cancellation must never run while a worker is mid-issuance, must be repeatable without double-cancelling, and - for an issued invoice - must reach the provider, not just flip a local flag. This PRD specifies those flows on top of the existing lifecycle.

2. Goals & Non-Goals

Goals

  • Adjust an issued (SUCCESS) invoice by creating a new correction invoice linked to the original, processed asynchronously through the issuance queue.
  • Issue the correction through the provider referencing the original issuance, so original and correction are tied at the provider / tax authority.
  • Support adjustment for VAT, POS-VAT, and ticket-VAT invoice types; refuse types that have no provider correction path.
  • Cancel an invoice with a required reason; for an issued invoice, also call the provider cancellation.
  • Make cancellation idempotent and blocked while the invoice is processing, so it can never race the issuance worker.
  • Record an immutable audit entry for every adjustment and cancellation event and push a real-time update to connected clients.

Non-Goals

  • The base issuance engine, retry policy, tax-authority submission, and webhook handling - specified in PRD-INV-001.
  • Replacement invoices - replacement is a recognized invoice origin in the data model, but issuing a replacement is not delivered in this increment.
  • The owner / cashier UI for choosing and reviewing a correction - the management experience is tracked under ISS (PRD-ISS-001).
  • Recomputing line items, taxes, or totals for the correction - the caller supplies the corrected figures; this flow carries and issues them.
  • PDF rendering of the correction or cancellation notice - produced provider-side.

3. Success Metrics

MetricTarget / signal
Correction linkageEvery correction invoice references its original and the original's issuance; none orphaned
No double issuanceA retried or duplicated adjustment job issues the correction at most once
Cancellation safetyZero cancellations applied to an invoice while it is being processed
IdempotencyRe-cancelling an already-cancelled invoice changes nothing and errors nothing
Audit completenessEvery adjustment and cancellation has a matching immutable audit-trail entry

4. Personas & Use Cases

PersonaGoal in this feature
Owner / ManagerCorrect or cancel an issued invoice legally, with a reason, when a mistake is found
CashierTrigger a correction for an invoice issued at the counter without re-creating the sale
Accountant (downstream)Rely on original ↔ correction linkage and the audit trail for tax reporting
BuyerReceive a corrected / cancelled invoice that the tax authority recognizes as linked to the original

Core scenarios: an owner finds a wrong price on an issued invoice → requests an adjustment with the corrected items and a description → a correction invoice is created linked to the original and queued → a worker issues it through the provider referencing the original issuance → its status turns to success and clients update live. Or: an owner cancels an issued invoice with a reason → the provider cancellation is called → the invoice is marked cancelled and the reason recorded. Cancelling an invoice that is still processing is refused; cancelling one that is already cancelled does nothing.

5. User Stories

  • As an owner, I want to correct an issued invoice by issuing a linked adjustment, so the record is fixed without editing the original.
  • As an owner, I want the correction tied to the original at the provider, so the tax authority sees them as one chain.
  • As an owner, I want adjustments to run in the background and not double-issue if a job retries, so corrections are reliable.
  • As an owner, I want to cancel an issued invoice with a reason and have the provider cancellation actually happen, so the cancellation is legally real.
  • As an owner, I want cancellation refused while an invoice is still being issued, so I never corrupt an in-flight invoice.
  • As an owner, I want re-submitting a cancellation to be harmless, so retries or double-clicks don't cause errors.
  • As an owner, I want every correction and cancellation recorded immutably and shown live, so the invoice history is always verifiable.

6. Functional Requirements

#RequirementURD ref
FR-1Only an issued (SUCCESS) invoice can be adjusted or cancelled; the correction always links back to the originalURD-INV-010
FR-2An adjustment creates a new correction invoice (adjustment origin) linked to the original and is processed asynchronously via the issuance queueURD-INV-011 · URD-INV-007
FR-3The correction carries its own line items, description, and totals, and is issued through the provider referencing the original issuanceURD-INV-012
FR-4Adjustment is supported for VAT, POS-VAT, and ticket-VAT invoice types; an unsupported type is refusedURD-INV-013
FR-5The async worker claims a pending adjustment under an optimistic lock so a retried or duplicated job issues the correction at most onceURD-INV-011
FR-6Cancellation marks the invoice cancelled with a required reason; for an issued invoice it also calls the provider cancellationURD-INV-014 · URD-INV-008
FR-7Cancellation is idempotent (re-cancelling is a no-op) and is blocked while the invoice is being processedURD-INV-015
FR-8Every adjustment and cancellation writes an immutable audit entry and notifies connected clients in real timeURD-INV-016 · URD-INV-006

Full requirement text and acceptance criteria live in the Tax & Invoice URD - INV. This PRD references them rather than restating them. The base lifecycle requirements (URD-INV-001..009) are specified in PRD-INV-001.

7. Non-Functional Requirements

AreaRequirement
ReliabilityAdjustment runs through the issuance queue with idempotent processing; the worker claims a pending correction under an optimistic transition (pending → processing) so a replayed job cannot double-issue
ImmutabilityThe original is never edited; correction and cancellation are new records / new audit entries, append-only
Linkage integrityA correction stores its original as parent and issues with the original issuance's reference id; the cancellation reason is persisted on the invoice
State safetyCancellation refuses an invoice in processing and short-circuits one already cancelled; adjustment refuses an original not in success
Tenancy & authzAdjustment and cancellation are scoped per merchant and gated by dedicated invoice adjust / cancel permissions; access to the target invoice is asserted before any action
Real-timeA correction or cancellation outcome is pushed to connected clients over the live channel
i18nUser-facing labels / statuses are bilingual ({ en, vi })

8. UX & Flows

Adjustment - asynchronous correction

Cancellation - state-guarded, idempotent

The owner triggers adjustment or cancellation from an issued invoice; adjustment returns immediately while the correction is issued in the background, and cancellation completes synchronously. The management surface that presents these actions and their results is tracked under ISS.

9. Data & Domain

EntityRole in these flows
InvoiceBoth the original and the correction; a correction carries an adjustment origin and a parent link to the original, plus its corrected items / totals and (for cancellation) the reason
InvoiceIssuanceThe original's issuance record whose reference id the correction quotes so provider and tax authority link them
InvoiceAuditTracingThe immutable per-event entry written for the correction's lifecycle and for the cancellation

Invoice origin is one of origin, adjustment, or replacement; this increment uses origin (the source invoice) and adjustment (the correction). Conceptual only - full schema and invariants live in the invoice domain model.

10. Dependencies & Assumptions

Depends on

  • Invoice lifecycle & issuance (PRD-INV-001, URD-INV-001..009) - the issuance engine, audit trail, and the original invoice + its issuance record.
  • Invoice configuration (URD-CFG) - the original's provider config is resolved to issue the correction and to call provider cancellation.
  • Provider gateway (@nx/iiapi) - the adapter that issues the correction and performs provider-side cancellation per invoice type.
  • @nx/core - invoice origin / status constants and the shared invoice model.

Assumptions

  • The original invoice was issued successfully and retains its issuance reference and provider config mapping.
  • The caller supplies the corrected line items, description, and totals; this flow does not recompute them.
  • Connected clients listen on the live channel for invoice updates.

11. Risks & Open Questions

Risk / questionMitigation / status
Duplicate / retried adjustment job double-issues a correctionWorker claims the pending correction under an optimistic pending → processing transition; a second worker skips
Cancellation races an in-flight issuanceCancellation refuses an invoice in processing; the adjustment worker also skips a correction cancelled while queued
Repeated cancellation requestsIdempotent - an already-cancelled invoice short-circuits with no error
Correction not recognized as linked by the tax authorityThe correction quotes the original issuance reference so provider and CQT tie them together
Adjusting an unsupported invoice typeOnly VAT / POS-VAT / ticket-VAT have a provider correction path; other types are refused
Replacement invoicesreplacement origin is modeled but issuing a replacement is out of scope for this increment

12. Release Plan & Launch Criteria

AspectPlan
PhaseP1 (foundation) - part of INV in the URD feature catalog
RolloutAll merchants with an active invoice profile; gated by invoice adjust / cancel permissions
MigrationNone - correction reuses the existing invoice entity (adjustment origin + parent link) and the issuance queue
Launch criteriaAdjusting an issued invoice creates a linked correction, queues it, and issues it via the provider referencing the original; a retried job does not double-issue; cancelling an issued invoice calls the provider and records status + reason; cancellation is refused while processing and is a no-op when already cancelled; every action writes an audit entry and notifies clients
MonitoringAdjustment success / failure rate, double-claim skips, cancellation outcomes (provider vs local), audit-entry coverage

13. FAQ

Can an issued invoice be edited? No. It is corrected by issuing a linked adjustment invoice, or cancelled with a reason. The original is never changed.

What ties a correction to its original? The correction stores the original as its parent and is issued quoting the original's issuance reference, so the provider and the tax authority treat them as one chain.

What if the adjustment job runs twice? It can't double-issue. A worker claims the pending correction under an optimistic lock; any second attempt sees it is no longer pending and skips.

Which invoices can I adjust? Only ones that were issued successfully (SUCCESS), and only VAT, POS-VAT, or ticket-VAT types - other types have no provider correction path and are refused.

Can I cancel an invoice that is still being issued? No - cancellation is refused while the invoice is processing. Wait for issuance to finish, then cancel.

Is cancelling twice a problem? No - cancellation is idempotent. A second request on an already-cancelled invoice does nothing and returns cleanly.

Does cancelling an issued invoice reach the provider? Yes - for an issued (SUCCESS) invoice the provider cancellation is called before the invoice is marked cancelled locally and the reason recorded.

References

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.