Skip to main content

upp-data -- Data Objects

The upp-data library provides a schema-driven data object system that forms the backbone of the entire application. Every persistent entity -- users, places, products, tickets -- is represented as a DataObject with a declarative schema that defines its fields, types, default values, and relationships to other objects.

This system exists for three reasons:

  1. Synchronization. The application works offline and syncs changes with the backend. The schema tells the sync engine exactly which fields to send, how to serialize them, and which related objects must be committed together.
  2. Validation. Field types are enforced at runtime. Setting a number field to a string throws an error immediately, catching bugs early instead of corrupting the database.
  3. Relationship management. Parent-child and reference relationships are declared once in the schema. The framework then generates typed getters, setters, Add/Del methods, and handles cascading commits and deep copies automatically.

Understanding DataObjects is essential before working on any feature, because virtually all application logic reads from and writes to these objects.

For refresh and notification when schema fields or child relation sets change — the OnRefresh and OnChildChanged observables and the patchValueToUpdateDoRefresh chain — see the canonical in-repo note libs/upp-data/src/modules/model/objects/upp-data-object.mdc (maintained next to objects/; not duplicated on this page).

Schema to Object Flow


How Schemas Work

Every DataObject starts with a schema definition -- a const object that acts as a DSL (domain-specific language) describing the database table it maps to. The schema has three top-level properties:

PropertyTypePurpose
namestringThe database table name (e.g. "PRODUCT", "TICKET")
fields_schemaColumn[]Array of column definitions describing each field
relate_schemaRelate[]Array of relationship definitions connecting this object to others

Here is a simplified example from the User object:

const _schema = {
name: "USER",
fields: [
{ name: "status", type: "string", default: "PA" },
{ name: "email", type: "string" },
{ name: "firstname", type: "string" },
{ name: "lastname", type: "string" },
{ name: "photo", type: "file" },
{ name: "phone", type: "string" },
{ name: "lang", type: "string" },
{ name: "license", type: "boolean", default: false },
],
relate: [
{ direction: "<", target: "PLACE", by: "user", name: "place", reason: "places", child: true, class: PlaceType },
{ direction: "<", target: "STAFF", by: "user", name: "employee", reason: "employees", child: true, class: EmployeeType },
{ direction: "<", target: "SESSION", by: "user", name: "session", reason: "sessions", child: true, class: SessionType }
]
} as const;

The as const assertion is critical -- it enables TypeScript to infer literal types from the schema, which CreateObject() uses to generate a fully typed class.

Field Definitions

Each entry in the fields array is a _schemaColumn with the following properties:

PropertyTypeRequiredDescription
namestringYesColumn name as it appears in the database
typestringYesOne of the supported field types (see below)
defaultanyNoDefault value assigned when the object is created locally
aliasstringNoAlternative property name in the TypeScript class. For example, the field prodopt in the database is exposed as option in the class when alias: "option" is set
volatilebooleanNoWhen true, the field is not sent to the server during commits. Used for locally-computed or server-managed fields like created and updated timestamps

Field Types

The schema supports six field types. Each type determines how data is validated on write, serialized for the server, and deserialized when loaded from the database:

TypeTypeScript TypeStored As (Server)Description
stringstring | nullVARCHAR/TEXTPlain text value
filestring | nullFile pathA urlfile object internally ({ url, b64 }). The getter returns the URL or base64 string; the setter accepts either a URL or a data-URI
numbernumber | nullDECIMAL/INT (as string)Numeric value. Sent as a string to the server, parsed back to number on load
dateDate | nullMySQL datetime stringJavaScript Date object. Converted to/from MySQL datetime format automatically
booleanboolean | null'0' or '1'Boolean value. Stored as a single character in the database
objidstring | nullINT (foreign key)A foreign key reference to another object. Fields of this type are usually paired with a relationship definition in relate

Relationship Definitions

Each entry in the relate array is a _schemaRelate that describes how this object connects to another:

PropertyTypeDescription
direction'>' or '<'> means this object references another (outgoing, 1:1). < means other objects reference this one (incoming, 1:N)
targetstringThe database table name of the related object
bystringThe field on the related object that stores the reference. For > relations, this is the field name on the child side; for < relations, it is the field on the child that points back to this parent
namestringThe accessor name for 1:1 relations (used as the getter/setter property name)
reasonstringThe key used to store the relationship internally. For 1:N relations, this becomes the list accessor name (e.g. products, tickets)
childbooleanWhen true, this related object is considered a child -- it will be included in commit operations and deep copies when the parent is committed or copied
classclassOptional typed class reference for proper TypeScript casting of the related object

Outgoing Relations (> -- 1:1)

An outgoing relation means "this object holds a reference to another object." The reason field stores the objid of the referenced object. The framework generates:

  • A getter that returns the related object instance (or null)
  • A setter that accepts a DataObject or null, updating the reference

For example, a Ticket has { direction: ">", target: "PLACE", name: "place", reason: "place" }. This means ticket.place returns the Place object that the ticket belongs to, and ticket.place = somePlace updates the reference.

Incoming Relations (< -- 1:N)

An incoming relation means "other objects reference this one." The framework generates:

  • A list getter using the reason name (e.g. place.products returns an array of Product objects)
  • An Add<Name> method (e.g. place.AddProduct(product))
  • A Del<Name> method (e.g. place.DelProduct(product))

The name property (singular) is used to build the Add/Del method names with the first letter capitalized.

Child Relations

When child: true, the relationship has cascading behavior:

  • Commits: When the parent is committed via DoCommit(), all child objects are included in the commit payload automatically. This ensures the server receives the complete object graph in one operation.
  • Deep copies: When the parent is copied via Copy(), all child objects are recursively copied as well, maintaining the relationship structure in the new copy.
  • Overwrite: When a copied parent is overwritten back onto the original via Overwrite(), the child structure is restored recursively.

Non-child relations (like Ticket > Place) are simple references -- the ticket points to the place, but committing a ticket does not commit the place.


The Class Hierarchy

Understanding the inheritance chain helps when reading the source code or extending objects:

BaseObject              Abstract base. Tracks commit state (_toInsert, _toUpdate, _toDelete),
| manages the unique identifier (_uuid/objid), and defines the commit pipeline.
|
v
RelatedObject Adds relationship management: _children, _chldlist, SetChild/AddChild/DelChild,
| and the bidirectional relation tracking (_related map).
|
v
DataObject Adds the view layer (ViewObject/viewProxy), deep copy support (Copy/Overwrite),
| refresh propagation (OnRefresh, UpRefresh), and patchValue for change tracking.
|
v
BaseClass<T> Generated by CreateObject(). Dynamically defines typed getters/setters for all
| schema fields, implements Change/Depend/Children for the commit pipeline,
| and handles Data/Info serialization based on the schema.
|
v
[Your Object] Your concrete class (e.g. Ticket, Product). Extends the generated BaseClass
with domain-specific logic: custom getters, business rules, flag management,
and the viewProxy override for the UI layer.

CreateObject -- The Schema Compiler

CreateObject(schema) is the function that bridges the gap between a static schema definition and a live TypeScript class. It:

  1. Validates the schema structure (field types, relation directions)
  2. Creates an abstract class extending BaseClass<T> with the schema baked in
  3. Defines getters/setters on the prototype for every field, with runtime type validation
  4. Defines relationship accessors on the prototype (1:1 getters/setters, 1:N list getters and Add/Del methods)
  5. Returns the class constructor, which you then extend with your concrete class

When a derived class overrides a field getter/setter (common for status, photo, payment), the base accessor is remapped to __base__<fieldname>. This allows the derived class to call this['__base__status'] to read/write the raw value while adding business logic in the override.


Object Lifecycle

Creating a New Object

When you instantiate a DataObject with objid = null, the framework treats it as a new object that needs to be inserted into the server:

