Skip to main content

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

StatusNameDescription
PRPreparationInitial state when a ticket is created but not yet activated
ACActiveTicket is open and accepting modifications
RDReadyKitchen/preparation is complete, ticket awaits payment
PDPaidTicket has been paid (cash, card, or online)
PPPaid (account)Paid via account (pending settlement)
CCCancelledTicket has been cancelled
DEDeletedTicket has been soft-deleted
SPSplitterVisual separator line in the product list (not a real ticket status)
RCRecycledInternal status used during offer recalculation
UNUndoLegacy 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:

FlagMeaning
OrderPrintedThe order has been printed to the kitchen printer
ReadyNotifiedA "ready" notification has been sent
TicketReadyThe ticket is marked as ready for pickup/delivery
TicketToPayPayment has been requested
TicketPaidPayment has been confirmed
TicketReopenedThe 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:

  1. Create TicketProduct: A new TicketProduct is instantiated with ticket, product, amount, taxrate (from the product), and status='AC'. The line always points to the base catalog product; no per-line Product is created.
  2. Create TicketOptions: For each selected option, a TicketOption is created with product (the TicketProduct) and option (the ProductOpt).
  3. 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's amount is incremented instead of creating a new line.
  4. Sort position: New products receive a sort value 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 the ticketproduct.type field, but only when the base product has no type of its own (info.type && !product.type).
  • TicketProductView.type resolves the effective type as product?.type ?? ticketproduct.type — the catalog product wins when it defines a type; the line-level type is 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.

MethodDescription
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 SP splitter 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 TicketOffer and 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 × amount at its taxrate.
  • Each extra contributes its charge at its taxrate.
  • Each discount is subtracted from the total.
  • The result updates ticket.price (total with tax) and ticket.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 as RC and stores it for potential reuse.
  • GetRecycle(product): Finds a recycled product matching the requested one (via IsJoin), restores its amount and status, and returns it.
  • DelRecycle(): After recalculation, any remaining recycled products are marked DE.

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:

  1. !IsPaid — commercial payment not yet closed (includes tickets in PP with a pending guest payment request):
    • If ToInvoice, stores the requested mode in pendpay and defers the rest until invoice is created (RN-CAJA-49).
    • Otherwise schedules a print (deferred to commit) when the cash drawer should open (OpenOnCash / OpenOnCard from place config), unless paying an account invoice (invceac set).
    • Sets paidby, paidin (current session), and paidon (current date).
    • Sets payment and status: PP for PAYACCOUNT, PD for all other modes (cash, card, mixed, etc.).
    • When status becomes PD, the status setter resolves the cash register internally and assigns till if not already set (cash attribution — RN-CAJA-31).
  2. IsPaid — ticket already commercially closed (paid at till or sent to account): only updates ticket.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-statepaymentPaidFlagToPayFlagClosed by
Account pendingPAYACCOUNTtruefalseSettlement (ACT-CAJA-LIQUIDAR-TICKET)
GUEST request (till)PAYCASH / PAYCARDfalsetrueWorker (ACT-CAJA-EJECUTAR-COBRO, status = 'PD')
GUEST request (online)PAYLINEfalsetrueServer 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:

PhaseStatuspaidbypaidinpaidontill
Send to accountPPwho sendssession that registers the chargeregistration timestampnull
Settlement / cash intakePDunchangedliquidator (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), and payment.
  • 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 if ticket.till is still null (RN-CAJA-62). Liquidation may preset till before PD (task 06).
  • Internal resolution on PD: private getter in TicketView (not part of the public API). Evaluated after paidin is set in the payment flow. Order: sole entry in place.TillAreas, then paidin.device if IsPos, then ticket creator device for bar (ticket.session.device — RN-CAJA-53). Without paidin, multi-TPV table tickets stay unresolved until payment assigns an executor. QRCODE.till assigns the table to a TILL for multi-TPV routing; it does not override TicketView.till on payment (RN-CAJA-39).
  • Real cash intake (PD) always gets a till via the status setter; PAYACCOUNT in PP keeps till null until settlement even though paidin/paidon are 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

  1. TicketView.OnReturn(selections, { cash, card }) — marks selected units as returned (DE with refund on paid lines), adjusts mixed[] by method, recalculates net price, and cancels the ticket if nothing valid remains.
  2. OnCommit(null) — persists the change; when the net total drops on a fiscally registered ticket, _TicketChange creates a rectificative TicketChange with reason P (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.
  • AskWaiter with reason = 'REFUND' — not created.
  • OnRefund() — removed; returns are complete after OnReturn + 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

  1. Status adjustment: If the ticket is in PR, it moves to AC. If the ticket is empty (no products), it moves to CC.

  2. _OrderNo() (async): Requests a unique order number from the server. Runs in parallel with subsequent steps.

  3. _OpenDrawer(): If the print module has a pending drawer-open request (ToOpen), it fires the OpenDrawer() event immediately.

  4. _TicketChange() (async): Creates the fiscal TicketChange record. See TicketChange Numbering below.

  5. Await order number: Waits for the server to return the order number before proceeding to print.

  6. _PrintTicket(): If the print module has a pending print request (ToPrint), it fires the PrintTicket() event.

  7. _CommitTicket() (async):

    • If the ticket is a copy (CopyOf is set), calls Overwrite() 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.
  8. _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)

