PRD: Invoice adjustment & cancellation flows
| Module | Tax & Invoice (CORE-10) | PRD ID | PRD-INV-003 |
| Status | Shipped | Owner | Tax & Invoice squad |
| Date | 2026-06-15 | Version | v1.0 |
| Packages | @nx/invoice · @nx/core · @nx/iiapi | URD | INV |
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 -
replacementis 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
| Metric | Target / signal |
|---|---|
| Correction linkage | Every correction invoice references its original and the original's issuance; none orphaned |
| No double issuance | A retried or duplicated adjustment job issues the correction at most once |
| Cancellation safety | Zero cancellations applied to an invoice while it is being processed |
| Idempotency | Re-cancelling an already-cancelled invoice changes nothing and errors nothing |
| Audit completeness | Every adjustment and cancellation has a matching immutable audit-trail entry |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner / Manager | Correct or cancel an issued invoice legally, with a reason, when a mistake is found |
| Cashier | Trigger 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 |
| Buyer | Receive 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
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Only an issued (SUCCESS) invoice can be adjusted or cancelled; the correction always links back to the original | URD-INV-010 |
| FR-2 | An adjustment creates a new correction invoice (adjustment origin) linked to the original and is processed asynchronously via the issuance queue | URD-INV-011 · URD-INV-007 |
| FR-3 | The correction carries its own line items, description, and totals, and is issued through the provider referencing the original issuance | URD-INV-012 |
| FR-4 | Adjustment is supported for VAT, POS-VAT, and ticket-VAT invoice types; an unsupported type is refused | URD-INV-013 |
| FR-5 | The async worker claims a pending adjustment under an optimistic lock so a retried or duplicated job issues the correction at most once | URD-INV-011 |
| FR-6 | Cancellation marks the invoice cancelled with a required reason; for an issued invoice it also calls the provider cancellation | URD-INV-014 · URD-INV-008 |
| FR-7 | Cancellation is idempotent (re-cancelling is a no-op) and is blocked while the invoice is being processed | URD-INV-015 |
| FR-8 | Every adjustment and cancellation writes an immutable audit entry and notifies connected clients in real time | URD-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
| Area | Requirement |
|---|---|
| Reliability | Adjustment 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 |
| Immutability | The original is never edited; correction and cancellation are new records / new audit entries, append-only |
| Linkage integrity | A correction stores its original as parent and issues with the original issuance's reference id; the cancellation reason is persisted on the invoice |
| State safety | Cancellation refuses an invoice in processing and short-circuits one already cancelled; adjustment refuses an original not in success |
| Tenancy & authz | Adjustment 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-time | A correction or cancellation outcome is pushed to connected clients over the live channel |
| i18n | User-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
| Entity | Role in these flows |
|---|---|
Invoice | Both 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 |
InvoiceIssuance | The original's issuance record whose reference id the correction quotes so provider and tax authority link them |
InvoiceAuditTracing | The immutable per-event entry written for the correction's lifecycle and for the cancellation |
Invoice origin is one of
origin,adjustment, orreplacement; this increment usesorigin(the source invoice) andadjustment(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 / question | Mitigation / status |
|---|---|
| Duplicate / retried adjustment job double-issues a correction | Worker claims the pending correction under an optimistic pending → processing transition; a second worker skips |
| Cancellation races an in-flight issuance | Cancellation refuses an invoice in processing; the adjustment worker also skips a correction cancelled while queued |
| Repeated cancellation requests | Idempotent - an already-cancelled invoice short-circuits with no error |
| Correction not recognized as linked by the tax authority | The correction quotes the original issuance reference so provider and CQT tie them together |
| Adjusting an unsupported invoice type | Only VAT / POS-VAT / ticket-VAT have a provider correction path; other types are refused |
| Replacement invoices | replacement origin is modeled but issuing a replacement is out of scope for this increment |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (foundation) - part of INV in the URD feature catalog |
| Rollout | All merchants with an active invoice profile; gated by invoice adjust / cancel permissions |
| Migration | None - correction reuses the existing invoice entity (adjustment origin + parent link) and the issuance queue |
| Launch criteria | Adjusting 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 |
| Monitoring | Adjustment 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
- URD: Tax & Invoice - Invoice Lifecycle
- Related PRD: Invoice lifecycle & issuance · E-invoice provider integration · Invoice issuance experience
- Module: Tax & Invoice - overview + traceability
- Developer: @nx/invoice · @nx/core · iiapi