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:
- 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.
- Validation. Field types are enforced at runtime. Setting a
numberfield to a string throws an error immediately, catching bugs early instead of corrupting the database. - Relationship management. Parent-child and reference relationships are declared once in the schema. The framework then generates typed getters, setters,
Add/Delmethods, 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 patchValue → ToUpdate → DoRefresh 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:
| Property | Type | Purpose |
|---|---|---|
name | string | The 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:
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Column name as it appears in the database |
type | string | Yes | One of the supported field types (see below) |
default | any | No | Default value assigned when the object is created locally |
alias | string | No | Alternative 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 |
volatile | boolean | No | When 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:
| Type | TypeScript Type | Stored As (Server) | Description |
|---|---|---|---|
string | string | null | VARCHAR/TEXT | Plain text value |
file | string | null | File path | A urlfile object internally ({ url, b64 }). The getter returns the URL or base64 string; the setter accepts either a URL or a data-URI |
number | number | null | DECIMAL/INT (as string) | Numeric value. Sent as a string to the server, parsed back to number on load |
date | Date | null | MySQL datetime string | JavaScript Date object. Converted to/from MySQL datetime format automatically |
boolean | boolean | null | '0' or '1' | Boolean value. Stored as a single character in the database |
objid | string | null | INT (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:
| Property | Type | Description |
|---|---|---|
direction | '>' or '<' | > means this object references another (outgoing, 1:1). < means other objects reference this one (incoming, 1:N) |
target | string | The database table name of the related object |
by | string | The 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 |
name | string | The accessor name for 1:1 relations (used as the getter/setter property name) |
reason | string | The key used to store the relationship internally. For 1:N relations, this becomes the list accessor name (e.g. products, tickets) |
child | boolean | When 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 |
class | class | Optional 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
DataObjectornull, 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
reasonname (e.g.place.productsreturns an array ofProductobjects) - 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:
- Validates the schema structure (field types, relation directions)
- Creates an abstract class extending
BaseClass<T>with the schema baked in - Defines getters/setters on the prototype for every field, with runtime type validation
- Defines relationship accessors on the prototype (1:1 getters/setters, 1:N list getters and
Add/Delmethods) - 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 insertionobjidisnull-- the server will assign a real ID during sync_uuidis auto-generated -- used as a temporary identifier until the server responds with a realobjid
Loading From the Server (Sync)
When data arrives from the server (during synchronization), the framework calls OnUpdate(info) on the object. This method:
- Checks if the object can be updated (
_CanUpdate) - Compares the incoming data against the last update to skip no-ops
- Calls
_OnUpdate(info)which deserializes database values into typed properties via the schema - Resolves or replaces the object in storage (maps the server-assigned
objidto the local_uuid) - Triggers
DoRefresh()to notify the view layer - Marks
_isLoaded = trueand completes the_waitLoadedsubject
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:
- Validates the value type against the schema
- Calls
patchValue()to check if the value actually changed - 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();
_CommitMap()walks the object and all itschild: truerelations recursively, collecting every object that has_toInsert,_toUpdate, or_toDeleteflags set- For each object, it builds a
CommitChangecontaining: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
data.Commit(changes)queues all changesdata.Flush(force)sends them to the server (immediately ifforce = 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 = truecreates an isolated context.- If no
ObjectOptions.storageis 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. Completedstores the unix timestamp from the lastreports/completeresponse for partial objects. For non-partial objects,Completedis alwaysnull.- Promotion to global is explicit (
IsPartial = false) and only allowed whenCompleted != nulland 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 allchild: truerelations, setting_copyofon each clone to point back to the originalOverwrite()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:
| Status | Meaning |
|---|---|
AC | Active -- the object is live and valid |
PA | Pending -- awaiting activation (common for new users) |
DE | Deleted -- logically deleted (soft delete) |
PR | Preparing -- used for tickets that are being built |
RD | Ready -- used for tickets that are ready to serve |
CC | Cancelled -- used for tickets that were cancelled |
CT | Catalog -- 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:
| Field | Type | Description |
|---|---|---|
status | string | PA = pending activation, AC = active, DE = deleted |
email | string | Login email address. Unique per user |
firstname | string | First name |
lastname | string | Last name |
photo | file | Profile photo (URL or base64) |
phone | string | Phone number |
lang | string | Preferred language code (e.g. "es", "en") |
license | boolean | Whether the user holds a valid license |
Computed properties:
fullname-- concatenatesfirstnameandlastnameIsActive--truewhenstatus == 'AC'IsPending--truewhenstatus == 'PA'
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| child | PLACE | places | Yes | Places owned by this user |
| child | STAFF | employees | Yes | Employee roles this user holds (one per place) |
| child | SESSION | sessions | Yes | Active 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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, DE = deleted |
id | string | A session identifier (used as the UUID override -- _uuid returns this.id) |
deviceid | string | Unique device identifier |
Computed properties:
IsActive--truewhenstatus == 'AC'andupdatedis within the last 600 seconds (10 minutes). This means sessions expire automatically when the device stops sending updates.
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | USER | user | No | The logged-in user |
| ref | QRCODE | qrcode | No | The QR code (table) the session is associated with |
| child | FCM | fcm | No | Firebase 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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, DE = deleted |
alias | string | Display name override for this place |
allow | string | Permission bitmask stored as a string of '0' and '1' characters |
email | string | Contact email (may differ from the user's main email) |
photo | file | Photo 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:
| Position | Permission | Description |
|---|---|---|
| 0 | StaffManage | Can manage employees |
| 1 | OfferManage | Can manage offers |
| 2 | ProductManage | Can manage the product catalog |
| 3 | TableManage | Can manage QR codes/tables |
| 4 | ExtraManage | Can manage extras/surcharges |
| 5 | PlaceManage | Can manage place settings |
| 6 | LicenceManage | Can manage the licence |
| 7 | CanCompliment | Can apply complimentary items |
| 8 | CanAvailable | Can set availability |
| 9 | POSManage | Can use the POS interface |
| 10 | ReportAccess | Can view reports |
| 11 | TillManage | Can manage the cash register |
| 12 | TillOpen | Can open the cash drawer |
| 13 | CanReopen | Can reopen closed tickets |
| 14 | CanModify | Can modify existing tickets |
| 15 | CanCancel | Can cancel tickets |
| 16 | CanReturn | Can process returns |
Use employee.IsAllowedTo(EmployeePermission.ProductManage) to check and employee.SetAllowTo(EmployeePermission.ProductManage, true) to grant.
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | USER | user | No | The underlying user account |
| ref | PLACE | place | No | The 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).
| Member | Purpose |
|---|---|
es / en / eu / zh | Display 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)
- Add the column to tenant schema:
ALTER TABLE I18NVALUE ADD COLUMN <LANG> TEXT DEFAULT NULL. - Update
server/src/App/Resources/model/p_schema.jsonand theI18NVALUEfields inlibs/upp-data/src/modules/model/objects/i18nvalue.ts. - 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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active |
name | string | Business name (scalar; not menu i18n) |
description | objid → I18NVALUE | Optional multilingual description for menu / QR (display via PlaceView.description or tr(row)) |
photo | file | Main photo. Falls back to a generated avatar from the name |
logo | file | Business logo |
expires | date | License expiration date |
business | string | Type of business |
company | boolean | Whether it is a registered company |
activity | string | Business activity code |
taxid | string | Tax identification number (CIF/NIF) |
phone | string | Contact phone |
Computed properties:
avatar-- generates a placeholder image URL from the place namephoto-- returns the uploaded photo, or falls back to the avatarlang-- default language for menu texts on printed tickets (optionLanguage, fallback'ES')IsValid--truewhenstatus == 'AC'IsExpired--truewhenexpiresis in the pastInitialize()-- emits onOnInitializeto signal that the place has been fully loaded for the first time
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | USER | user | No | The owner of this place |
| ref | ADDRESS | address | Yes | Physical address (geocoded) |
| ref | ADDRESS | fiscal | Yes | Fiscal/billing address |
| ref | I18NVALUE | description | Yes | Optional place description for menu / QR |
| child | QRCODE | qrcodes | Yes | Tables/QR codes |
| child | PRODUCT | products | Yes | Product catalog |
| child | OFFER | offers | Yes | Active offers/menus |
| child | EXTRA | extras | Yes | Surcharges |
| child | DISCOUNT | discounts | Yes | Discounts |
| child | STAFF | employees | Yes | Employees |
| child | TICKET | tickets | Yes | All tickets (orders) |
| child | PRICEZONE | pricezones | Yes | Price-adjustment zones (mesas via QRCODE.pricezone) |
| child | PRICEPERIOD | priceperiods | Yes | Time-of-day price periods shared by all zones |
| child | STRIPE | stripes | Yes | Payment provider accounts |
| child | PAYACCOUNT | accounts | Yes | Customer payment accounts |
| child | RASPPI | services | Yes | Connected hardware services |
| child | PLACEOPT | options | Yes | Place configuration options |
| child | PLACELINK | links | Yes | External URLs (PLACELINK.link: e.g. homepage, social keys, google for Google Business Profile, pdfmenu, pemcert) |
| child | INVOICEBUSINESS | clients | Yes | Invoice clients |
| child | AUDIT | audits | Yes | Audit trail entries |
| child | PAYMENT | transactions | Yes | Payment 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:
| Field | Type | Description |
|---|---|---|
place | objid | Parent place |
status | string | AC = active, DE = deleted |
id | string | Service identifier/name |
ping | date | Last seen/ping time |
version | string | Installed service version |
RASPPI relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | PLACE | place | No | The place that owns this service |
| child | RASPPICONNECT | connects | Yes | Device-specific connection entries |
| child | PRINTER | printers | Yes | Receipt printers attached to this service |
| child | SCALE | scales | Yes | Weighing scales attached to this service |
| child | CODESCAN | codescans | Yes | USB HID barcode scanners configured for this TPV service |
RASPPICONNECT key fields:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, DE = deleted |
rasppi | objid | Parent service (RASPPI) |
device | string | Unique client device identifier |
connect | string | Remote URI (host:port) when remote mode is enabled |
hassvc | boolean | Whether 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.till → Till, 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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, DE = deleted (logical, not reactivated) |
name | string | Optional alias in multi-TPV mode; null in single-TPV mode |
rasppi | objid | Hardware 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:
| Direction | Target | Accessor | What it means |
|---|---|---|---|
| ref | RASPPI | rasppi | Hardware service for this till (0..1, RN-TILL-16) |
| child | QRCODE | qrcodes | Tables assigned to this till (multi-TPV) |
| child | TILLCHANGE | tillchanges | Pending 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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, DE = deleted (cascades: clears QRCODE.pricezone on assigned tables; does not delete place periods) |
name | objid → I18NVALUE | Multi-language zone name |
sort | number | Display 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:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | PLACE | place | No | Owning 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:
| Field | Type | Description |
|---|---|---|
status | string | AC / DE / UN (UN = temporarily disabled) |
place | objid | Owning place (PLACE child priceperiods) |
name | objid → I18NVALUE | Period label (e.g. "Morning") |
allday | boolean | If true, active without checking weekdays/time |
weekdays | string (7) | Day mask ('1' = active); ignored if allday |
ini / end | string HH:MM | Time range (no midnight crossing); ignored if allday |
timezone | string | Same pattern as OFFERPERIOD |
sort | number | Overlap priority — lower wins (first in list) |
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | PLACE | place | No | Owning 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:
| Field | Type | Description |
|---|---|---|
status | string | PR = open-period draft (client only); AC = committed close |
till | objid | Cash register (TILL) this close belongs to (RN-CAJA-42) |
ini / end | date | Close window bounds; end is set at commit |
cash | number | Net sum of TILLCHANGE tipo CASH (or unset) linked at close (RN-AUD-05) |
card | number | Net sum of TILLCHANGE tipo CARD linked at close (RN-AUD-05) |
sequence | number | Close number ≥ 1 per place; volatile — assigned atomically on server INSERT (RN-AUD-06); null until sync after commit |
report | string | JSON 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:
| Direction | Target | Accessor | What it means |
|---|---|---|---|
| ref | PLACE | place | Owning place |
| ref | SESSION | session | Session that performed the close |
| ref | TILL | till | Cash register for this close |
| child | TILLCHANGE | tillchanges | Cash/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:
| Field | Type | Description |
|---|---|---|
type | string | 'CASH' (default) or 'CARD' — adjustment bucket (RN-AUD-16) |
amount | number | Signed amount (positive = in, negative = out) |
concept | string | Operator note |
till | objid | Cash register where the movement was recorded (RN-AUD-03) |
audit | objid | Committed close that includes this movement (null while period is open) |
Relationships:
| Direction | Target | Accessor | What it means |
|---|---|---|---|
| ref | PLACE | place | Owning place |
| ref | SESSION | session | Session that recorded the movement |
| ref | TILL | till | Logical cash register for this in/out |
| ref | AUDIT | audit | Committed 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.
| API | Use |
|---|---|
LoadReport() | Aggregates tickets for the open period (PR only); returns AuditInfoReport with schemaVersion = 0; sets ini/end on pending audit |
Report | PR: lazy-load online → cache; offline best-effort. AC: delegates to Audit.Report sync |
LoadTickets() | Ticket rows for the period window |
sequence | Proxy to server-assigned close number |
isLoading | Set 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:
| Field | Type | Description |
|---|---|---|
formatted | string | Full formatted address string (e.g. "Calle Mayor 5, 28001 Madrid") |
door | string | Door/apartment number |
latitude | number | GPS latitude |
longitude | number | GPS longitude |
accuracy | number | Location accuracy in meters |
timezone | string | IANA timezone (defaults to 'UTC') |
street | string | Street name |
number | string | Street number |
zipcode | string | Postal code |
town | string | City/town |
province | string | Province/state |
country | string | Country |
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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, PA = pending setup |
alias | string | Human-readable name (e.g. "Table 5", "Bar") |
number | number | Table number |
chairs | number | Number of diners / table capacity; DB default 1. New tables created from the app use 4 unless another value is supplied. |
till | objid | TILL logical cash register assigned to this table |
pricezone | objid | PRICEZONE price-adjustment zone; null = default zone (base PRODUCT.price; period overrides still apply) |
Computed properties:
IsPending--truewhenstatus == 'PA'QrCodeUrl(language)-- builds the guest access URL using linkedQRSCAN(authorization+objid)
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | PLACE | place | No | The place this table belongs to |
| ref | QRSCAN | qrscan | No | Global scan/access row linked to this table |
| ref | FLOORAREA | area | Yes | The area/zone of the place (e.g. terrace, indoor) |
| ref | DRAWITEM | draw | Yes | Visual representation on a floor plan |
| ref | TILL | till | No | Logical cash register assigned to this table |
| ref | PRICEZONE | pricezone | No | Price-adjustment zone (null = default zone) |
| child | ASKWAITER | requests | Yes | Waiter call requests from this table |
| child | EXTRATABLE | extras | Yes | Extras/surcharges specific to this table |
| child | TICKET | tickets | Yes | Active 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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, DE = deleted |
name | string | Human readable area name |
photo | file | Optional image associated with the area (URL or base64) |
coord_w, coord_h | number | Area width/height in cm |
Computed properties:
IsValid--truewhen the row has a non-deleted status (statuspresent and not'DE').
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| child | DRAWITEM | draws | Yes | Geometric items rendered on the floor plan |
| child | QRCODE | qrcodes | Yes | Tables 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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, DE = deleted |
type | string | Item type discriminator |
area | objid | Parent floor area (FLOORAREA) |
shape | string | Shape kind identifier |
custom | string | Optional serialized custom data |
sort | number | Z-order / sort index (default 0) |
coord_x, coord_y | number | Position |
coord_w, coord_h | number | Width and height |
coord_r | number | Rotation |
Computed properties:
IsValid--truewhen the row has a non-deleted status (statuspresent and not'DE').
Runtime-only (not persisted, not synced):
konva-- getter/setter typed asany. Holds the running Konva node (or group/shape) instance in aWeakRefso it does not keep the object alive and is never written to the database. Assignnullto clear. If the shape has been garbage-collected, the getter returnsnull.
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | FLOORAREA | area | No | Floor area this item belongs to |
| child | QRCODE | qrcodes | Yes | QR 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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active |
authorization | string | Auth token for the scan session |
qrcode | objid | The QR code that was scanned |
number | number | Table number (copied from QrCode) |
alias | string | Table alias (copied from QrCode) |
Relationships:
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | PLACE | place | No |
PrintDevice
Table: PRINTER | Default status: AC
Represents a physical printer connected to the system (typically a thermal receipt printer).
Key fields:
| Field | Type | Description |
|---|---|---|
name | string | Printer display name |
device | string | Device path or address |
type | string | Printer type/protocol |
mt, mr, mb, ml | number | Margins in millimeters (top, right, bottom, left) |
width | number | Paper width in millimeters |
font | number | Font size |
copies | number | Number of copies to print (default: 1) |
updates | boolean | Print order updates |
posudts | boolean | Print POS updates |
oncash | boolean | Print on cash payment |
oncard | boolean | Print on card payment |
bold | boolean | Use bold font |
style | string | Print 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:
| Field | Type | Description |
|---|---|---|
rasppi | objid | Parent RASPPI service |
status | string | AC = active, DE = deleted |
name | string | Optional 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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, DE = deleted, CT = catalog template |
name | string | Display name |
description | string | Description shown to customers |
photo | file | Product image. Falls back to a generated avatar |
price | number | Base price (tax-exclusive) |
taxrate | number | Tax rate percentage. Defaults to AppConstants.defaultTaxRate if not set |
code | string | Short internal product code for manual lookup when optionProductCodes is enabled (numeric keypad search in cart). Not the barcode scanned by the HID reader |
barcode | string | Barcode string (EAN-13, UPC, GS1-128, etc.) used by the USB HID scanner and cart resolution via PlaceView.doBarCode |
ctgref | string | Catalog reference -- links to a product template in the global catalog |
type | string | Product type classifier |
sort | number | Sort order within its parent or place |
isgroup | boolean | Whether this product is a visual group/header (not orderable itself) |
generic | boolean | Whether this is a generic/customizable product |
isfamily | boolean | Whether this product has family variants |
flags | string | Bitmask for visibility and pricing flags |
Flag system: Similar to the Employee permission bitmask:
| Position | Flag | Description |
|---|---|---|
| 0 | HideApp | Hide from the customer-facing QR app |
| 1 | HidePdf | Hide from the PDF menu |
| 2 | HideWeb | Hide from the web menu |
| 3 | MarketPrice (IsMarket) | Product does not have a fixed price (price entered at sale time) |
| 4 | Weighted (IsWeighted) | Product is sold by weight |
Computed properties:
avatar/photo-- fallback photo generation from the product nametaxrate-- returnsAppConstants.defaultTaxRatewhen no explicit rate is setfamily-- returns the first validFamilyfrom thefamilieslistlast-- follows thenextreplacement 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:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | PLACE | place | No | The place this product belongs to |
| ref | PRODUCT | parent | Yes | Parent product in a hierarchy |
| ref | PRODUCT | next | No | Active product that replaced this catalog template |
| child | CATEGORY | categories | Yes | Option categories for this product |
| child | PRESELECT | selects | Yes | Preset option combinations |
| child | FAMILY | families | Yes | Product family variants |
| child | PRODUCTPRICE | prices | Yes | Optional 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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, DE = deleted |
price | number | Override amount for this product + zone (+ optional period) |
pricezone | objid | PRICEZONE or null for the default zone |
priceperiod | objid | Place PRICEPERIOD or null for a custom zone's default override |
Relationships:
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | PRODUCT | product | No |
| ref | PRICEZONE | pricezone | No (nullable) |
| ref | PRICEPERIOD | priceperiod | No |
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:
| Field | Type | Description |
|---|---|---|
name | string | Category name (e.g. "Size", "Toppings") |
sort | number | Display order |
type | string | Category type |
fxmin | number | Minimum number of selections required |
fxmax | number | Maximum 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:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | PRODUCT | product | No | The product this category belongs to |
| child | PRODUCTOPT | options | Yes | The options within this category |
| child | CATEGORYDEP | depends | Yes | Dependencies 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:
| Field | Type | Description |
|---|---|---|
name | string | Option name |
price | number | Additional price for this option |
description | string | Description |
sort | number | Display order within the category |
flags | string | Option flags |
ctgref | string | Catalog reference (volatile -- not persisted) |
Relationships:
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | PRODUCT | product | No |
| ref | CATEGORY | category | No |
| child | OPTIONPRICE | prices | Yes |
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:
| Field | Type | Description |
|---|---|---|
status | string | AC = active, DE = deleted |
price | number | Override amount for this product option + zone (+ optional period) |
pricezone | objid | PRICEZONE or null for the default zone |
priceperiod | objid | Place PRICEPERIOD or null for a custom zone's default override |
Relationships:
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | PRODUCTOPT | productopt | No |
| ref | PRICEZONE | pricezone | No (nullable) |
| ref | PRICEPERIOD | priceperiod | No |
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:
| Field | Type | Description |
|---|---|---|
name | string | Preselect name (e.g. "Classic Combo") |
photo | file | Photo |
description | string | Description |
code | string | Short internal code for manual lookup (same semantics as PRODUCT.code; independent of barcode) |
barcode | string | Barcode string for HID scanner assignment and cart resolution when the shortcut carries its own barcode |
sort | number | Display order |
Computed properties:
IsMarket,IsWeighted-- delegated from the parentproduct
Relationships:
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | PRODUCT | product | No |
| child | PRESELECTOPT | options | Yes |
Family, FamilyProduct, FamilyPeriod
The Family system allows a product to have different variants or to be available only during certain time periods.
Family (FAMILY):
| Field | Type | Description |
|---|---|---|
allday | boolean | Whether the family is available all day |
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | PRODUCT | product | No |
| child | FAMILYPRODUCT | products | Yes |
| child | FAMILYPERIOD | periods | Yes |
FamilyProduct (FAMILYPRODUCT) -- links a family to specific products and preselects:
| Field | Type | Description |
|---|---|---|
sort | number | Display order |
Its IsValid getter checks both product.IsValid and preselect.IsValid.
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | FAMILY | family | No |
| ref | PRESELECT | preselect | No |
| ref | PRODUCT | product | No |
FamilyPeriod (FAMILYPERIOD) -- defines when a family is available:
| Field | Type | Description |
|---|---|---|
weekdays | string | Bitmask of active weekdays |
ini | string | Start time |
end | string | End time |
timezone | string | Timezone 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:
| Field | Type | Description |
|---|---|---|
name | objid → I18NVALUE | Offer name (e.g. "Lunch Menu") |
description | objid → I18NVALUE | Description |
photo | file | Photo |
price | number | Offer price |
ismenu | boolean | Whether this is a structured menu (with categories like starters, mains, desserts) |
allday | boolean | Whether the offer is available all day |
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | PLACE | place | No | Owning place |
| child | OFFERPRODUCT | products | Yes | Products included in the offer |
| child | OFFERPERIOD | periods | Yes | Time periods when the offer is active |
OfferProduct (OFFERPRODUCT) -- a product within an offer:
| Field | Type | Description |
|---|---|---|
grouped | number | Grouping count (for menus: how many of this item are included) |
menuctg | objid → I18NVALUE | Menu category label (e.g. "Starters", "Mains") |
OfferOption (OFFERPRODUCTOPT) -- preselected options for an offer product:
| Direction | Target | Accessor |
|---|---|---|
| ref | OFFERPRODUCT | product |
| ref | PRODUCTOPT | option |
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:
| Field | Type | Description |
|---|---|---|
name | objid → I18NVALUE | Extra name |
description | objid → I18NVALUE | Description |
photo | file | Photo |
type | string | Extra type |
price | number | Price |
allday | boolean | Available all day |
allprd | boolean | Applies to all products (vs. specific product selection) |
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | PLACE | place | No | Owning place |
| child | EXTRATABLE | qrcodes | Yes | Specific tables this extra applies to |
| child | EXTRAPRODUCT | products | Yes | Specific products this extra applies to |
| child | EXTRAPERIOD | periods | Yes | Time 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:
| Field | Type | Description |
|---|---|---|
name | objid → I18NVALUE | Discount name |
description | objid → I18NVALUE | Description |
type | string | Discount type (percentage, fixed amount, etc.) |
value | number | Discount value (percentage or fixed amount depending on type) |
allday | boolean | Available all day |
allprd | boolean | Applies to all products |
cumulative | boolean | Whether this discount can be combined with other discounts |
Relationships:
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | PLACE | place | No |
| child | DISCOUNTPRODUCT | products | Yes |
| child | DISCOUNTPERIOD | periods | Yes |
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:
| Field | Type | Description |
|---|---|---|
place | objid | Owning place |
status | string | AC = active, BL = blocked, DE = logically deleted |
name | string | Account display name |
client | objid → INVOICEBUSINESS | Optional fiscal client linked to the account |
Domain helpers:
IsValid—truewhenstatus != 'DE'IsBlocked—truewhenstatus == 'BL'business,taxid,address— convenience getters from the linkedclient(INVOICEBUSINESS)
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | PLACE | place | No | Owning place |
| ref | INVOICEBUSINESS | client | No | Fiscal client for invoicing |
| child | ACCOUNTINVOICE | invoices | Yes | Consolidated 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:
| Field | Type | Description |
|---|---|---|
account | objid → PAYACCOUNT | Parent payment account |
since / until | date | Period covered by the summary invoice |
series / invoice | string | Recapitulative numbering (serie A) |
amount | number | Total settled amount |
payment | string | Settlement method (PAYCASH / PAYCARD; legacy CASHPAY / CARDPAY normalized on read) |
Domain helpers:
SetInvoiceNumber()— assigns serie A and the next device counter viaPlaceView.NextDeviceTicket(SerialType.A, InvoiceType.A)SplitTaxes— returns aPriceInfoaggregating each ticket's frozenticketdata.Registry(commercial.totals.taxes)AddTicket/tickets— in-memory ticket list for fiscal XML (persistence usesticket.invceac)AddTicketBAI/AddVeriFactu— fiscal stubs (implemented in later fiscal tasks)
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | PAYACCOUNT | account | No | Parent account |
| child | TICKETBAI | ticketbai | Yes | TicketBAI recapitulative payload |
| child | VERIFACTU | verifactu | Yes | VeriFactu recapitulative payload |
Tickets included in the summary are linked through TICKET.invceac → ACCOUNTINVOICE. 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:
| Field | Type | Description |
|---|---|---|
status | string | Lifecycle state (see below) |
price | number | Computed total price |
refund | number | Total refunded amount (default: 0) |
taxes | number | Computed tax amount |
orderno | string | Human-readable order number |
series | string | Invoice series (default: 'S00000000') |
invceno | string | Invoice number |
elapsed | number | Time elapsed since order creation |
comments | string | Order comments/notes |
payment | string | Payment method used |
given | number | Amount given by the customer (for cash change calculation) |
flags | string | State flags bitmask |
tmcommit | date | Timestamp when the ticket was committed |
paidon | date | Payment timestamp |
till | objid | Logical cash register (TILL) where income is assigned (null until payment/settlement) |
audit | objid | Committed till close (AUDIT) that includes this ticket (null while period is open) |
created | date | Creation timestamp (volatile) |
updated | date | Last update timestamp (volatile) |
Ticket lifecycle:
| Status | Meaning |
|---|---|
PR | Preparing -- the order is being built |
RD | Ready -- the order is ready to be served/picked up |
PD | Paid -- the order has been paid |
CC | Cancelled |
DE | Deleted |
When status is set to RD, the ReadyFlag is automatically set to true.
Payment types (TicketPayment):
| Value | Meaning |
|---|---|
PAYCASH | Cash payment |
PAYCARD | Card payment |
PAYLINE | Online payment |
PAYMIXED | Mixed payment (part cash, part card, etc.) |
PAYACCOUNT | Payment to a customer account |
The payment setter includes backwards compatibility mapping for legacy values (CASHPAY -> PAYCASH, CARDPAY -> PAYCARD, etc.).
Flag system:
| Position | Flag | Description |
|---|---|---|
| 0 | PrintedFlag | The order has been sent to the printer |
| 1 | NotifiedFlag | The "order ready" notification was sent to the customer |
| 2 | ReadyFlag | The order is ready for pickup/service |
| 3 | ToPayFlag | Payment has been requested |
| 4 | PaidFlag | The order has been paid |
| 5 | ReopenedFlag | The ticket was reopened after payment |
Computed properties:
invoice-- returns the first validTicketInvoicefrom theinvoiceslistIsCancelled--truewhen status isCCorDEIsDeleted--truewhen status isDE
Relationships:
| Direction | Target | Accessor | Child | What it means |
|---|---|---|---|---|
| ref | SESSION | session | No | Session that created the order |
| ref | PLACE | place | No | Place where the order was made |
| ref | QRCODE | qrcode | No | Table where the order was placed |
| ref | SESSION | paidby | No | Session that requests payment (pending collection) |
| ref | SESSION | paidin | No | Session that executes payment with real income |
| ref | TILL | till | No | Logical cash register (TILL) where income is assigned |
| ref | AUDIT | audit | No | Committed till close that includes this ticket |
| ref | PAYACCOUNT | account | Yes | Customer account used for payment |
| ref | ACCOUNTINVOICE | invceac | Yes | Account invoice |
| child | TICKETPRODUCT | products | Yes | Ordered products |
| child | TICKETOFFER | offers | Yes | Applied offers |
| child | TICKETEXTRA | extras | Yes | Applied extras/surcharges |
| child | TICKETDISCOUNT | discounts | Yes | Applied discounts |
| child | TICKETMIXEDPAY | mixed | Yes | Split payment entries |
| child | ASKWAITER | requests | Yes | Waiter call requests |
| child | TICKETCHANGE | changes | Yes | Change history (audit trail) |
| child | TICKETINVOICE | invoices | Yes | Generated invoices |
TicketProduct
Table: TICKETPRODUCT | Default status: AC
Represents a specific product line item on a ticket.
Key fields:
| Field | Type | Description |
|---|---|---|
amount | number | Quantity ordered. Setting to 0 automatically sets status = 'DE' |
price | number | Unit price at the time of order |
charge | number | Total charge (amount * price + modifiers) |
fixed | boolean | Whether the price is fixed (vs. market price entered manually) |
refund | number | Refunded amount for this line |
taxrate | number | Tax rate applied |
market | number | Market price (for IsMarket products) |
weight | number | Weight (for IsWeighted products) |
comments | string | Line-item comments (e.g. "no onion") |
sort | number | Display order |
flags | string | Event history bitstring (AC, RD, PP, PD, CC, DE) |
addlines | number | Net units added to the cart on this line (cumulative; supports decimals, e.g. 0.5 for half portions) |
dellines | number | Net 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, orTicketView.SetAmount(delegates toSetLines). Assigningamountdirectly does not updateaddlines/dellines. TicketView.DelOneOfremovesmin(1, amount)viaDelLines(a0.5line counts0.5indellines, not1).- Cancellation/return (
status = 'CC'→DE) incrementsdellinesby the currentamounton both paid and unpaid tickets; paid closed lines may also setrefund. - Moving a line between tickets (
OnMerge,OnSplit), internal split (Remove), grouping merge, and recycle do not changeaddlines/dellines(net zero in the global cart). - Editing a cart line (cart
OnUpdate: decrease sourceaddlines, setamount = 0, thenToCart) transfersaddlineswithout changing ticket-wideaddlines/dellinestotals. TicketView.lineActivityexposes ticket-wide{ addlines, dellines }withamountandincome(cached; invalidated onTICKET/TICKETPRODUCTrefresh).TicketData.Refreshsnapshots line-counter totals intoregistry.commercial.addlinesandregistry.commercial.dellinesas{ amount, income }(income from final unit charge; zeroed when the ticket is cancelled). The server projects them intoTICKETDATA.report.activityon sync.- Setting
amount = 0triggersstatus = 'DE' - Setting
sorttriggers a refresh on the parent ticket IsValidexcludes statusRC(recycled) andDEwithout refund;DEwithrefund > 0stays valid for return history
Relationships:
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | TICKET | ticket | No |
| ref | PRODUCT | product | No |
| ref | TICKETOFFER | offer | Yes |
| child | TICKETOPTION | options | Yes |
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:
| Field | Type | Description |
|---|---|---|
amount | number | Quantity |
price | number | Offer price |
charge | number | Total charge |
fixed | boolean | Fixed price flag |
refund | number | Refund amount |
Relationships:
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | TICKET | ticket | No |
| ref | OFFER | offer | No |
| child | TICKETPRODUCT | products | Yes |
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 asoption) -- theProductOptthat 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:
| Field | Type | Description |
|---|---|---|
reason | string | Change reason code |
series | string | Invoice series at time of change |
invoice | string | Invoice number at time of change |
total | number | Total amount at time of change |
base | number | Tax base at time of change |
taxes | number | Tax amount at time of change |
Change reason codes:
| Code | Meaning |
|---|---|
F | First -- initial ticket creation |
P | Price changed |
I | Invoice created |
D | Destination change |
C | Cancelled |
Relationships:
| Direction | Target | Accessor | Child |
|---|---|---|---|
| ref | TICKET | ticket | No |
| ref | TICKETCHANGE | prev | No |
| ref | SESSION | session | No |
| child | VERIFACTU | verifactus | Yes |
| child | TICKETBAI | ticketbais | Yes |
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 portionamount-- 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);
});