const ticket = new Ticket(null, dataService);
ticket.place = currentPlace;
ticket.qrcode = currentTable;
ticket.status = 'PR';

At this point:

  • _toInsert = true -- the object is flagged for insertion
  • objid is null -- the server will assign a real ID during sync
  • _uuid is auto-generated -- used as a temporary identifier until the server responds with a real objid

Loading From the Server (Sync)

When data arrives from the server (during synchronization), the framework calls OnUpdate(info) on the object. This method:

  1. Checks if the object can be updated (_CanUpdate)
  2. Compares the incoming data against the last update to skip no-ops
  3. Calls _OnUpdate(info) which deserializes database values into typed properties via the schema
  4. Resolves or replaces the object in storage (maps the server-assigned objid to the local _uuid)
  5. Triggers DoRefresh() to notify the view layer
  6. Marks _isLoaded = true and completes the _waitLoaded subject

After loading, the object has a real objid and is fully resolved in the data store.

Modifying an Object

Setting any schema-defined property triggers the generated setter, which:

  1. Validates the value type against the schema
  2. Calls patchValue() to check if the value actually changed
  3. If changed, sets ToUpdate = true, which flags the object for the next commit
product.name = "Cafe con leche";
product.price = 2.50;
product.taxrate = 10;

Each property change is tracked internally. The Change getter later collects all current field values for the commit payload.

Committing Changes

Committing sends changes to the server. The process works through a recursive commit map:

await ticket.DoCommit();
  1. _CommitMap() walks the object and all its child: true relations recursively, collecting every object that has _toInsert, _toUpdate, or _toDelete flags set
  2. For each object, it builds a CommitChange containing:
    • relation: identifies the object (table + objid or uuid)
    • entrynfo: the serialized field data plus the action (do_insert, do_update, do_delete)
    • requires: dependency information ensuring related objects exist before this one
  3. data.Commit(changes) queues all changes
  4. data.Flush(force) sends them to the server (immediately if force = true, otherwise on next sync cycle)

This design ensures that an entire object graph (e.g. a ticket with all its products, options, offers, and discounts) is committed atomically.

Partial Context (Isolated Graph Loads)

upp-data supports creating objects in a partial context for on-demand loads (for example ticket detail graphs from reporting flows) without contaminating the live global store.

  • ObjectOptions.partial = true creates an isolated context.
  • If no ObjectOptions.storage is provided, an ad-hoc storage is created for the partial root and shared by descendants.
  • Resolution order is: contextual storage -> global store -> new instance.
  • A partial object cannot coexist with a global object with the same table + objid; collisions throw an error.
  • Completed stores the unix timestamp from the last reports/complete response for partial objects. For non-partial objects, Completed is always null.
  • Promotion to global is explicit (IsPartial = false) and only allowed when Completed != null and there is no global collision.

dataService exposes helpers for isolated graph loading (BuildPartialGraph and FetchByItem).

Deep Copies and Overwrite

The copy/overwrite pattern is used for editing workflows where you want to modify an object without affecting the original until the user confirms:

const copy = ticket.Copy();

copy.status = 'RD';
copy.AddProduct(newProduct);

const original = copy.Overwrite();
await original.DoCommit();
  • Copy() creates a deep clone of the object and all child: true relations, setting _copyof on each clone to point back to the original
  • Overwrite() transfers the copy's data back to the original, recursively restoring all relationships, and removes the copy from the data store

Object Status Convention

Most objects use a status string field with these common values:

StatusMeaning
ACActive -- the object is live and valid
PAPending -- awaiting activation (common for new users)
DEDeleted -- logically deleted (soft delete)
PRPreparing -- used for tickets that are being built
RDReady -- used for tickets that are ready to serve
CCCancelled -- used for tickets that were cancelled
CTCatalog -- used for products that come from the catalog template

The status setter in most objects includes a guard: if (this.status == 'DE') return; -- once an object is deleted, its status cannot be changed back. When a non-committed object is deleted, it is also removed from its parent's child list automatically.


Core Objects

User

Table: USER | Default status: PA (Pending)

The User object represents an authenticated person in the system. A user owns places and has sessions on devices.

Key fields:

FieldTypeDescription
statusstringPA = pending activation, AC = active, DE = deleted
emailstringLogin email address. Unique per user
firstnamestringFirst name
lastnamestringLast name
photofileProfile photo (URL or base64)
phonestringPhone number
langstringPreferred language code (e.g. "es", "en")
licensebooleanWhether the user holds a valid license

Computed properties:

  • fullname -- concatenates firstname and lastname
  • IsActive -- true when status == 'AC'
  • IsPending -- true when status == 'PA'

Relationships:

DirectionTargetAccessorChildWhat it means
childPLACEplacesYesPlaces owned by this user
childSTAFFemployeesYesEmployee roles this user holds (one per place)
childSESSIONsessionsYesActive device sessions

A user can own multiple places and be an employee at other users' places simultaneously.


Session

Table: SESSION | Default status: AC

A Session represents a device that is currently logged in. It ties a user to a device and optionally to a specific QR code (table) they scanned.

Key fields:

FieldTypeDescription
statusstringAC = active, DE = deleted
idstringA session identifier (used as the UUID override -- _uuid returns this.id)
deviceidstringUnique device identifier

Computed properties:

  • IsActive -- true when status == 'AC' and updated is within the last 600 seconds (10 minutes). This means sessions expire automatically when the device stops sending updates.

Relationships:

DirectionTargetAccessorChildWhat it means
refUSERuserNoThe logged-in user
refQRCODEqrcodeNoThe QR code (table) the session is associated with
childFCMfcmNoFirebase Cloud Messaging tokens for push notifications

Employee

Table: STAFF | Default status: AC

An Employee links a User to a Place with specific permissions. This is the authorization model -- what a person is allowed to do at a particular place.

Key fields:

