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
UNUndoPending refund on a reopened ticket

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 pending refunds

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'.
  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). 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, and comments:

interface ToCartInfo {
market?: number | null;
weight?: number | null;
comments?: string | null;
}
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):

  1. If the ticket is not already paid/to-pay:
    • Schedules a print operation (deferred to commit) if cash drawer should open (OpenOnCash or OpenOnCard from place config).
    • Sets paidby (current session), paidon (current date).
    • Sets status to PP for account payments, PD otherwise.
  2. Ticket payment field is updated.

When payment is removed (null):

  • Clears paidby, given, paidon, paidin.
  • Resets status to AC.

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, sets ReopenedFlag, status AC, deletes UN items
Merge ticketsticketView.OnMerge(other)Moves products and discounts from another ticket, deletes it
CommitticketView.OnCommit(payment, transaction?)Full commit flow via TicketCommit