Skip to main content

Place and Catalog

The place and catalog system in upp-data manages business entities, their product catalogs, pricing modifiers (offers, extras, discounts), and the QR code table infrastructure. The primary classes are Place (DataObject), _PlaceView (ViewObject), and the supporting Catalog and SearchTool utilities.

PlaceView​

_PlaceView extends ViewObject<Place> and is the main interface for working with a business location. It wraps the Place DataObject with computed collections, configuration accessors, and management actions.

Initialization​

The PlaceView waits for the underlying Place object's OnInitialize event before performing its own initialization. This ensures all related objects (products, offers, etc.) are loaded before the view attempts to compute its collections.

constructor(object: Place, data: dataService) {
super(object, data);
this.place.OnInitialize.pipe(take(1)).subscribe(() => {
this.Initialize();
});
}

Lazy Services​

Services are resolved lazily from the Angular injector to avoid circular dependencies:

ServicePurpose
adhocServiceAd-hoc HTTP requests (catalog loading, QR code printing)
clockServiceTime management and refresh ticks
languageServiceTranslations
toastServiceUser notifications
alertServiceModal alerts
configServiceLocal device configuration (ticket numbering)

Children Collections​

All collections are cached and invalidated when the corresponding entity type changes (via RefreshView(targets)). Each collection filters for IsValid objects and wraps them as ViewObjects:

GetterTypeSourceInvalidated by
userUserViewplace.user via alives.get()—
tablesQrCodeView[]place.qrcodesQRCODE
productsProductView[]place.productsPRODUCT
offersOfferView[]place.offersOFFER
extrasExtraView[]place.extrasEXTRA
discountsDiscountView[]place.discountsDISCOUNT
employeesEmployeeView[]place.employeesSTAFF
ticketsTicketView[]place.tickets (sorted by updated desc)TICKET
DrawAreasany[]Place structure / draw areas (TODO: define row type + model; today always [])PLACE, QRCODE
PriceAreasPriceZoneView[]Active price-adjustment zones (place.pricezones, IsValid, sorted by sort). Historical getter name; see upp-model.mdc § PRICEZONEPLACE, PRICEZONE, QRCODE
TillAreasTill[]Active tills with valid service + ≥1 terminal (Till.IsValid)PLACE, TILL, RASPPI, RASPPICONNECT

PriceAreas and TillAreas are typed in the model. DrawAreas remains provisional until its sync/model contract exists.

The products getter has special logic: it skips catalog templates that have been replaced (i.e., IsCatalog && last !== self && last.IsValid), showing only the effective version.

ProductsLength returns the count of non-group products (excludes containers).

Refresh Mechanism​

The RefreshView(targets: string[]) method is called when any child entity changes. It invalidates the corresponding cached collection and triggers DoRefreshView():

private RefreshView(targets: string[]) {
if (targets.includes('PRODUCT')) {
this._validproducts = null;
this._groupproducts = null;
this.DoRefreshView();
}
// ... similar for QRCODE, OFFER, EXTRA, DISCOUNT, STAFF, TICKET
}

doRegister​

Subscribes to the Place object's OnRefresh and each QR code's OnRefresh (for alert tracking). Uses WeakRef(this) to prevent the subscription from keeping the PlaceView alive.

Alert System​

Tracks QR code tables that require waiter attention:

PropertyTypeDescription
AlertSizenumberNumber of tables currently needing attention
AlertTablesQrCode[]Array of QrCode objects needing attention

RefreshAlerts(qrcode) checks qrcode.IsValid && qrcode.Attention to decide whether a table should be in the alert set.

Place Configuration​

Ticket and payment behaviour reads PLACEOPT (and related) via getters on the Place DataObject. The PlaceView proxy merges the view with the underlying place, so templates and TicketView still use place.taxrate, place.CanCash, etc.

PropertySource (PLACEOPT / notes)
ProductCodesoptionProductCodes
taxrateoptionTaxRate (numeric), else AppConstants.defaultTaxRate
TicketPrepaymentoptionPrePayment
TicketPreparationoptionTrackTicket
CanCashAlways true (efectivo siempre permitido)
CanCardoptionCreditCard
CanAccountoptionPayAccount
OpenOnCash / OpenOnCardoptionOpenCashpay (default true if unset) / optionOpenCardpay
CashModal / CardModaloptionCashModal / optionCardModal
IsPosSession device is an operational TPV (Place.IsPos: active RASPPICONNECT to a place RASPPI; PosDevice.till via service)
FiscalWarning!optionFiscalReady (solo estado fiscal listo; independiente de futuras reglas en CanPayTickets)
SendVFTEnabledoptionVftEnabled y criterio interno de región Verifactu (hoy siempre inactivo hasta el TODO en Place)
SendBAIEnabledoptionBaiEnabled y heurística interna sobre dirección fiscal (territorio histórico); no exponer getters auxiliares fuera de Place

The Place DataObject also defines MultiplePOS (GetConfigOption('optionMultiplePOS', false)), CanPayTickets (≥1 valid till and optionFiscalReady), and PosWarning (no valid till).

Device configuration: four sources of truth​