FieldTypeDescription
statusstringAC = active, DE = deleted
aliasstringDisplay name override for this place
allowstringPermission bitmask stored as a string of '0' and '1' characters
emailstringContact email (may differ from the user's main email)
photofilePhoto override for this place

Delegated properties (read from the linked User):

  • firstname, lastname, fullname, phone

Permission system: The allow field is a positional bitmask where each position corresponds to an EmployeePermission enum value:

PositionPermissionDescription
0StaffManageCan manage employees
1OfferManageCan manage offers
2ProductManageCan manage the product catalog
3TableManageCan manage QR codes/tables
4ExtraManageCan manage extras/surcharges
5PlaceManageCan manage place settings
6LicenceManageCan manage the licence
7CanComplimentCan apply complimentary items
8CanAvailableCan set availability
9POSManageCan use the POS interface
10ReportAccessCan view reports
11TillManageCan manage the cash register
12TillOpenCan open the cash drawer
13CanReopenCan reopen closed tickets
14CanModifyCan modify existing tickets
15CanCancelCan cancel tickets
16CanReturnCan process returns

Use employee.IsAllowedTo(EmployeePermission.ProductManage) to check and employee.SetAllowTo(EmployeePermission.ProductManage, true) to grant.

Relationships:

DirectionTargetAccessorChildWhat it means
refUSERuserNoThe underlying user account
refPLACEplaceNoThe place this employment belongs to

I18nValue (menu translations)

Table: I18NVALUE

Each row stores the translations for one translatable field (for example, a product name). Owner tables (PRODUCT, CATEGORY, …) will reference these rows through objid foreign keys in later milestones; H0 only introduces the table and the model object.

Language columns: ES, EN, EU, ZH — aligned with languageService.GetLanguages(). The schema generates getters/setters per column (row.ES, row.EN, …): the stored value for each language.

Display resolution: getters es, en, eu, zh return the text to show for that language. If the primary column is null, fallback follows languageService.GetLanguages() order (ES → EU → EN → ZH). The caller picks the getter matching the active context (UI language, ticket place.lang, etc.). Edition reads/writes the uppercase column directly.

Frozen maps: I18nTr(data, values, lang) resolves text from plain I18nValues snapshots (ticket registry, TicketBAI) without a persisted row.

Multilingual map (values): symmetric getter/setter over a { [languageCode]: string } map across the supported languages (languageService.GetLanguages()). Reading returns the non-null stored columns (or null when all are empty); writing assigns each supported column from the map (trimmed; missing or blank entries become null) through the per-column setters, so the change is tracked like any other field edit. This is the contract used by the upp-language-input / upp-language-textarea widgets: the form holds the map and the edition screen assigns it on apply (row.values = map, creating the row when the owner had none).

MemberPurpose
es / en / eu / zhDisplay string for that language
values (get/set)Full multilingual map for reading the form value and writing it back on apply

Access pattern: like Address, each owner holds its own I18nValue instance (via relation/FK in H1). Callers use the related child from the model, not a static lookup.

JSON maps (informes): resolution over plain { ES, EN, … } maps belongs in feature/reports when that milestone is implemented — not in this data object.

Place default language: Place.lang / PlaceView.lang reads optionLanguage from PLACEOPT; if unset, 'ES'.

Adding a new language (procedure)

  1. Add the column to tenant schema: ALTER TABLE I18NVALUE ADD COLUMN <LANG> TEXT DEFAULT NULL.
  2. Update server/src/App/Resources/model/p_schema.json and the I18NVALUE fields in libs/upp-data/src/modules/model/objects/i18nvalue.ts.
  3. Only then append the language to languageService.GetLanguages() and expose it in edition selectors.

Never add a language to GetLanguages() before the I18NVALUE column exists.


Place and Infrastructure

Place

Table: PLACE | Default status: AC

A Place is the central entity of the system -- it represents a physical business location (restaurant, bar, shop). Everything else (products, tickets, tables, employees) belongs to a place.

Key fields:

FieldTypeDescription
statusstringAC = active
namestringBusiness name (scalar; not menu i18n)
descriptionobjid → I18NVALUEOptional multilingual description for menu / QR (display via PlaceView.description or tr(row))
photofileMain photo. Falls back to a generated avatar from the name
logofileBusiness logo
expiresdateLicense expiration date
businessstringType of business
companybooleanWhether it is a registered company
activitystringBusiness activity code
taxidstringTax identification number (CIF/NIF)
phonestringContact phone

Computed properties:

  • avatar -- generates a placeholder image URL from the place name
  • photo -- returns the uploaded photo, or falls back to the avatar
  • lang -- default language for menu texts on printed tickets (optionLanguage, fallback 'ES')
  • IsValid -- true when status == 'AC'
  • IsExpired -- true when expires is in the past
  • Initialize() -- emits on OnInitialize to signal that the place has been fully loaded for the first time

Relationships:

DirectionTargetAccessorChildWhat it means
refUSERuserNoThe owner of this place
refADDRESSaddressYesPhysical address (geocoded)
refADDRESSfiscalYesFiscal/billing address
refI18NVALUEdescriptionYesOptional place description for menu / QR
childQRCODEqrcodesYesTables/QR codes
childPRODUCTproductsYesProduct catalog
childOFFERoffersYesActive offers/menus
childEXTRAextrasYesSurcharges
childDISCOUNTdiscountsYesDiscounts
childSTAFFemployeesYesEmployees
childTICKETticketsYesAll tickets (orders)
childPRICEZONEpricezonesYesPrice-adjustment zones (mesas via QRCODE.pricezone)
childPRICEPERIODpriceperiodsYesTime-of-day price periods shared by all zones
childSTRIPEstripesYesPayment provider accounts
childPAYACCOUNTaccountsYesCustomer payment accounts
childRASPPIservicesYesConnected hardware services
childPLACEOPToptionsYesPlace configuration options
childPLACELINKlinksYesExternal URLs (PLACELINK.link: e.g. homepage, social keys, google for Google Business Profile, pdfmenu, pemcert)
childINVOICEBUSINESSclientsYesInvoice clients
childAUDITauditsYesAudit trail entries
childPAYMENTtransactionsYesPayment transactions

The Place object is the root of the data tree. During synchronization, the entire place graph (products, offers, tickets, etc.) is loaded as children of the place.


Rasppi and RasppiConnect

Tables: RASPPI, RASPPICONNECT

RASPPI represents a POS hardware service configured for a place.
RASPPICONNECT stores the per-device connection details to that service.

RASPPI key fields:

FieldTypeDescription
placeobjidParent place
statusstringAC = active, DE = deleted
idstringService identifier/name
pingdateLast seen/ping time
versionstringInstalled service version

RASPPI relationships:

DirectionTargetAccessorChildWhat it means
refPLACEplaceNoThe place that owns this service
childRASPPICONNECTconnectsYesDevice-specific connection entries
childPRINTERprintersYesReceipt printers attached to this service
childSCALEscalesYesWeighing scales attached to this service
childCODESCANcodescansYesUSB HID barcode scanners configured for this TPV service

RASPPICONNECT key fields:

FieldTypeDescription
statusstringAC = active, DE = deleted
rasppiobjidParent service (RASPPI)
devicestringUnique client device identifier
connectstringRemote URI (host:port) when remote mode is enabled
hassvcbooleanWhether this device is connected to the service

In the place device settings, when a device is an operational TPV it must provide a service id and can optionally provide a remote URI. This is persisted through RASPPI + RASPPICONNECT.

Normative spec (single source): upp-model.mdc → section Entidad TILL y servicio de impresión§5 Separación caja / servicio / dispositivo (RN-CSD-01…07); till → service via Till.service; service → terminals via Rasppi.terminals; RN-TILL-16.

Layer summary: Till = logical cash register (income, audits). Rasppi = hardware service for that till. RasppiConnect = how this PC uses the service. Ticket.till / Audit.tillTill, not Rasppi (RN-CAJA-43).

UI responsibilities: Single-TPV — no cash-register UI; first POS activation on tab Device creates shared RASPPI and implicit TILL (RN-TILL-17a). Multi-TPV — tab Device only RASPPI + connect; till lifecycle in cash register control panel (TODO). See RN-CSD-01/02 in upp-model.mdc.

Summary: Single-TPV — at most one active TILL and one shared RASPPI; N bar PCs with N RASPPICONNECT. Multi-TPV — one TILL and one dedicated RASPPI per cash register (0..1 service per till; no sharing between tills). TILL.name: see RN-TILL-14 / RN-CSD-01; UI shows trimmed name or @place_till_name_default.


Till

Table: TILL (tenant database)

Logical cash register for the place. Full rules: upp-model.mdc § TILL (RN-TILL-03 single, RN-TILL-04 multi, RN-TILL-16 till↔service 0..1).

Key fields:

FieldTypeDescription
statusstringAC = active, DE = deleted (logical, not reactivated)
namestringOptional alias in multi-TPV mode; null in single-TPV mode
rasppiobjidHardware service (RASPPI) for this till; single source of truth for till↔service (0..1, RN-TILL-16)

Canonical relations: Till.service (0..1 RASPPI hardware, read from TILL.rasppi as the single source of truth, null when unlinked). Operational POS terminals (PCs) belong to the service: Rasppi.terminals (devices with an active RASPPICONNECT, hassvc); a till without service has no terminals. See upp-model.mdc § TILL.

Relationships:

DirectionTargetAccessorWhat it means
refRASPPIrasppiHardware service for this till (0..1, RN-TILL-16)
childQRCODEqrcodesTables assigned to this till (multi-TPV)
childTILLCHANGEtillchangesPending in/out and recent committed movements (sync: audit IS NULL or updated within 7 days)

PriceZone

Table: PRICEZONE | Default status: AC

Price-adjustment zone for a place: a named group of tables that can share optional product price overrides. Time-of-day periods belong to the place, not to the zone. Normative contract: upp-model.mdc § Zonas de precio y franja horaria (RN-PZ-*, RF-PZ-*).

Key fields:

FieldTypeDescription
statusstringAC = active, DE = deleted (cascades: clears QRCODE.pricezone on assigned tables; does not delete place periods)
nameobjid → I18NVALUEMulti-language zone name
sortnumberDisplay order in place lists

Default zone: implicit — QRCODE.pricezone == null / PRODUCTPRICE.pricezone == null. Not persisted as a PRICEZONE row. Base price is always PRODUCT.price; period overrides for the default zone are allowed (RN-PZ-02).

Relationships:

DirectionTargetAccessorChildWhat it means
refPLACEplaceNoOwning place

Tables are not a child relation: PriceZoneView.tables filters place.qrcodes where _qrcode.pricezone === this (same pattern as TillView.tables). Periods are not children of the zone (PLACE.priceperiods).


PricePeriod

Table: PRICEPERIOD | Default status: AC

Named time window belonging to one PLACE (weekdays mask + ini/end + optional allday/timezone + sort for overlap priority). Shared by all price zones of that place. Evaluation: PricePeriod.IsActiveAt(at). Active period selection: PlaceView.ActivePeriod(at) (lowest sort among active periods). Created from the place editor (new PricePeriod(...) + place.AddPriceperiod(...), schema-generated).

Key fields:

FieldTypeDescription
statusstringAC / DE / UN (UN = temporarily disabled)
placeobjidOwning place (PLACE child priceperiods)
nameobjid → I18NVALUEPeriod label (e.g. "Morning")
alldaybooleanIf true, active without checking weekdays/time
weekdaysstring (7)Day mask ('1' = active); ignored if allday
ini / endstring HH:MMTime range (no midnight crossing); ignored if allday
timezonestringSame pattern as OFFERPERIOD
sortnumberOverlap priority — lower wins (first in list)

Relationships:

DirectionTargetAccessorChildWhat it means
refPLACEplaceNoOwning place

Audit

Table: AUDIT (tenant database)

Till-close record for a TILL. Full rules: upp-model.mdc § Cierres de caja (RN-AUD-*), consumer feature/audits (upp-feature-audits.mdc).

Key fields:

FieldTypeDescription
statusstringPR = open-period draft (client only); AC = committed close
tillobjidCash register (TILL) this close belongs to (RN-CAJA-42)
ini / enddateClose window bounds; end is set at commit
cashnumberNet sum of TILLCHANGE tipo CASH (or unset) linked at close (RN-AUD-05)
cardnumberNet sum of TILLCHANGE tipo CARD linked at close (RN-AUD-05)
sequencenumberClose number ≥ 1 per place; volatile — assigned atomically on server INSERT (RN-AUD-06); null until sync after commit
reportstringJSON snapshot of aggregated totals (AuditInfoReport); volatile — maintained by server (RN-AUD-07); client reads, never writes on commit

Aggregates: open period uses AuditView.LoadReport() (client, schemaVersion = 0). Committed closes read Audit.Report from sync (RN-AUD-08). Contract: audits.mdc.

Relationships:

DirectionTargetAccessorWhat it means
refPLACEplaceOwning place
refSESSIONsessionSession that performed the close
refTILLtillCash register for this close
childTILLCHANGEtillchangesCash/card adjustments linked at commit

TillChange

Table: TILLCHANGE (tenant database)

Manual cash or card in/out while the till period is open. Movements with audit null belong to the current open window; at close they are linked to the committed AUDIT (RN-AUD-03, RN-AUD-05). Each movement stores the logical cash register in till (assigned when the operator records in/out).

Key fields:

FieldTypeDescription
typestring'CASH' (default) or 'CARD' — adjustment bucket (RN-AUD-16)
amountnumberSigned amount (positive = in, negative = out)
conceptstringOperator note
tillobjidCash register where the movement was recorded (RN-AUD-03)
auditobjidCommitted close that includes this movement (null while period is open)

Relationships:

DirectionTargetAccessorWhat it means
refPLACEplaceOwning place
refSESSIONsessionSession that recorded the movement
refTILLtillLogical cash register for this in/out
refAUDITauditCommitted close (null until linked at till close)

Access: Till.tillchanges — all movements synced for this till (datamodel.json: audit IS NULL or updated within 7 days). TillView.tillchanges — pending only (audit null). TillView.cashchanges / cardchanges — pending filtered by type (CASH or unset → cash; CARD → card). Flat entity: no dedicated ViewObject; consumers use TillChange directly (RN-20). Creation is orchestrated by feature/audits (place/till FKs, till.AddTillchange, change.DoCommit()). Committed movements appear on Audit.tillchanges.


AuditView (aggregation API)

View wrapper for Audit. Obtained via dataService.alives.get(audit) like other domain views.

APIUse
LoadReport()Aggregates tickets for the open period (PR only); returns AuditInfoReport with schemaVersion = 0; sets ini/end on pending audit
ReportPR: lazy-load online → cache; offline best-effort. AC: delegates to Audit.Report sync
LoadTickets()Ticket rows for the period window
sequenceProxy to server-assigned close number
isLoadingSet only by lazy Report getter (not by explicit LoadReport)

Ticket source: ticketService.Query — consumed only inside AuditView, not from features directly.


Address

Table: ADDRESS

An Address stores geocoded location data. It is always a child of another object (typically a Place for both the physical and fiscal addresses).

Key fields:

FieldTypeDescription
formattedstringFull formatted address string (e.g. "Calle Mayor 5, 28001 Madrid")
doorstringDoor/apartment number
latitudenumberGPS latitude
longitudenumberGPS longitude
accuracynumberLocation accuracy in meters
timezonestringIANA timezone (defaults to 'UTC')
streetstringStreet name
numberstringStreet number
zipcodestringPostal code
townstringCity/town
provincestringProvince/state
countrystringCountry

No relationships. Address objects are always embedded as children of a parent object.


QrCode

Table: QRCODE | Default status: AC

A QrCode represents a table or service point in a place. Customers scan the QR code to access the menu and place orders. In a restaurant context, each table has its own QR code.

Key fields:

FieldTypeDescription
statusstringAC = active, PA = pending setup
aliasstringHuman-readable name (e.g. "Table 5", "Bar")
numbernumberTable number
chairsnumberNumber of diners / table capacity; DB default 1. New tables created from the app use 4 unless another value is supplied.
tillobjidTILL logical cash register assigned to this table
pricezoneobjidPRICEZONE price-adjustment zone; null = default zone (base PRODUCT.price; period overrides still apply)

Computed properties:

  • IsPending -- true when status == 'PA'
  • QrCodeUrl(language) -- builds the guest access URL using linked QRSCAN (authorization + objid)

Relationships:

DirectionTargetAccessorChildWhat it means
refPLACEplaceNoThe place this table belongs to
refQRSCANqrscanNoGlobal scan/access row linked to this table
refFLOORAREAareaYesThe area/zone of the place (e.g. terrace, indoor)
refDRAWITEMdrawYesVisual representation on a floor plan
refTILLtillNoLogical cash register assigned to this table
refPRICEZONEpricezoneNoPrice-adjustment zone (null = default zone)
childASKWAITERrequestsYesWaiter call requests from this table
childEXTRATABLEextrasYesExtras/surcharges specific to this table
childTICKETticketsYesActive tickets on this table

FloorArea

Table: FLOORAREA | Default status: AC

A FloorArea represents one editable zone of a place floor plan (for example indoor, terrace, or bar area).
It stores plan metadata and the optional area image configured from the floor-area edit panel.

Key fields:

FieldTypeDescription
statusstringAC = active, DE = deleted
namestringHuman readable area name
photofileOptional image associated with the area (URL or base64)
coord_w, coord_hnumberArea width/height in cm

Computed properties:

  • IsValid -- true when the row has a non-deleted status (status present and not 'DE').

Relationships:

DirectionTargetAccessorChildWhat it means
childDRAWITEMdrawsYesGeometric items rendered on the floor plan
childQRCODEqrcodesYesTables assigned to this area

DrawItem

Table: DRAWITEM | Default status: AC

A DrawItem stores the persisted layout of an element on a floor plan (position, size, rotation, shape type, and optional custom payload). The Konva layer reads and writes these fields through sync like any other data object.

Key fields:

FieldTypeDescription
statusstringAC = active, DE = deleted
typestringItem type discriminator
areaobjidParent floor area (FLOORAREA)
shapestringShape kind identifier
customstringOptional serialized custom data
sortnumberZ-order / sort index (default 0)
coord_x, coord_ynumberPosition
coord_w, coord_hnumberWidth and height
coord_rnumberRotation

Computed properties:

  • IsValid -- true when the row has a non-deleted status (status present and not 'DE').

Runtime-only (not persisted, not synced):

  • konva -- getter/setter typed as any. Holds the running Konva node (or group/shape) instance in a WeakRef so it does not keep the object alive and is never written to the database. Assign null to clear. If the shape has been garbage-collected, the getter returns null.

Relationships:

DirectionTargetAccessorChildWhat it means
refFLOORAREAareaNoFloor area this item belongs to
childQRCODEqrcodesYesQR codes whose draw field points at this item

QrScan

Table: QRSCAN | Default status: AC

A QrScan is the global entry point created when a customer scans a QR code. It is used as the sync root for the guest "table" load -- when a guest scans, the system creates or retrieves a QrScan that gives them access to the appropriate place and table data.

Key fields:

FieldTypeDescription
statusstringAC = active
authorizationstringAuth token for the scan session
qrcodeobjidThe QR code that was scanned
numbernumberTable number (copied from QrCode)
aliasstringTable alias (copied from QrCode)

Relationships:

DirectionTargetAccessorChild
refPLACEplaceNo

PrintDevice

Table: PRINTER | Default status: AC

Represents a physical printer connected to the system (typically a thermal receipt printer).

Key fields:

FieldTypeDescription
namestringPrinter display name
devicestringDevice path or address
typestringPrinter type/protocol
mt, mr, mb, mlnumberMargins in millimeters (top, right, bottom, left)
widthnumberPaper width in millimeters
fontnumberFont size
copiesnumberNumber of copies to print (default: 1)
updatesbooleanPrint order updates
posudtsbooleanPrint POS updates
oncashbooleanPrint on cash payment
oncardbooleanPrint on card payment
boldbooleanUse bold font
stylestringPrint style template (default: 'NULL')

No relationships defined in the schema.


CodescanDevice

Table: CODESCAN | Default status: AC

Represents a USB HID keyboard-emulation barcode scanner connected to the POS PC and associated with a RASPPI service (same level as printers and scales). The scanner does not use WebSocket or the local service protocol; the browser receives scans through barcodeService in upp-base.

Key fields:

FieldTypeDescription
rasppiobjidParent RASPPI service
statusstringAC = active, DE = deleted
namestringOptional label shown in device settings

UI: Managed in feature/devices (upp-codescan-edit / upp-codescan-view) under the place service editor. Features gate barcode UI on placeView.service?.codescans.length.

Cart resolution: PlaceView.doBarCode(code) matches PRODUCT.barcode and PRESELECT.barcode (exact). Manual numeric search uses PRODUCT.code / PRESELECT.code via PlaceView.doPrdCode(pattern) when optionProductCodes is enabled.


Catalog Objects

The catalog is the product hierarchy of a place. It consists of products, their options (customizations), categories (groupings of options), preselects (preset option combinations), families (product variants), and associated scheduling.

Product

Table: PRODUCT | Default status: AC

A Product is the core item in the catalog -- anything that can be ordered. Products can form hierarchies (a product can have a parent product), and catalog templates link to their active replacement via next.

Key fields:

FieldTypeDescription
statusstringAC = active, DE = deleted, CT = catalog template
namestringDisplay name
descriptionstringDescription shown to customers
photofileProduct image. Falls back to a generated avatar
pricenumberBase price (tax-exclusive)
taxratenumberTax rate percentage. Defaults to AppConstants.defaultTaxRate if not set
codestringShort internal product code for manual lookup when optionProductCodes is enabled (numeric keypad search in cart). Not the barcode scanned by the HID reader
barcodestringBarcode string (EAN-13, UPC, GS1-128, etc.) used by the USB HID scanner and cart resolution via PlaceView.doBarCode
ctgrefstringCatalog reference -- links to a product template in the global catalog
typestringProduct type classifier
sortnumberSort order within its parent or place
isgroupbooleanWhether this product is a visual group/header (not orderable itself)
genericbooleanWhether this is a generic/customizable product
isfamilybooleanWhether this product has family variants
flagsstringBitmask for visibility and pricing flags

Flag system: Similar to the Employee permission bitmask:

PositionFlagDescription
0HideAppHide from the customer-facing QR app
1HidePdfHide from the PDF menu
2HideWebHide from the web menu
3MarketPrice (IsMarket)Product does not have a fixed price (price entered at sale time)
4Weighted (IsWeighted)Product is sold by weight

Computed properties:

  • avatar / photo -- fallback photo generation from the product name
  • taxrate -- returns AppConstants.defaultTaxRate when no explicit rate is set
  • family -- returns the first valid Family from the families list
  • last -- follows the next replacement chain to the definitive place product

Catalog behavior: Products can come from a shared catalog template. When IsCatalog is set to false, it cascades down to all children, transitioning them from CT (catalog) to AC (active) status.

Relationships:

DirectionTargetAccessorChildWhat it means
refPLACEplaceNoThe place this product belongs to
refPRODUCTparentYesParent product in a hierarchy
refPRODUCTnextNoActive product that replaced this catalog template
childCATEGORYcategoriesYesOption categories for this product
childPRESELECTselectsYesPreset option combinations
childFAMILYfamiliesYesProduct family variants
childPRODUCTPRICEpricesYesOptional price overrides by price zone / period

Ticket price resolution: ProductView.tableprice(place, qrcode, at?) applies zone/period overrides from product.prices within the effective zone of the table (or default zone if none): active place period → custom-zone default → product.price. No cross-zone fallback (RN-PZ-12). Consumed by TicketProductView.__prodtprice. Same lookup priority as ProductOptView.tableprice (see ProductOpt below), each with its own independent implementation — no shared helper (RN-PZ-14). Full rules: upp-model.mdc RF-PZ-07 / RN-PZ-11 / RN-PZ-14.


ProductPrice

Table: PRODUCTPRICE | Default status: AC

Optional override of a product's price for one zone (or the default zone via pricezone = null), either for the whole custom zone (priceperiod = null) or for a specific place PRICEPERIOD. The default zone never has a priceperiod = null row. Create/update/delete from the product editor (GetPriceFor / SetPriceFor); the model owns the relation and setters.

Key fields:

FieldTypeDescription
statusstringAC = active, DE = deleted
pricenumberOverride amount for this product + zone (+ optional period)
pricezoneobjidPRICEZONE or null for the default zone
priceperiodobjidPlace PRICEPERIOD or null for a custom zone's default override

Relationships:

DirectionTargetAccessorChild
refPRODUCTproductNo
refPRICEZONEpricezoneNo (nullable)
refPRICEPERIODpriceperiodNo

Category

Table: CATEGORY | Default status: AC

A Category groups related options for a product. For example, a burger might have categories "Size" (with options Small/Medium/Large) and "Extras" (with options Cheese/Bacon/Egg).

Key fields:

FieldTypeDescription
namestringCategory name (e.g. "Size", "Toppings")
sortnumberDisplay order
typestringCategory type
fxminnumberMinimum number of selections required
fxmaxnumberMaximum number of selections allowed

The fxmin/fxmax fields enforce selection constraints. For example, fxmin: 1, fxmax: 1 means the customer must pick exactly one option.

Relationships:

DirectionTargetAccessorChildWhat it means
refPRODUCTproductNoThe product this category belongs to
childPRODUCTOPToptionsYesThe options within this category
childCATEGORYDEPdependsYesDependencies on other categories

ProductOpt

Table: PRODUCTOPT | Default status: AC

A ProductOpt (product option) is a selectable customization within a category. For example, "Large" in a "Size" category, or "Extra cheese" in a "Toppings" category.

Key fields:

FieldTypeDescription
namestringOption name
pricenumberAdditional price for this option
descriptionstringDescription
sortnumberDisplay order within the category
flagsstringOption flags
ctgrefstringCatalog reference (volatile -- not persisted)

Relationships:

DirectionTargetAccessorChild
refPRODUCTproductNo
refCATEGORYcategoryNo
childOPTIONPRICEpricesYes

Ticket price resolution: ProductOptView.tableprice(place, qrcode, at?) applies zone/period overrides from productopt.prices with the same priority as ProductView.tableprice (active place period in the effective zone → custom-zone default → productopt.price), with its own independent implementation (no shared helper). Consumed, with the ticket's real place/table, by TicketProductView.__optnsprice/__finalprice, TicketOfferView.__offerprice and OfferView.SavingAmount; reference reads without a table context (catalog, shortcuts, category minimum, menu print, option editor) keep reading the plain productopt.price. Full rules: upp-model.mdc RF-PZ-08 / RN-PZ-15.


OptionPrice

Table: OPTIONPRICE | Default status: AC

Optional override of a product option's price for one zone (or the default zone via pricezone = null), either for the whole custom zone (priceperiod = null) or for a specific place PRICEPERIOD. Same contract as ProductPrice, applied to a ProductOpt instead of a Product.

Key fields:

FieldTypeDescription
statusstringAC = active, DE = deleted
pricenumberOverride amount for this product option + zone (+ optional period)
pricezoneobjidPRICEZONE or null for the default zone
priceperiodobjidPlace PRICEPERIOD or null for a custom zone's default override

Relationships:

DirectionTargetAccessorChild
refPRODUCTOPTproductoptNo
refPRICEZONEpricezoneNo (nullable)
refPRICEPERIODpriceperiodNo

Preselect

Table: PRESELECT | Default status: AC

A Preselect represents a preset combination of options for a product. Instead of choosing options one by one, the customer can select a preselect that applies a predefined set of choices. Think of it as a "variant" or "combo" of a product.

Key fields:

FieldTypeDescription
namestringPreselect name (e.g. "Classic Combo")
photofilePhoto
descriptionstringDescription
codestringShort internal code for manual lookup (same semantics as PRODUCT.code; independent of barcode)
barcodestringBarcode string for HID scanner assignment and cart resolution when the shortcut carries its own barcode
sortnumberDisplay order

Computed properties:

  • IsMarket, IsWeighted -- delegated from the parent product

Relationships:

DirectionTargetAccessorChild
refPRODUCTproductNo
childPRESELECTOPToptionsYes

Family, FamilyProduct, FamilyPeriod

The Family system allows a product to have different variants or to be available only during certain time periods.

Family (FAMILY):

FieldTypeDescription
alldaybooleanWhether the family is available all day
DirectionTargetAccessorChild
refPRODUCTproductNo
childFAMILYPRODUCTproductsYes
childFAMILYPERIODperiodsYes

FamilyProduct (FAMILYPRODUCT) -- links a family to specific products and preselects:

FieldTypeDescription
sortnumberDisplay order

Its IsValid getter checks both product.IsValid and preselect.IsValid.

DirectionTargetAccessorChild
refFAMILYfamilyNo
refPRESELECTpreselectNo
refPRODUCTproductNo

FamilyPeriod (FAMILYPERIOD) -- defines when a family is available:

FieldTypeDescription
weekdaysstringBitmask of active weekdays
inistringStart time
endstringEnd time
timezonestringTimezone for the period

Pricing Objects

Offer

Table: OFFER | Default status: AC

An Offer is a special price or menu deal for a place. It can bundle multiple products at a set price and be restricted to certain time periods.

Key fields:

FieldTypeDescription
nameobjid → I18NVALUEOffer name (e.g. "Lunch Menu")
descriptionobjid → I18NVALUEDescription
photofilePhoto
pricenumberOffer price
ismenubooleanWhether this is a structured menu (with categories like starters, mains, desserts)
alldaybooleanWhether the offer is available all day

Relationships:

DirectionTargetAccessorChildWhat it means
refPLACEplaceNoOwning place
childOFFERPRODUCTproductsYesProducts included in the offer
childOFFERPERIODperiodsYesTime periods when the offer is active

OfferProduct (OFFERPRODUCT) -- a product within an offer:

FieldTypeDescription
groupednumberGrouping count (for menus: how many of this item are included)
menuctgobjid → I18NVALUEMenu category label (e.g. "Starters", "Mains")

OfferOption (OFFERPRODUCTOPT) -- preselected options for an offer product:

DirectionTargetAccessor
refOFFERPRODUCTproduct
refPRODUCTOPToption

Note: prodopt is aliased to option for cleaner access.

OfferPeriod (OFFERPERIOD) -- availability windows (same structure as FamilyPeriod).


Extra

Table: EXTRA | Default status: AC

An Extra is a surcharge or additional service that can be applied to orders. Examples: table service charge, delivery fee, rush order premium.

Key fields:

FieldTypeDescription
nameobjid → I18NVALUEExtra name
descriptionobjid → I18NVALUEDescription
photofilePhoto
typestringExtra type
pricenumberPrice
alldaybooleanAvailable all day
allprdbooleanApplies to all products (vs. specific product selection)

Relationships:

DirectionTargetAccessorChildWhat it means
refPLACEplaceNoOwning place
childEXTRATABLEqrcodesYesSpecific tables this extra applies to
childEXTRAPRODUCTproductsYesSpecific products this extra applies to
childEXTRAPERIODperiodsYesTime periods when the extra is active

ExtraProduct (EXTRAPRODUCT), ExtraPeriod (EXTRAPERIOD), ExtraTable (EXTRATABLE), and ExtraOption (EXTRAPRODUCTOPT) follow the same pattern as the Offer sub-objects.


Discount

Table: DISCOUNT | Default status: AC

A Discount defines a price reduction that can be applied to tickets. It can target all products or specific ones, and be restricted to time windows.

Key fields:

FieldTypeDescription
nameobjid → I18NVALUEDiscount name
descriptionobjid → I18NVALUEDescription
typestringDiscount type (percentage, fixed amount, etc.)
valuenumberDiscount value (percentage or fixed amount depending on type)
alldaybooleanAvailable all day
allprdbooleanApplies to all products
cumulativebooleanWhether this discount can be combined with other discounts

Relationships:

DirectionTargetAccessorChild
refPLACEplaceNo
childDISCOUNTPRODUCTproductsYes
childDISCOUNTPERIODperiodsYes

DiscountProduct (DISCOUNTPRODUCT) -- IsValid also checks that the linked product.IsValid.

DiscountPeriod (DISCOUNTPERIOD) -- same time-window structure as other periods.


PayAccount

Table: PAYACCOUNT | Default status: AC

A PayAccount represents a customer payment account at a place. Tickets can be charged to an account (payment = PAYACCOUNT) and consolidated invoices are issued against it.

Key fields:

FieldTypeDescription
placeobjidOwning place
statusstringAC = active, BL = blocked, DE = logically deleted
namestringAccount display name
clientobjid → INVOICEBUSINESSOptional fiscal client linked to the account

Domain helpers:

  • IsValidtrue when status != 'DE'
  • IsBlockedtrue when status == 'BL'
  • business, taxid, address — convenience getters from the linked client (INVOICEBUSINESS)

Relationships:

DirectionTargetAccessorChildWhat it means
refPLACEplaceNoOwning place
refINVOICEBUSINESSclientNoFiscal client for invoicing
childACCOUNTINVOICEinvoicesYesConsolidated account invoices

Use AddInvoice / DelInvoice to manage ACCOUNTINVOICE children. PayAccountView exposes the same validity flags and fiscal convenience getters for UI layers.

Offline behaviour (feature/account): when viewService.IsOnline is false, the UI does not show reliable pending amounts or remote ticket history; settlement, proforma, TPV charge-to-account, and account deletion require connectivity. Block/unblock and read-only account metadata remain available. The operational panel shows @place_reports_offline; accountService.Tickets / Pending / Settled return [] and do not call place/ticketlst (RN-PAC-12, RV-PAC-04). Full contract: libs/upp-data/src/modules/model/views/payaccount.mdc and libs/feature/account/upp-feature-account.mdc §3.1 (RN-ACC-OFFLINE-*, RV-ACC-OFFLINE-*, QA checklist §7).

Ticket list API (feature/account): components bind AccountInfo via accountService.bind(payAccountView) and call Tickets(), Pending(), Settled(); dirty reflects shared cache invalidation. Rows are filtered/materialized from ticketService.Query only (RN-TLC-18); see place ticket list and payaccount.mdc.


AccountInvoice

Table: ACCOUNTINVOICE

An AccountInvoice is the consolidated summary invoice issued when a PayAccount is settled. It covers a period of tickets (since / until), carries serie A numbering, and may have fiscal children (TICKETBAI, VERIFACTU).

Key fields:

FieldTypeDescription
accountobjid → PAYACCOUNTParent payment account
since / untildatePeriod covered by the summary invoice
series / invoicestringRecapitulative numbering (serie A)
amountnumberTotal settled amount
paymentstringSettlement method (PAYCASH / PAYCARD; legacy CASHPAY / CARDPAY normalized on read)

Domain helpers:

  • SetInvoiceNumber() — assigns serie A and the next device counter via PlaceView.NextDeviceTicket(SerialType.A, InvoiceType.A)
  • SplitTaxes — returns a PriceInfo aggregating each ticket's frozen ticketdata.Registry (commercial.totals.taxes)
  • AddTicket / tickets — in-memory ticket list for fiscal XML (persistence uses ticket.invceac)
  • AddTicketBAI / AddVeriFactu — fiscal stubs (implemented in later fiscal tasks)

Relationships:

DirectionTargetAccessorChildWhat it means
refPAYACCOUNTaccountNoParent account
childTICKETBAIticketbaiYesTicketBAI recapitulative payload
childVERIFACTUverifactuYesVeriFactu recapitulative payload

Tickets included in the summary are linked through TICKET.invceacACCOUNTINVOICE. AccountInvoiceView enriches account as PayAccountView; other fields delegate through the object proxy.


Ticket Objects

The ticket domain is the transactional heart of the system. A Ticket represents a customer order, and it contains products, offers, extras, discounts, payment information, and audit history.

Ticket

Table: TICKET | Default status: PR (Preparing)

A Ticket is a customer order at a specific table in a place. It progresses through a lifecycle from preparation to payment.

Key fields:

FieldTypeDescription
statusstringLifecycle state (see below)
pricenumberComputed total price
refundnumberTotal refunded amount (default: 0)
taxesnumberComputed tax amount
ordernostringHuman-readable order number
seriesstringInvoice series (default: 'S00000000')
invcenostringInvoice number
elapsednumberTime elapsed since order creation
commentsstringOrder comments/notes
paymentstringPayment method used
givennumberAmount given by the customer (for cash change calculation)
flagsstringState flags bitmask
tmcommitdateTimestamp when the ticket was committed
paidondatePayment timestamp
tillobjidLogical cash register (TILL) where income is assigned (null until payment/settlement)
auditobjidCommitted till close (AUDIT) that includes this ticket (null while period is open)
createddateCreation timestamp (volatile)
updateddateLast update timestamp (volatile)

Ticket lifecycle:

StatusMeaning
PRPreparing -- the order is being built
RDReady -- the order is ready to be served/picked up
PDPaid -- the order has been paid
CCCancelled
DEDeleted

When status is set to RD, the ReadyFlag is automatically set to true.

Payment types (TicketPayment):

ValueMeaning
PAYCASHCash payment
PAYCARDCard payment
PAYLINEOnline payment
PAYMIXEDMixed payment (part cash, part card, etc.)
PAYACCOUNTPayment to a customer account

The payment setter includes backwards compatibility mapping for legacy values (CASHPAY -> PAYCASH, CARDPAY -> PAYCARD, etc.).

Flag system:

PositionFlagDescription
0PrintedFlagThe order has been sent to the printer
1NotifiedFlagThe "order ready" notification was sent to the customer
2ReadyFlagThe order is ready for pickup/service
3ToPayFlagPayment has been requested
4PaidFlagThe order has been paid
5ReopenedFlagThe ticket was reopened after payment

Computed properties:

  • invoice -- returns the first valid TicketInvoice from the invoices list
  • IsCancelled -- true when status is CC or DE
  • IsDeleted -- true when status is DE

Relationships:

DirectionTargetAccessorChildWhat it means
refSESSIONsessionNoSession that created the order
refPLACEplaceNoPlace where the order was made
refQRCODEqrcodeNoTable where the order was placed
refSESSIONpaidbyNoSession that requests payment (pending collection)
refSESSIONpaidinNoSession that executes payment with real income
refTILLtillNoLogical cash register (TILL) where income is assigned
refAUDITauditNoCommitted till close that includes this ticket
refPAYACCOUNTaccountYesCustomer account used for payment
refACCOUNTINVOICEinvceacYesAccount invoice
childTICKETPRODUCTproductsYesOrdered products
childTICKETOFFERoffersYesApplied offers
childTICKETEXTRAextrasYesApplied extras/surcharges
childTICKETDISCOUNTdiscountsYesApplied discounts
childTICKETMIXEDPAYmixedYesSplit payment entries
childASKWAITERrequestsYesWaiter call requests
childTICKETCHANGEchangesYesChange history (audit trail)
childTICKETINVOICEinvoicesYesGenerated invoices

TicketProduct

Table: TICKETPRODUCT | Default status: AC

Represents a specific product line item on a ticket.

Key fields:

FieldTypeDescription
amountnumberQuantity ordered. Setting to 0 automatically sets status = 'DE'
pricenumberUnit price at the time of order
chargenumberTotal charge (amount * price + modifiers)
fixedbooleanWhether the price is fixed (vs. market price entered manually)
refundnumberRefunded amount for this line
taxratenumberTax rate applied
marketnumberMarket price (for IsMarket products)
weightnumberWeight (for IsWeighted products)
commentsstringLine-item comments (e.g. "no onion")
sortnumberDisplay order
flagsstringEvent history bitstring (AC, RD, PP, PD, CC, DE)
addlinesnumberNet units added to the cart on this line (cumulative; supports decimals, e.g. 0.5 for half portions)
dellinesnumberNet units removed from the cart on this line (cumulative; same decimal precision as amount)

Origin contract: callers (UI, keyboard, DelOneOf, sync) must supply quantities already normalized to hundredths; AddLines/DelLines/SetLines do not re-round in the model.

Custom behavior:

  • Cart quantity changes from user actions must go through AddLines, DelLines, SetLines, or TicketView.SetAmount (delegates to SetLines). Assigning amount directly does not update addlines/dellines.
  • TicketView.DelOneOf removes min(1, amount) via DelLines (a 0.5 line counts 0.5 in dellines, not 1).
  • Cancellation/return (status = 'CC'DE) increments dellines by the current amount on both paid and unpaid tickets; paid closed lines may also set refund.
  • Moving a line between tickets (OnMerge, OnSplit), internal split (Remove), grouping merge, and recycle do not change addlines/dellines (net zero in the global cart).
  • Editing a cart line (cart OnUpdate: decrease source addlines, set amount = 0, then ToCart) transfers addlines without changing ticket-wide addlines/dellines totals.
  • TicketView.lineActivity exposes ticket-wide { addlines, dellines } with amount and income (cached; invalidated on TICKET / TICKETPRODUCT refresh).
  • TicketData.Refresh snapshots line-counter totals into registry.commercial.addlines and registry.commercial.dellines as { amount, income } (income from final unit charge; zeroed when the ticket is cancelled). The server projects them into TICKETDATA.report.activity on sync.
  • Setting amount = 0 triggers status = 'DE'
  • Setting sort triggers a refresh on the parent ticket
  • IsValid excludes status RC (recycled) and DE without refund; DE with refund > 0 stays valid for return history

Relationships:

DirectionTargetAccessorChild
refTICKETticketNo
refPRODUCTproductNo
refTICKETOFFERofferYes
childTICKETOPTIONoptionsYes

TicketOffer

Table: TICKETOFFER | Default status: AC

When a customer orders an offer/menu, a TicketOffer is created, and its TicketProduct children represent the individual items in the offer.

Key fields:

FieldTypeDescription
amountnumberQuantity
pricenumberOffer price
chargenumberTotal charge
fixedbooleanFixed price flag
refundnumberRefund amount

Relationships:

DirectionTargetAccessorChild
refTICKETticketNo
refOFFERofferNo
childTICKETPRODUCTproductsYes

TicketExtra, TicketDiscount, TicketOption

These objects record the extras, discounts, and options that were applied to a specific ticket or ticket product:

TicketExtra (TICKETEXTRA) -- an extra/surcharge applied to the ticket:

  • taxrate, charge, refund -- pricing at the time of application
  • References: ticket, extra

TicketDiscount (TICKETDISCOUNT) -- a discount applied to the ticket:

  • charge -- the discount amount
  • References: ticket, discount

TicketOption (TICKETOPTION) -- a product option selected for a ticket product:

  • prodopt (aliased as option) -- the ProductOpt that was selected
  • References: product (TicketProduct), option (ProductOpt)

TicketChange

Table: TICKETCHANGE

Records every significant change to a ticket for audit and compliance purposes (particularly important for Spanish fiscal regulations like TicketBAI and VeriFactu).

Key fields:

FieldTypeDescription
reasonstringChange reason code
seriesstringInvoice series at time of change
invoicestringInvoice number at time of change
totalnumberTotal amount at time of change
basenumberTax base at time of change
taxesnumberTax amount at time of change

Change reason codes:

CodeMeaning
FFirst -- initial ticket creation
PPrice changed
IInvoice created
DDestination change
CCancelled

Relationships:

DirectionTargetAccessorChild
refTICKETticketNo
refTICKETCHANGEprevNo
refSESSIONsessionNo
childVERIFACTUverifactusYes
childTICKETBAIticketbaisYes

The prev reference creates a linked list of changes, allowing you to trace the complete history of a ticket.


TicketInvoice and MixedPayment

TicketInvoice (TICKETINVOICE) -- links a ticket to a client for invoicing:

  • References: ticket, client (INVOICEBUSINESS)

MixedPayment (TICKETMIXEDPAY) -- when payment is split across methods:

  • payment -- the payment type for this portion
  • amount -- the amount paid with this method
  • References: ticket

Creating a New DataObject

Follow this step-by-step process to add a new data object to the system.

Step 1: Define the Schema

Create a new file in libs/upp-data/src/modules/model/objects/. Define a const schema with as const:

import { dataService } from '../../data';
import { DataObject, ObjectOptions } from "../base";
import { CreateObject } from "../item";

import { PlaceType } from '../model';

const _schema = {
name: "RESERVATION",
fields: [
{ name: "status", type: "string", default: "AC" },
{ name: "place", type: "objid" },
{ name: "name", type: "string" },
{ name: "date", type: "date" },
{ name: "guests", type: "number" },
{ name: "comments", type: "string" },
{ name: "phone", type: "string" },
],
relate: [
{ direction: ">", target: "PLACE", by: "place", name: "place", reason: "place", child: false, class: PlaceType },
]
} as const;

Step 2: Generate the Base Class

const _ReservationClass = CreateObject(_schema);
if (!_ReservationClass) {
throw new Error("Failed to create BaseClass for '" + _schema.name + "'");
}

Step 3: Create the Concrete Class

export class Reservation extends _ReservationClass {
constructor(objid: string | null, data: dataService, objoptions: ObjectOptions | null = null) {
super(_schema, objid, data, objoptions || {});
}

Copy(store: Array<DataObject> = []): Reservation {
return this._Copy(store) as Reservation;
}

get status(): string | null {
return this['__base__status'];
}

set status(value: string) {
if (this.status == 'DE') {
return;
}
this['__base__status'] = value;
if (this.place) {
if ((this.status == 'DE') && !this.Commited) {
this.place.DelReservation(this);
} else {
this.place.DoRefresh(this.table);
}
}
}

get IsValid(): boolean {
return (!!this.status && (this.status != 'DE'));
}
}

Step 4: Register in the Object Factory

Add the new class to the ObjectFactory so the framework can instantiate it during sync:

ObjectFactory.register('RESERVATION', Reservation);

Step 5: Add the Relationship to the Parent

If Place should have a list of reservations, add a relation to the Place schema:

{ direction: "<", target: "RESERVATION", by: "place", name: "reservation", reason: "reservations", child: true, class: ReservationType }

Step 6: Export and Wire Up

Export the class from the model barrel file and create a type reference for use in other schemas.


Common Patterns

Querying Objects

Objects are accessed through relationships, not direct queries. Start from the root (Place) and navigate:

const activeProducts = place.products.filter(p => p.IsValid);

const pendingTickets = place.tickets.filter(t => t.status === 'PR');

const employeeList = place.employees.filter(e => e.IsValid);

Filtering by Status

The convention IsValid means "not deleted." For more specific checks:

const activeUsers = user.places
.filter(p => p.IsValid)
.filter(p => !p.IsExpired);

const unpaidTickets = place.tickets
.filter(t => t.IsValid && !t.IsCancelled && !t.PaidFlag);

Working with Relations

Setting a 1:1 relation:

ticket.place = myPlace;
ticket.qrcode = myTable;
ticket.session = currentSession;

Adding to a 1:N relation:

place.AddProduct(newProduct);
ticket.AddProduct(newTicketProduct);

Removing from a 1:N relation:

place.DelProduct(oldProduct);

Creating and Committing a Ticket

const ticket = new Ticket(null, dataService);
ticket.place = currentPlace;
ticket.qrcode = currentTable;
ticket.session = currentSession;

const tp = new TicketProduct(null, dataService);
tp.product = someProduct;
tp.amount = 2;
tp.price = someProduct.price;
tp.taxrate = someProduct.taxrate;
ticket.AddProduct(tp);

await ticket.DoCommit();

Editing with Copy/Overwrite

const copy = ticket.Copy();

copy.comments = "Updated comment";
for (const tp of copy.products) {
if (tp.product === someProduct) {
tp.amount = 3;
}
}

const original = copy.Overwrite();
await original.DoCommit();

Waiting for an Object to Load

When you have a reference to an object that may not have received its data from the server yet:

await product.WaitLoaded();
console.log(product.name);

Working with Flags

Both Ticket and Product use a bitmask flags field. Each has named getters/setters:

ticket.ReadyFlag = true;
if (ticket.PaidFlag) { ... }

product.HideApp = true;
if (product.IsMarket) { ... }

Subscribing to Changes

Objects emit observables you can subscribe to:

ticket.OnRefresh.subscribe(tables => {
console.log("Ticket refreshed due to changes in:", tables);
});

ticket.OnCommit.subscribe(() => {
console.log("Ticket committed successfully");
});

ticket.OnChildChanged.subscribe(relationName => {
console.log("Children changed for relation:", relationName);
});