ValueMeaningWhen used
XProvisionalUnpaid tickets
STicketPaid tickets without invoice
CTicket + InvoicePaid tickets with invoice at creation
FInvoiceInvoice created after payment
RRectificationPrice changes on reopened tickets

InvoiceType (Invoice Number)

ValueMeaningWhen used
XProvisionalUnpaid tickets
TTicketStandard ticket number
CTicket + InvoiceCombined ticket/invoice number
FInvoiceStandalone invoice number
RRectificationRectification number
ARecapitulativeRecapitulative invoice

Numbering Logic

The _TicketChange() method determines what fiscal action is needed based on the ticket's state:

First Commit (no previous TicketChange)

ScenarioSeriesInvoiceTicketChange?Reason
Unpaid ticketXXNoProvisional numbering only; no fiscal document
Paid without invoiceSTYesReason 'F' (first)
Paid with invoiceCCYesReason 'F' (first)

Subsequent Commits (ticket reopened)

ScenarioSeriesInvoiceReason
Price changedRR'P' (price changed)
Invoice created after paymentFF'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:

FieldSource
ticketThe parent ticket
prevPrevious TicketChange (null for first)
sessionCurrent session
seriesSerial number (from NextDeviceTicket)
invoiceInvoice number (from NextDeviceTicket)
totalpriceinfo.tax.total
basepriceinfo.tax.base
taxespriceinfo.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'):

  1. A new Verifactu object is created.
  2. Its change property is set to the newly created TicketChange.
  3. verifactu.Refresh() is called (async) to populate the fiscal data.
  4. If successful, the Verifactu object is added as a child of the TicketChange.

TicketBAI

If place.SendBAIEnabled is true (i.e. OptionSend === 'BAI'):

  1. A new Ticketbai object is created.
  2. Its change property is set to the TicketChange.
  3. ticketbai.Refresh() is called to generate the TicketBAI signature.
  4. 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

MethodParametersDescription
DoPrint(drawer, isauto, oncommit)drawer: boolean, isauto: boolean, oncommit: booleanSchedules 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

ObservableEventPayload
OnPrintPrint requestedPrintInfo { printer?, isauto, drawer }
OnDrawerDrawer open requestedPrintInfo { 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

ActionMethodDescription
Create ticketnew Ticket(null, data)Creates a ticket in PR status
Add productticketView.ToCart(product, options, amount, info)Adds product with auto-grouping
Add one moreticketView.AddOneOf(ticketproduct)Copies and adds 1 unit
Remove oneticketView.DelOneOf(ticketproduct)Decrements the highest-amount group
Add splitterticketView.AddSplit()Inserts a visual separator
Add discountticketView.AddDiscount(discount)Applies a discount
Mark readyticketView.OnReady()Sets status to RD
CancelticketView.OnCancel()Sets status to CC
ReopenticketView.OnOpen()Clears payment, account, invoice, till, paidby/paidin/paidon; sets ReopenedFlag, status AC
Return productsticketView.OnReturn(selections, refund)Single-step return on a paid ticket; UI then OnCommit(null)
Merge ticketsticketView.OnMerge(other)Moves products and discounts from another ticket, deletes it
CommitticketView.OnCommit(payment, transaction?)Full commit flow via TicketCommit