Place-level settings (PLACEOPT.value, e.g. optionTaxRate, optionFiscalReady) apply to the whole business. Per-terminal settings use four separate stores — do not mix them:

LayerWhat it storesSource of truthTypical keys / API
Visualization cacheTheme, zoom, scrollbar, virtual keyboard (kiosk), forced mobile/desktopviewService → localStorage upp_cfg_DEVICE_VIEWview.Theme, view.ZoomLevel, view.ForcedMode, …
TPV / till (DB)Logical cash registerTenant DB — TILL (+ TILL.rasppi → RASPPI)Single: created on first POS activation (tab Dispositivo, implicit — no cash-register UI). Multi: panel control cajas (TODO). PlaceView.TillAreas, PosDevice.till, Place.IsPos
Logger campaign (DB, sync-only read)Remote logging campaign idTenant DB — DEVICEOPT (optionLoggerCampaign) via syncPlace.Campaign, DeviceOption._OnUpdate
Print service (DB)TPV hardware service + PC connectionTenant DB — RASPPI + RASPPICONNECTTab Dispositivo GrantService; per-device PlaceView.connect

Rules for implementers:

  • UI visualization preferences are never written to DEVICEOPT or TILL; use viewService / DEVICE_VIEW.
  • Single-TPV: first POS activation on tab Dispositivo creates the shared RASPPI and implicit TILL (RN-TILL-17a); no cash-register UI on tab Datos.
  • Multi-TPV: tab Dispositivo creates RASPPI + connect only; till lifecycle in panel control cajas (TODO, RN-TILL-17d).
  • Changing place does not reset visualization; the VIEW bucket is tied to the browser profile, not state.device.

UI for device settings lives in feature/place (tab Dispositivo). Contract details: libs/feature/place/upp-feature-place.mdc §5.

Continuous ticket entry (bar/POS flow)​

In feature/ticket, continuous entry in Nx: after a successful commit (with payment or send-only), the editor opens a fresh bar ticket automatically only when all these conditions are true:

  • The edited ticket is a bar ticket on POS (ticket.qrcode == null).
  • The current device is configured as POS (place.IsPos).

The status of the ticket being committed is not evaluated for this decision; the new ticket is created in initial state PR.

If any condition fails (table ticket or non-POS device), the editor closes back to the view panel.

Ticket Numbering​

The PlaceView manages per-device sequential numbering for ticket series and invoices, stored locally in configService.

TicketDeviceInfo​

interface TicketDeviceInfo {
X: number | null; // last provisional (unpaid) ticket number
T: number | null; // last ticket number
F: number | null; // last invoice number
R: number | null; // last rectificative number
A: number | null; // last ticket (recapitulative) number
C: number | null; // last ticket (with invoice) number
bai: string | null; // last sent ticketbai signature
}

InitializeTickets()​

Called during PlaceView initialization when Access is LOGIN. Guest (QR) sessions skip this: they must not load or write the device ticket store; the server assigns series/invceno.

  1. Always calls _LastServerTickets() (device/getticket), even when local _DEVICE_TICKETS already looks complete.
  2. _LastServerTickets merges each counter with Math.max(local, server) into ticketstore.
  3. Stores the in-flight result in _ticketsready (gate that fiscal NextDeviceTicket awaits). If reconcile fails and there is no usable local cache, initialization throws.

NextDeviceTicket(serialType, invoiceType): Promise<[string | null, string | null]>​

Generates the next serial and invoice number:

  1. For fiscal types (T/R/F/A/C), awaits _ticketsready. Series X may proceed without waiting.
  2. If reconcile failed: refuse fiscal emission when configService.writefailed is set (user toast @place_ticket_create_error; detail only in the thrown error). Otherwise emit from local and console.warn the developer reason.
  3. Reads the current counter from ticketstore for the given invoiceType.
  4. Increments the counter and persists it.
  5. Formats serial as {SerialType}{deviceSerial} and invoice as {InvoiceType}{padded_number} (e.g. T00000021).
  6. Returns [serial, invoice].

Product Catalog​

Product Hierarchy​

Products form a tree structure through the parent relation:

FlagMeaning
isgroupProduct is a container (folder) for other products
isfamilyProduct has associated Family objects for time-based grouping
IsCatalogProduct comes from a shared catalog template

Catalog Items​

Products with status='CT' are catalog templates loaded from the server. When a place already has an active product for the same ctgref, the template's next points to that local product. When a place customizes a catalog template, the new active product is linked from the template (catalog.next = active). The PlaceView's products getter filters out replaced catalog templates.

Use Product.last (or ProductView.last) to resolve any node in the replacement chain to the definitive place product.

GroupedProducts​

The GroupedProducts getter organizes products into display groups:

type GroupedInfo = {
parent: ProductView | null;
children: ProductView[];
};
type CatalogGrouped = GroupedInfo[];

Products are grouped by their parent:

  • Root products (no parent or parent is the place) form standalone groups.
  • Products whose parent has isgroup=true are nested under their parent group.
  • The result is an array of GroupedInfo, each with an optional parent and an array of children.

Catalog Loading​

get catalog(): Catalog

