PRD: Order split, merge & rollback
| Module | Sale (CORE-07) | PRD ID | PRD-ORD-002 |
| Status | Shipped | Owner | Sale squad |
| Date | 2026-06-15 | Version | v1.0 |
| Packages | @nx/sale · @nx/core | URD | ORD |
TL;DR
Lets a cashier restructure a table's draft orders without rebuilding them by hand. Split carves one draft into several new drafts, allocating chosen items and quantities (and an optional name / customer) to each; a line whose full quantity is taken moves whole, a partial take splits the line and leaves the remainder behind. Merge folds several drafts into one and cancels the sources. Rollback undoes a merge - every item walks back to the exact order it came from. Combos always travel as one unit, an append-only transfer-history lineage stamps every move, the reserved stock travelling with the items, and each operation is one all-or-nothing transaction that re-prices the orders it touches.
1. Context & Problem
A draft order (ORD) already plays both cart and committed-order roles, but a real service counter constantly reshapes drafts: two friends who sat together now want separate bills; three tabs opened by mistake should be one; a merge done in haste must be undone. The order lifecycle alone offers no way to move items between drafts - a cashier would have to cancel and re-key, losing prices, customer links, and the stock already reserved.
Without a first-class restructure path, the counter either refuses these everyday requests or rebuilds orders manually, which drops reserved inventory, mis-prices combos, and leaves no trace of what moved where. This increment closes that gap on top of the existing draft order: declarative split / merge / rollback operations that move items (and their reserved stock) atomically, keep combos intact, and record a reversible lineage so a merge can always be walked back.
2. Goals & Non-Goals
Goals
- Split a draft into N new drafts by allocating items and quantities, with full-quantity moves and partial-quantity line splits.
- Merge one or more drafts into a target draft and cancel the emptied sources.
- Rollback a merge so every item returns to its original source order, restored to draft.
- Keep combos atomic - a lead and all its children always move together with full quantity.
- Stamp an append-only transfer-history lineage on every moved item so the path is auditable and reversible.
- Carry reserved stock (allocation usages) with the items and re-price every affected order, all inside one transaction.
Non-Goals
- Bill splitting into independently-payable checks - that is
CHK(PRD-CHK-001); this PRD reshapes orders, not payment checks. - Splitting or merging non-draft orders - restructure is DRAFT-only (C-02).
- Restructuring an order that has active checks - blocked until the checks are rolled back.
- Payment, refund, or stock-mutation logic - owned by Payment and Inventory.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Reshape without re-key | A cashier splits, merges, or rolls back from one action - no cancel-and-re-enter |
| Atomicity | A failed sub-step leaves every participating order exactly as it was |
| Combo integrity | No split ever orphans a combo lead or child; combo members never partially move |
| Reversibility | Any merge can be rolled back to the original orders using only the recorded lineage |
| Stock continuity | Reserved stock follows the items - no double-reservation, no lost reservation |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Cashier | Split a shared tab into separate bills, or combine mistaken tabs, in one step |
| Manager | Undo a wrong merge cleanly, with the original orders restored intact |
| Owner | Trust that every reshape keeps prices, combos, and reserved stock correct |
Core scenario: a table of four opened one draft but wants two bills. The cashier splits the draft - assigning two mains and a shared starter (full quantity) to bill A and the rest to bill B - and the system creates two new drafts, moves the lines (splitting the shared-by-quantity ones), carries the reserved stock, re-prices both, and cancels the now-empty original. Later they realise it should have been one bill, merge B back into A, and - spotting a third error - roll the merge back so B is restored exactly as it was.
5. User Stories
- As a cashier, I split a draft into new drafts by choosing items and quantities per bill, so a shared tab becomes separate bills in one step.
- As a cashier, I take a line's full quantity or just part of it, so I can move a whole dish or just two of five.
- As a cashier, I merge several drafts into one, so mistaken separate tabs become a single bill and the others close.
- As a manager, I roll a merge back, so a wrong combine is undone and every item returns to its original order.
- As an owner, I want combos to move as a unit and reserved stock to travel with the items, so reshaping never corrupts a bundle or its stock.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Split a DRAFT order into N new DRAFT orders, each allocated specific items and quantities, with an optional name and customer per new order | URD-ORD-012 · URD-ORD-017 |
| FR-2 | A line whose full quantity is assigned moves whole; a partial assignment splits the line, leaving the remaining quantity on the source | URD-ORD-017 |
| FR-3 | A combo's lead and all its children must move together, at full quantity, into the same target; partial or orphaning combo moves are refused | URD-ORD-018 |
| FR-4 | A fully-emptied source is cancelled (FULL_SPLIT); a partially-split source is retained and re-priced | URD-ORD-017 |
| FR-5 | Split refuses a non-positive quantity, an unknown item, or an over-allocation (assignments exceeding the line's available quantity) | URD-ORD-017 |
| FR-6 | Merge moves every item of one or more DRAFT source orders into one DRAFT target and cancels the sources (MERGED_INTO_<target>) | URD-ORD-013 · URD-ORD-019 |
| FR-7 | Only orders of the same merchant and sale channel may merge | URD-ORD-019 |
| FR-8 | Every transferred item appends a transfer-history entry (source, target, time, quantity) forming the split / merge lineage | URD-ORD-020 |
| FR-9 | A merge can be rolled back while the target is still DRAFT and was merged; items return to their original sources via the lineage, and each source is restored to DRAFT | URD-ORD-021 |
| FR-10 | On rollback, quantity added to a line after the merge stays on the target; only the originally-transferred quantity returns to the source | URD-ORD-021 |
| FR-11 | Split, merge and rollback are atomic; allocation usages clone (split) / move (merge) / cancel (rollback) in lock-step and affected orders are re-priced | URD-ORD-022 |
| FR-12 | An order with active checks cannot be split, merged, or rolled back | URD-ORD-023 |
Full requirement text and acceptance criteria live in the Sale URD - ORD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Atomicity | Each operation is one all-or-nothing transaction - any failed sub-step (item move, source cancel, reprice) rolls the whole reshape back |
| Concurrency | Participating orders and their items are row-locked for the duration so two cashiers cannot reshape the same draft at once |
| Auditability | The transfer-history lineage is append-only - a move adds an entry, a rollback pops only the last one; corrections are new state, never edited history |
| Stock continuity | Reserved allocation usages travel with the items - cloned on split, moved on merge, cancelled on rollback - so reservations are never lost or duplicated |
| Pricing | Every order whose items changed is re-priced; split re-prices both the new orders and a retained partial source |
| Tenancy & authz | All operations scoped per merchant (x-merchant-id) and gated by split / merge / rollback permissions |
| i18n | User-facing labels and rejection reasons are bilingual ({ en, vi }) |
8. UX & Flows
Split a draft
Merge then roll back
The cashier surface offers a split panel (assign items and quantities to one or more new bills, name each, optionally set a customer), a merge action (pick sources and a target), and a rollback action on a merged draft.
9. Data & Domain
| Entity | Role in reshape |
|---|---|
SaleOrder | The draft being reshaped; carries the markers stamped by each operation (split / merge timestamps, cancellation reason, origin link) |
SaleOrderItem | The line that moves between orders; a full move relocates it, a partial move splits it into a new line |
TransferHistoryEntry | One append-only step of a line's lineage - source order, target order, time, and quantity moved; the last entry tells rollback where to return the line |
Conceptual only - full schema and invariants live in the sale domain model. Order-to-order relations are soft references; integrity is enforced in the split / merge services, not by database constraints.
10. Dependencies & Assumptions
Depends on
- Sale order lifecycle (ORD) - restructure operates only on DRAFT orders and reuses checkout / cancel transitions.
- Combo fan-out (URD-ORD-005) - the lead / child structure that the atomicity guard protects.
- Bill splitting (CHK) - an order with active checks is blocked from reshape until the checks are rolled back.
- Reserved stock (allocation usages) - cloned / moved / cancelled in step with the items.
@nx/core- sale order and item models, status helpers, and the transfer-history shape.
Assumptions
- A cashier reshapes only their own merchant's and channel's drafts.
- Reserved stock for the source order exists before a split or merge and is the authority for what travels.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| A partial failure leaves orders half-reshaped | Each operation is one transaction with row locks - any failure rolls the whole reshape back |
| A split orphans a combo lead or child | Up-front atomicity guard - combo members must all move together at full quantity, else refused |
| Rollback can't tell where an item came from | The append-only transfer-history lineage records each move; rollback reads the last entry per line |
| Quantity changed after a merge, then rolled back | Surplus added after the merge stays on the target; only the originally-transferred quantity returns |
| Two cashiers reshape the same draft at once | Participating orders and items are row-locked for the operation's duration |
| Reserved stock lost or double-counted | Allocation usages clone on split, move on merge, cancel on rollback - in lock-step with the items |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - ORD reshape capability in the URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | None - reshape runs on the existing draft order and item models; transfer-history is an additive field |
| Launch criteria | Split allocates items / quantities into new drafts (full and partial moves) and cancels or re-prices the source; merge folds sources into a target and cancels them; rollback restores sources from the lineage; combos move atomically; active-check orders are blocked; every operation is atomic with reserved stock following the items |
| Monitoring | Reshape error rate by reason (over-allocation, orphaned combo, active checks, not-merged), rollback success rate, stock-reservation consistency after reshape |
13. FAQ
How is this different from check splitting? Check splitting (CHK) divides one order into independently-payable checks for the same bill. This reshapes the orders themselves - moving items between separate drafts. See PRD-CHK-001.
Can I split or merge an order that's past draft? No - reshape is DRAFT-only (C-02). Once an order checks out, its items are locked.
What happens to a combo when I split? It moves as one unit. The lead and all its children must go to the same new bill at full quantity; assigning only part of a combo is refused before anything saves.
If I take only some of a line's quantity, what happens to the rest? The line splits - the taken quantity becomes a line on the new order, the remainder stays on the source, and both are re-priced.
Can every merge be undone? Yes, while the target is still draft and has no active checks. Rollback uses each line's transfer-history lineage to return it to the exact order it came from; quantity added after the merge stays on the target.
Does reserved stock survive a reshape? Yes - reserved allocation usages are cloned on split, moved on merge, and cancelled on rollback, in lock-step with the items, so reservations are never lost or duplicated.
References
- URD: Sale - ORD
- Related PRD: Sale order & cart · Check split & merge
- Module: Sale - overview + traceability
- Developer: @nx/sale · @nx/core · domain model