Ticket System
The ticket system in upp-data manages the complete lifecycle of sales tickets — from creation, through product selection, pricing, payment, and fiscal document generation. The core classes are Ticket (DataObject), _TicketView (ViewObject), TicketCommit (commit orchestrator), and TicketPrint (printing orchestrator).
Ticket Lifecycle
Status Codes
| Status | Name | Description |
|---|---|---|
PR | Preparation | Initial state when a ticket is created but not yet activated |
AC | Active | Ticket is open and accepting modifications |
RD | Ready | Kitchen/preparation is complete, ticket awaits payment |
PD | Paid | Ticket has been paid (cash, card, or online) |
PP | Paid (account) | Paid via account (pending settlement) |
CC | Cancelled | Ticket has been cancelled |
DE | Deleted | Ticket has been soft-deleted |
SP | Splitter | Visual separator line in the product list (not a real ticket status) |
RC | Recycled | Internal status used during offer recalculation |
UN | Undo | Legacy line status; no longer used in the product-return flow (returns go directly to DE) |
Flags (Bit String)
Tickets carry a bit string in the flags field, with each flag tracking a specific lifecycle event:
| Flag | Meaning |
|---|---|
OrderPrinted | The order has been printed to the kitchen printer |
ReadyNotified | A "ready" notification has been sent |
TicketReady | The ticket is marked as ready for pickup/delivery |
TicketToPay | Payment has been requested |
TicketPaid | Payment has been confirmed |
TicketReopened | The ticket was reopened after being paid |
State Diagram
Status Getters on TicketView
The _TicketView exposes convenient boolean getters that combine status and flags:
get IsRecent(): boolean // updated within AppConstants.recentMs
get IsEmpty(): boolean // no valid products
get IsOpened(): boolean // active, not cancelled, not ready (or ready but unpaid without prepayment)
get IsClosed(): boolean // inverse of IsOpened
get IsReady(): boolean // ReadyFlag is set
get IsToPay(): boolean // ToPayFlag is set
get IsPaid(): boolean // PaidFlag is set
get IsCancel(): boolean // ticket.IsCancelled && no refundable lines pending selection
IsOpened has special logic for places with prepayment enabled: a ticket in RD status remains "open" only if place.TicketPrepayment is false.
Adding Products (ToCart)
The ToCart method on _TicketView is the primary API for adding products to a ticket:
ToCart(product: ProductView, options: Array<ProductOptView>, amount: number, info: ToCartInfo | null)
The flow:
- Create TicketProduct: A new
TicketProductis instantiated withticket,product,amount,taxrate(from the product), andstatus='AC'. The line always points to the base catalog product; no per-lineProductis created. - Create TicketOptions: For each selected option, a
TicketOptionis created withproduct(the TicketProduct) andoption(the ProductOpt). - Auto-grouping: Before inserting, the method checks if an identical product already exists in the ticket (same product, same options, same offer, same effective price, same routing
type). If found, the existing product'samountis incremented instead of creating a new line. - Sort position: New products receive a
sortvalue placing them at the end of the list; existing products are shifted to make room.
The ToCartInfo interface allows passing optional market price, weight, comments, a fixed (manual) price and a routing type:
interface ToCartInfo {
market?: number | null;
weight?: number | null;
comments?: string | null;
fixed?: number | null; // manual fixed price for the line
type?: 'BAR' | 'KIT' | null; // routing type for generic/various products
}
Generic / various products and routing type
Generic and "various" products (sold with a custom price and a chosen routing type) do not create a dedicated Product in the place catalog. Instead:
- The fixed price is stored on the line (
ticketproduct.fixed = true,ticketproduct.charge = info.fixed). - The routing type (
BAR/KIT) is stored on the line via theticketproduct.typefield, but only when the base product has notypeof its own (info.type && !product.type). TicketProductView.typeresolves the effective type asproduct?.type ?? ticketproduct.type— the catalog product wins when it defines a type; the line-leveltypeis the fallback used for generic/various items.
This keeps the catalog free of throwaway per-line products while preserving the routing information needed for future kitchen commands.
Related Product Operations
| Method | Description |
|---|---|
AddOneOf(ticketproduct) | Copies a product (with its options) and adds 1 unit — used for "+1" buttons |
DelOneOf(ticketproduct) | Finds the grouped product with the highest amount and decrements it |
AddSplit() | Inserts a visual splitter (status='SP') to separate product groups |
AddDiscount(discount) | Applies a DiscountView to the ticket, creating a TicketDiscount |
Price Calculation (_PriceInfo)
The priceinfo getter triggers a full price recalculation when the cached value is invalidated (any product, offer, extra, or discount changes). The _PriceInfo(recalculate) method orchestrates the entire pricing pipeline.
Calculation Pipeline
Step Details
1. ClearOffers: Removes the offer reference from all ticket products, resetting them to unaffiliated state.
2. GroupProducts(false) — Merge equal products:
- Splits products into sections (delimited by
SPsplitter products). - Within each section, finds products that are "equal" (same underlying product, same options, same offer) via
IsEqual(). - Merges equal products by summing their amounts and marking the duplicate as
RC(recycled).
3. ApplyOffers: Iterates over all active offers from the place:
- For each offer, calls
SavingAmount(ticketview)to calculate potential savings. - Picks the offer with the greatest savings.
- Creates a
TicketOfferand assigns matching products to it. - If an offer applies to only part of a product's amount, the product is split: the original keeps the unapplied portion, and a new (or recycled) product gets the applied portion.
- Loops until no more offers can improve savings (with a safety limit to prevent infinite loops).
4. GroupProducts(true) — Display grouping: After offers are applied, products are regrouped for display. Products with the same visual identity are linked via DisplayTo for the UI to show them as a single line.
5. ApplyExtras: Checks which extras from the place apply to this ticket:
- Removes extras that no longer apply (e.g., period expired, product removed).
- Adds new extras that now apply (e.g., table-specific extra, all-product extra).
6. ApplyDiscounts: Triggers calcinfo on each TicketDiscountView, which computes the discount charge.
7. Final summation: Using PriceInfo from upp-defs:
- Each product contributes
charge × amountat itstaxrate. - Each extra contributes its
chargeat itstaxrate. - Each discount is subtracted from the total.
- The result updates
ticket.price(total with tax) andticket.taxes.
Product Recycling
During offer recalculation, products may be split and recombined. To avoid unnecessary creation/deletion cycles, the system maintains a _torecycle array:
AddRecycle(product): Marks a product asRCand stores it for potential reuse.GetRecycle(product): Finds a recycled product matching the requested one (viaIsJoin), restores its amount and status, and returns it.DelRecycle(): After recalculation, any remaining recycled products are markedDE.
Payment
Setting the payment property on a TicketView triggers the payment flow:
set payment(value: TicketPayment | null)
Payment types (TicketPayment): 'PAYCASH', 'PAYCARD', 'PAYLINE', 'PAYMIXED', 'PAYACCOUNT'.
When payment is set (non-null)
The setter branches on IsPaid (PaidFlag), not on IsToPay:
!IsPaid— commercial payment not yet closed (includes tickets inPPwith a pending guest payment request):- If
ToInvoice, stores the requested mode inpendpayand defers the rest until invoice is created (RN-CAJA-49). - Otherwise schedules a print (deferred to commit) when the cash drawer should open (
OpenOnCash/OpenOnCardfrom place config), unless paying an account invoice (invceacset). - Sets
paidby,paidin(current session), andpaidon(current date). - Sets
paymentand status:PPforPAYACCOUNT,PDfor all other modes (cash, card, mixed, etc.). - When status becomes
PD, thestatussetter resolves the cash register internally and assignstillif not already set (cash attribution — RN-CAJA-31).
- If
IsPaid— ticket already commercially closed (paid at till or sent to account): only updatesticket.payment(change payment method on a closed ticket — RN-CAJA-29).
Account charges use the same path: TicketView.account setter delegates to payment = 'PAYACCOUNT'. Account-pending tickets stay in PP with PaidFlag true and ToPayFlag false; pending account is detected by payment === 'PAYACCOUNT' and invceac == null, not by IsToPay (RN-CAJA-74).
Pending payment sub-states in PP (RN-CAJA-73)
All pending-payment tickets use status = 'PP'; discriminate by payment and flags:
| Sub-state | payment | PaidFlag | ToPayFlag | Closed by |
|---|---|---|---|---|
| Account pending | PAYACCOUNT | true | false | Settlement (ACT-CAJA-LIQUIDAR-TICKET) |
| GUEST request (till) | PAYCASH / PAYCARD | false | true | Worker (ACT-CAJA-EJECUTAR-COBRO, status = 'PD') |
| GUEST request (online) | PAYLINE | false | true | Server webhook / sync (ACT-CAJA-PAYLINE-OK) |
All GUEST requests share the same entry state (RN-CAJA-61): PP, payment set to the requested mode, ToPayFlag true, PaidFlag false, paidby = guest session. Only the closing actor differs: till worker sets paidin/paidon/till and keeps paidby unchanged (RN-CAJA-23); server for online pay.
v4 always sets payment when entering PP (no PP without payment). After server confirmation for online pay → PD + PAYLINE with payment unchanged (RN-CAJA-PAYLINE-02). Worker status = 'PD' must not close PAYLINE tickets (RV-CAJA-004).
Dual model for PAYACCOUNT (RN-CAJA-75)
Account tickets use paidin and paidon in two phases:
| Phase | Status | paidby | paidin | paidon | till |
|---|---|---|---|---|---|
| Send to account | PP | who sends | session that registers the charge | registration timestamp | null |
| Settlement / cash intake | PD | unchanged | liquidator (overwrites) | cash intake timestamp (overwrites) | batch till |
The daily income report classifies pending account by payment + created (RN-INC-09); it does not require paidon to be null. SII and period closes use paidon with this dual semantics (phase A = account registration, phase B = cash intake).
PAYMIXED follows the same till attribution as atomic cash/card payment (RN-CAJA-51).
When payment is removed (null)
- Clears
paidby,given,paidon,paidin,till(directly on the DataObject), andpayment. - Resets status to
AC.
OnOpen() performs a full reopen cleanup: same payment fields plus account, invoice, and ReopenedFlag, then status AC (RN-CAJA-30).
Till attribution (till)
TicketView.till: public getter/setter for the persisted logical cash register (TILL). Setter assigns only ifticket.tillis still null (RN-CAJA-62). Liquidation may presettillbeforePD(task 06).- Internal resolution on
PD: private getter inTicketView(not part of the public API). Evaluated afterpaidinis set in the payment flow. Order: sole entry inplace.TillAreas, thenpaidin.deviceifIsPos, then ticket creator device for bar (ticket.session.device— RN-CAJA-53). Withoutpaidin, multi-TPV table tickets stay unresolved until payment assigns an executor.QRCODE.tillassigns the table to aTILLfor multi-TPV routing; it does not overrideTicketView.tillon payment (RN-CAJA-39). - Real cash intake (
PD) always gets a till via thestatussetter;PAYACCOUNTinPPkeepstillnull until settlement even thoughpaidin/paidonare set at registration (RN-CAJA-75).
Product returns (single step)
Paid tickets can return selected products in one operation: the operator picks units, splits cash/card refund amounts on the same screen, and confirms. There is no intermediate pending-refund state and no deferred «confirm refund» step.
Domain flow
TicketView.OnReturn(selections, { cash, card })— marks selected units as returned (DEwithrefundon paid lines), adjustsmixed[]by method, recalculates net price, and cancels the ticket if nothing valid remains.OnCommit(null)— persists the change; when the net total drops on a fiscally registered ticket,_TicketChangecreates a rectificativeTicketChangewith reasonP(same pipeline as reopen/repay price changes).
Cash refund is always available regardless of the original payment method. Card refund cannot exceed the amount originally charged by card. When cash is refunded on a non-mixed ticket, the ticket becomes PAYMIXED with net amounts per method for till closing.
What is not part of the return flow
- Line status
UN(pending refund) — not emitted. AskWaiterwithreason = 'REFUND'— not created.OnRefund()— removed; returns are complete afterOnReturn+ commit.
Returned lines stay visible on closed tickets with refund > 0 for history and printing.
TicketCommit Flow
The TicketCommit class orchestrates the full commit sequence. It is accessed via ticket.Commiter (lazy-initialized).
CommitTicket Sequence
Step-by-Step
-
Status adjustment: If the ticket is in
PR, it moves toAC. If the ticket is empty (no products), it moves toCC. -
_OrderNo()(async): Requests a unique order number from the server. Runs in parallel with subsequent steps. -
_OpenDrawer(): If the print module has a pending drawer-open request (ToOpen), it fires theOpenDrawer()event immediately. -
_TicketChange()(async): Creates the fiscalTicketChangerecord. See TicketChange Numbering below. -
Await order number: Waits for the server to return the order number before proceeding to print.
-
_PrintTicket(): If the print module has a pending print request (ToPrint), it fires thePrintTicket()event. -
_CommitTicket()(async):- If the ticket is a copy (
CopyOfis set), callsOverwrite()to apply the copy's data back to the original. - Pushes the ticket to the transaction (if provided) or calls
DoCommit()directly. - Adds the ticket to the place's and QR code's ticket collections.
- If the ticket is a copy (
-
_LogLocalChange(): Logs the commit activity locally.
TicketChange Numbering
Each paid ticket generates a TicketChange record containing the fiscal series and invoice number. The numbering system uses two enums defined in place.ts:
SerialType (Series)
| Value | Meaning | When used |
|---|---|---|
X | Provisional | Unpaid tickets |
S | Ticket | Paid tickets without invoice |
C | Ticket + Invoice | Paid tickets with invoice at creation |
F | Invoice | Invoice created after payment |
R | Rectification | Price changes on reopened tickets |
InvoiceType (Invoice Number)
| Value | Meaning | When used |
|---|---|---|
X | Provisional | Unpaid tickets |
T | Ticket | Standard ticket number |
C | Ticket + Invoice | Combined ticket/invoice number |
F | Invoice | Standalone invoice number |
R | Rectification | Rectification number |
A | Recapitulative | Recapitulative invoice |
Numbering Logic
The _TicketChange() method determines what fiscal action is needed based on the ticket's state:
First Commit (no previous TicketChange)
| Scenario | Series | Invoice | TicketChange? | Reason |
|---|---|---|---|---|
| Unpaid ticket | X | X | No | Provisional numbering only; no fiscal document |
| Paid without invoice | S | T | Yes | Reason 'F' (first) |
| Paid with invoice | C | C | Yes | Reason 'F' (first) |
Subsequent Commits (ticket reopened)
| Scenario | Series | Invoice | Reason |
|---|---|---|---|
| Price changed | R | R | 'P' (price changed) |
| Invoice created after payment | F | F | 'I' (invoice created) |
| Cancelled | — | — | 'C' (cancelled) — no new TicketChange |
The _InvoiceNumber(reason) method calls place.NextDeviceTicket(serialType, invoiceType) which manages per-device sequential numbering stored in configService.
TicketChange Record Structure
When a TicketChange is created, it stores:
| Field | Source |
|---|---|
ticket | The parent ticket |
prev | Previous TicketChange (null for first) |
session | Current session |
series | Serial number (from NextDeviceTicket) |
invoice | Invoice number (from NextDeviceTicket) |
total | priceinfo.tax.total |
base | priceinfo.tax.base |
taxes | priceinfo.tax.taxes |
TicketBAI / Verifactu Integration
After creating a TicketChange, the commit flow checks if fiscal integrations are enabled on the place. The system to use is configured via place.OptionSend, an enumerated value: '' (none), 'BAI' (TicketBAI), 'VFCT' (Verifactu). Only one system can be active per place.
Verifactu
If place.SendVFTEnabled is true (i.e. OptionSend === 'VFCT'):
- A new
Verifactuobject is created. - Its
changeproperty is set to the newly createdTicketChange. verifactu.Refresh()is called (async) to populate the fiscal data.- If successful, the Verifactu object is added as a child of the
TicketChange.
TicketBAI
If place.SendBAIEnabled is true (i.e. OptionSend === 'BAI'):
- A new
Ticketbaiobject is created. - Its
changeproperty is set to theTicketChange. ticketbai.Refresh()is called to generate the TicketBAI signature.- If successful, it is added as a child of the
TicketChange.
Both integrations run sequentially within the _TicketChange() step, before the actual commit to the server.
TicketPrint
The TicketPrint class manages print requests and cash drawer operations with a deferred execution model.
API
| Method | Parameters | Description |
|---|---|---|
DoPrint(drawer, isauto, oncommit) | drawer: boolean, isauto: boolean, oncommit: boolean | Schedules or executes a print. If oncommit=true, stores flags for deferred execution. |
OpenDrawer() | — | Fires the drawer-open event and resets the request flag |
PrintTicket() | — | Fires the print event and resets all request flags |
Observables
| Observable | Event | Payload |
|---|---|---|
OnPrint | Print requested | PrintInfo { printer?, isauto, drawer } |
OnDrawer | Drawer open requested | PrintInfo { isauto, drawer } |
Deferred Printing
When oncommit=true in DoPrint(), the flags are stored:
_print_requested: A print is pending._dopen_requested: A drawer open is pending._onpay_requested: The print was triggered by a payment.
During CommitTicket, the _OpenDrawer() and _PrintTicket() steps check these flags and fire the events. This ensures the printer and drawer are only activated after the commit data is ready.
Ticket Actions Summary
| Action | Method | Description |
|---|---|---|
| Create ticket | new Ticket(null, data) | Creates a ticket in PR status |
| Add product | ticketView.ToCart(product, options, amount, info) | Adds product with auto-grouping |
| Add one more | ticketView.AddOneOf(ticketproduct) | Copies and adds 1 unit |
| Remove one | ticketView.DelOneOf(ticketproduct) | Decrements the highest-amount group |
| Add splitter | ticketView.AddSplit() | Inserts a visual separator |
| Add discount | ticketView.AddDiscount(discount) | Applies a discount |
| Mark ready | ticketView.OnReady() | Sets status to RD |
| Cancel | ticketView.OnCancel() | Sets status to CC |
| Reopen | ticketView.OnOpen() | Clears payment, account, invoice, till, paidby/paidin/paidon; sets ReopenedFlag, status AC |
| Return products | ticketView.OnReturn(selections, refund) | Single-step return on a paid ticket; UI then OnCommit(null) |
| Merge tickets | ticketView.OnMerge(other) | Moves products and discounts from another ticket, deletes it |
| Commit | ticketView.OnCommit(payment, transaction?) | Full commit flow via TicketCommit |