The Catalog class (lazy-initialized) handles loading product templates from the server via adhocService. It provides products that can be added to the place's catalog.

Backend endpoints:

  • GET catalog/catalog — Returns the list of demo catalogs (JSON).
  • POST catalog/import — Imports a catalog ZIP via chunked transfer (base64 chunks).
  • resources/catalog/images/ — Static path for catalog product images (relative to baseURL).
get search(): SearchTool

The SearchTool (lazy-initialized) provides text search over products and preselects. Used by the UI for product lookup by name or code.

doSearch(criteria: string): ProductView[] | PreselectView[]

Table Management​

MethodDescription
CreateTables(count)Creates the specified number of new QrCode objects for the place
DeleteTables(tables)Marks the specified QrCode objects as DE
PrintQrCodes(tables)Sends a request to the server to generate a PDF with QR codes for download

Offers System​

Offers represent special pricing for product bundles (e.g., a menu).

Offer Structure​

EntityTableDescription
OfferOFFERThe offer definition: name, price, ismenu, allday
OfferProductOFFERPRODUCTLinks an offer to a product, with grouped count and menuctg
OfferPeriodOFFERPERIODDefines when the offer is valid (weekdays, time range)
OfferOptionOFFERPRODUCTOPTLinks an OfferProduct to a specific ProductOpt

Offer Application​

Offers are auto-applied during price calculation (_PriceInfo). The algorithm:

  1. Collects all active offers from the place.
  2. For each offer, calculates the potential savings if applied to the current ticket.
  3. Picks the offer with the greatest savings.
  4. Creates a TicketOffer and assigns matching products to it.
  5. Repeats until no more offers improve savings.

An offer's AppliesTo(ticketView) method checks:

  • The offer's periods are currently active.
  • The ticket contains the required products (matching the offer's OfferProduct entries).
  • The required product amounts are available.

When ismenu=true, the offer represents a fixed-price menu. Products in the menu are organized by menuctg (menu category), and each grouped value indicates how many items from that product can be selected.

Extras System​

Extras are additional charges applied to tickets automatically (e.g., table service charge, terrace surcharge).

Extra Structure​

EntityTableDescription
ExtraEXTRAThe extra definition: name, type, price, allday, allprd
ExtraProductEXTRAPRODUCTLinks an extra to a specific product (when allprd=false)
ExtraPeriodEXTRAPERIODDefines when the extra is valid
ExtraTableEXTRATABLELinks an extra to a specific QR code table
ExtraOptionEXTRAPRODUCTOPTLinks an ExtraProduct to a specific ProductOpt

Extra Application​

Extras are auto-applied during price calculation:

  1. For each existing TicketExtra, check if its source Extra still applies. Remove if not.
  2. For each active Extra in the place, check if it applies to this ticket. Add if applicable and not already present.

An extra applies when:

  • Its periods are currently active (or allday=true).
  • It applies to all products (allprd=true) or the ticket contains a matching product.
  • It applies to all tables or the ticket's QR code matches an ExtraTable.

Discounts System​

Discounts reduce the ticket total, either as a fixed amount or a percentage.

Discount Structure​

EntityTableDescription
DiscountDISCOUNTThe discount definition: name, type, value, allday, allprd, cumulative
DiscountProductDISCOUNTPRODUCTLinks a discount to a specific product
DiscountPeriodDISCOUNTPERIODDefines when the discount is valid

Discount Properties​

PropertyDescription
typeDiscount type (percentage or fixed amount)
valueThe discount value
cumulativeIf true, can be combined with other discounts
allprdIf true, applies to all products; if false, only to linked products
alldayIf true, always active; if false, follows period schedules

Discount Application​

Discounts are manually added by the user (via AddDiscount()), not auto-applied. During price calculation, ApplyDiscounts() triggers calcinfo on each TicketDiscountView, which computes the charge based on the discount's type and value against the current ticket total.

Period Objects (Shared Pattern)​

All period objects (OfferPeriod, ExtraPeriod, DiscountPeriod, FamilyPeriod) share the same structure for defining time-based validity:

FieldTypeDescription
statusstring'AC' for active, 'DE' for deleted
weekdaysstringBitmask or comma-separated list of active days
inistringStart time (e.g., '09:00')
endstringEnd time (e.g., '14:00')
timezonestringTimezone for evaluation

Each period object references its parent (offer, extra, discount, or family) and follows the standard status setter pattern with parent notification on 'DE'.

Family System​

Families group products with time-based activation, typically for daily menus or special schedules.

Family Structure​

EntityTableDescription
FamilyFAMILYThe family definition, linked to a product with isfamily=true
FamilyProductFAMILYPRODUCTLinks a family to a product or preselect, with sort order
FamilyPeriodFAMILYPERIODDefines when the family is active

A product's family getter finds its first valid Family child. When isfamily=true, the product acts as a family container, and its FamilyProduct children define the actual items available during the family's active periods.

FamilyProduct​

Each FamilyProduct can link to either:

  • A Product directly, or
  • A Preselect (which itself groups product options).

IsValid on FamilyProduct checks both its own status and the validity of the linked product/preselect.