Appearance
Data Model Reference
This document describes the MongoDB data model used by the Upvendo backend. All models are implemented as RawModels (plain PHP objects) in app/RawModels/, with corresponding factories in app/RawFactories/.
Database Architecture
Upvendo uses a multi-database multi-tenant approach with MongoDB:
- Central database (
upvendo) -- Shared data: merchants, users, locations, roles, payment profiles, global customers - Tenant databases (one per merchant, e.g.,
vendor_acme_123) -- Merchant-specific data: menus, items, transactions, settings, inventories, etc.
The Merchant.database_name field determines which tenant database a merchant's data lives in.
Naming: the model class is
App\RawModels\Merchant(app/RawModels/Merchant.php). It was renamed fromVendor, but storage was not renamed:COLLECTION = 'vendors',MODEL = 'vendor', and the foreign key stored on user and location documents is stillvendor_id. There is noapp/RawModels/Vendor.phpand noVendorFactory-- it isMerchantFactory. The same split applies to the customer models: the classes areCustomerMerchant/CustomerMerchantAddress/CustomerMerchantTokenwhile theirmodeldiscriminators are stillcustomer_vendor/customer_vendor_address/customer_vendor_token.
Core Entity Hierarchy
Merchant (central DB, `vendors` collection)
|
+-- User (central DB) -- team members who manage the merchant
|
+-- Location (central DB) -- physical store/restaurant locations
| |
| +-- Menu (tenant DB) -- menus available at this location
| | +-- DisplayGroup -- visual grouping of items within a menu
| | +-- Item -- products/items in the display group
| |
| +-- Category (tenant DB) -- item categories
| +-- Device (central DB) -- kiosk devices at this location
| +-- DeviceProfile (tenant DB) -- configuration for devices
| +-- Transaction (tenant DB) -- orders placed at this location
| +-- Inventory (tenant DB) -- stock levels per item per location
| +-- TableSection (tenant DB) -- table/seating sections
| +-- Offer (tenant DB) -- promotional offers
| +-- Loyalty (tenant DB) -- loyalty program config
|
+-- BrandingProfile (tenant DB) -- visual branding settings
+-- BillingProfile (tenant DB) -- billing/subscription info
+-- Language (tenant DB) -- supported languages
+-- Subscription (central DB) -- active service subscriptionsPolymorphic Collections
A CollectionsOptimization command consolidated the original one-collection-per-entity layout into a handful of polymorphic collections that use a model discriminator field. The physical collection name usually does not match the model name -- read CONNECTION / COLLECTION / MODEL straight off the RawModel before writing a query, and always filter on model.
menus collection (tenant DB)
| Model Value | RawModel Class | Description |
|---|---|---|
item | Item | Products/menu items |
menu | Menu | Menu definitions |
modifier | Modifier | Individual modifiers |
modifier_group | ModifierGroup | Groups of modifiers |
display_group | DisplayGroup | Menu display groupings |
variant_group | VariantGroup | Product variant groups |
variant_option | VariantOption | Individual variant options |
offer | Offer | Promotional offers |
gift_card | GiftCard | Gift card records |
combo_definition | ComboDefinition | Combo/formula definitions |
upsell_group | UpsellGroup | Upsell groupings |
DisplayGroup is in menus, not settings -- querying settings with model: 'display_group' returns nothing.
settings collection (tenant DB)
| Model Value | RawModel Class | Description |
|---|---|---|
category | Category | Item categories |
language | Language | Supported languages |
device_profile | DeviceProfile | Device configuration profiles |
branding_profile | BrandingProfile | Colors, logos, visual branding |
billing_profile | BillingProfile | Billing/invoicing configuration |
table_section | TableSection | Restaurant table/seating sections |
loyalty | Loyalty | Loyalty program configuration |
reward | Reward | Loyalty reward definitions |
birthday_bonus | BirthdayBonus | Birthday bonus configuration |
domain_profile | DomainProfile | Custom-domain configuration |
gift_card_template | GiftCardTemplate | Gift card templates |
inactive_payment_profile | InactivePaymentProfile | Retired payment profile records |
translation_job_progress | TranslationJobProgress | Bulk-translation job progress |
qr_code_job_progress | QrCodeJobProgress | QR-code generation job progress |
kds_station | KdsStation | Kitchen Display station configuration |
kds_profile | KdsProfile | Kitchen Display profile configuration |
printer_profile | PrinterProfile | Printer configuration profiles |
pos_layout | PosLayout | First-party POS register layouts |
There is a separate central settings collection on the mongodb connection holding PaymentProfile (payment_profile), AllowedIpSetting (allowed_ip) and GuidedSetupVideoSetting (guided_setup_video).
transactions collection (tenant DB)
Stores transaction, transaction_item, order_session, third_party_transaction, invoice, split_payment_session, loyalty_point, point_redemption, reward_redemption, offer_redemption.
users and locations collections (central DB)
usersstoresuser,customer,customer_vendor(CustomerMerchant),devicelocationsstoreslocation,third_party_integration(and every provider subclass),customer_address,customer_vendor_address
activity_logs collection (tenant DB)
Stores activity_log, timeline, device_log, queue_counter, customer_vendor_token.
Always include a model filter when querying these collections to avoid returning the wrong entity type.
Central Database Models
Merchant
Class: App\RawModels\Merchant | Collection: vendors | Connection: mongodb | Model discriminator: vendor | Soft delete: Yes | Archive: Yes
The top-level entity representing a merchant/business. (Formerly Vendor; the class was renamed, the collection and discriminator were not.)
| Field | Type | Description |
|---|---|---|
_id | ObjectId | Primary key |
business_name | string | Display name of the business |
business_category | string | Business type (restaurant, frituur, etc.) |
database_name | string | Name of the tenant MongoDB database |
slug | string | URL-friendly unique identifier |
email | string | Primary contact email |
default_language | string | ID of the default Language in tenant DB (stored field is default_language; PHP property is default_language_id) |
branding_profile_id | string? | ID of the default BrandingProfile in tenant DB |
is_test | bool | Whether this is a test/demo vendor |
stripe_onboarding_completed | bool | Stripe Connect onboarding status |
guided_setup_complete | bool | Whether merchant finished guided setup |
provider_integrated | string | External POS provider (e.g., "square") |
Key relationships:
- Has many Users (via the
vendor_idfield on the user document) - Has many Locations (via the
vendor_idfield on the location document) - Uses TenancyTrait to switch tenant database
The stored document field is
vendor_id, but the hydrated PHP property ismerchant_id(UserFactory:merchant_id: $data['vendor_id'] ?? '';LocationFactory:merchant_id: $data['vendor_id'] ?? ''). Usevendor_idin Mongo queries andgetMerchantId()in PHP.
User
Collection: users | Connection: mongodb | Soft delete: Yes
Backoffice team member accounts.
| Field | Type | Description |
|---|---|---|
_id | ObjectId | Primary key |
email | string | Login email |
first_name | string | First name |
last_name | string | Last name |
username | string | Username |
password | string | Hashed password |
phone | string? | Phone number |
vendor_id | string | Primary merchant association (PHP property: merchant_id) |
vendor_ids | string[] | Extra merchant associations for partner administrators / reseller users -- written by TeamService and ResellerService, not a User constructor property |
reseller_id | string? | Reseller company this user belongs to |
role_ids | string[] | Assigned role IDs |
location_ids | string[] | Accessible location IDs |
all_location_access | bool | If true, has access to all vendor locations |
status | string | Account status (active, pending, etc.) |
otp | string? | Current one-time password |
last_otp_request | int? | Timestamp of last OTP request |
resend_counter | int | OTP resend rate limiter |
trusted_devices | array | List of trusted device fingerprints |
remember_me | bool | Remember me preference |
challenge_token | string? | Active challenge token for sensitive ops |
challenge_token_expires_at | int? | Challenge token expiry timestamp |
webauthn_challenge | string? | Current WebAuthn challenge |
passkeys | array | Registered passkey credentials |
photo_templates | array | Photo studio templates |
invite_token | string | Account invitation token |
invite_token_expires_at | UTCDateTime? | Invitation expiry |
forget_password_token | string | Password reset token |
forget_password_at | int | Password reset timestamp |
forget_password_counter | int | Reset attempt counter |
must_change_password | bool | Forces a password change on next login |
email_verified_at | UTCDateTime? | Email verification timestamp |
email_verification_token | string? | Email verification token |
email_verification_sent_at | int? | When the verification email was sent |
google_id | string? | Google account identifier (Google sign-in) |
Location
Collection: locations | Connection: mongodb | Soft delete: Yes
Physical store/restaurant locations belonging to a vendor.
| Field | Type | Description |
|---|---|---|
_id | ObjectId | Primary key |
name | string | Location display name |
description | string | Location description |
vendor_id | string | Parent merchant ID (PHP property: merchant_id) |
slug | string | URL-friendly identifier |
status | string | Location status |
category | string | Business category override |
location_type | string | Location type classification |
address | object | Structured address fields |
gmaps_address | object | Google Maps address data with components |
pinpoint | object | Latitude/longitude coordinates |
country_code | string | Two-letter country code (BE, FR, NL, etc.) |
currency | string | Currency code (EUR, USD, GBP, etc.) |
timezone | string | IANA timezone identifier |
preferred_language | string | Preferred language for this location |
business_hours | object | Weekly business hours per day |
restricted_dates | array | Dates when location is closed |
contact_information | object | Phone, email, social media links |
average_prep_time | int | Average order preparation time in minutes. Typed int on the model (app/RawModels/Location.php:125); a legacy string document is cast on hydration and a missing field reads as 0, not the 20 create default (app/RawFactories/LocationFactory.php:32) |
branding_profile_id | string | BrandingProfile ID in tenant DB |
payment_profile_id | string | PaymentProfile ID |
stripe_customer_id | string | Stripe customer identifier |
viva_wallet_physical_source_code | string | Viva Wallet physical terminal source |
viva_wallet_online_source_code | string | Viva Wallet online payment source |
online_ordering_setting | object | Online ordering configuration |
online_settings | object | General online platform settings |
in_house_setting | object | In-house/dine-in configuration |
qr_ordering_setting | object? | QR table ordering configuration |
receipt_setting | object | Receipt formatting and display settings |
loyalty_subscription_id | string? | Loyalty subscription reference |
online_ordering_subscription_id | string? | Online ordering subscription reference |
is_sms_subscribed | bool | Whether SMS notifications are enabled |
landing_page_cloudflare_image_id | string? | Cloudflare image ID for landing page |
external_data | object | Third-party integration data |
upvote_count | int | Restaurant suggestion upvotes |
Tenant Database Models
Item
Collection: menus | Connection: tenant | Model discriminator: item | Soft delete: Yes
Products/menu items sold by the merchant.
| Field | Type | Description |
|---|---|---|
_id | ObjectId | Primary key |
category_id | string | Parent category ID |
location_id | string | Location this item belongs to |
details | object | Multi-language name and description ({lang: {name, description}}) |
kitchen_name | string? | Short name for kitchen display |
price | Money | Base price -- App\ValueObjects\Money, stored in cents |
pricing | object | Platform-specific pricing ({platform: Money}), also in cents |
plu | string | PLU/barcode code |
status | string | Item status (active, disabled, etc.) |
tax_rate_code | string | Tax rate identifier |
content_id | string | Content/media reference |
cloudflare_image_id | string | Cloudflare Images ID |
variant_group_id | string? | Variant group (e.g., sizes) reference |
modifier_group_ids | string[] | Associated modifier group IDs |
display_group_ids | string[] | Display groups this item appears in |
upsell_group_ids | string[] | Upsell groups this item belongs to |
offer_ids | string[] | Applied offer IDs |
platforms | string[] | Platforms where item is visible (kiosk, online, etc.) |
ingredients | string[] | Ingredient labels |
allergens | string[] | Allergen labels |
dietary_preferences | string[] | Dietary preference tags (vegan, vegetarian, etc.) |
dietary_supplements | string[] | Dietary supplement tags |
contains_alcohol | bool | Whether item contains alcohol |
alcohol_type | string | Type of alcohol if applicable |
minimum_age | int | Minimum age requirement |
calorie_count | float | Calorie information |
prep_time_seconds | int | Preparation time in seconds. Inert: gated by ONLINE_ORDERING_INCLUDE_ITEM_PREP_TIME (config/upvendo.php:26, default false, unset in all deployed environments), so it reaches no customer-facing quote |
use_default_prep_time | bool | Whether to use location's default prep time. Server-managed; only false lets an item contribute its own prep time, and only while the gate above is on |
lead_time_days | int | Advance-order lead time in days (kiosk & online ordering) |
available_pickup_days | string[] | Days this item may be picked up on |
max_order_limit | int | Maximum quantity per order (0 = unlimited) |
is_global | bool | Whether this item is a global (multi-location) item |
global_group_key | string? | Key linking the per-location copies of a global item |
product_type | string | Product type -- ItemFactory default 'simple'; Item::isCombo() matches 'combo' or 'pack' |
combo_definition_id | string? | Linked ComboDefinition when the item is a combo/formula |
external_data | object | Third-party integration data (keyed by provider) |
external_ids | object | Third-party IDs (keyed by provider) |
birthday_bonus_id | string | Linked birthday bonus |
reward_ids | string[] | Linked loyalty reward IDs |
raw_value | object | Raw/unprocessed value data |
Indices (Item::INDICES): location_id, status, variant_group_id, plu
Key traits: HasExternalIds, HasLocation, HasModifierGroup, PriceTrait
Menu
Collection: menus | Connection: tenant | Model discriminator: menu | Soft delete: Yes
Menu definitions that group display groups and items for a location.
| Field | Type | Description |
|---|---|---|
_id | ObjectId | Primary key |
name | string | Menu name |
pos_name | string | POS-specific name |
description | string | Menu description |
location_id | string | Location this menu belongs to |
status | string | Menu status |
availability_type | string | When menu is available -- AvailabilityOptions enum: location-default, specific-day-time, always-available (hyphenated; there is no Custom case) |
availability | object | Custom availability schedule per day |
visibility | string[] | Channels where menu is visible (kiosk, online_ordering, etc.) |
device_profile_ids | string[] | Device profiles that use this menu |
display_group_ids | string[] | Display groups in this menu |
external_data | object | Third-party integration data |
published_hash | string? | Hash of the last published menu state |
has_draft | bool | Whether unpublished draft changes exist |
Category
Collection: settings | Connection: tenant | Model discriminator: category | Soft delete: Yes
Item categories for organizing products.
| Field | Type | Description |
|---|---|---|
_id | ObjectId | Primary key |
details | object | Multi-language name/description ({lang: {name, description}}) |
name | string? | Legacy single-language name |
content_id | string? | Content/media reference |
image_url | string | Category image URL |
item_count | int? | Cached count of items in this category |
external_ids | object | Third-party IDs |
external_data | object | Third-party integration data |
parent_id | string? | Parent category (categories are hierarchical) |
sort_order | int | Sort position among siblings (default 0) |
path | string[] | Ancestor chain, path-to-self convention |
status | string | Ordering-channel visibility -- CategoryStatuses, default Active. getEffectiveStatus() walks path so a restricted ancestor cascades to the whole subtree |
lead_time_days | int | Advance-order lead time inherited by this category's items (default 0) |
available_pickup_days | string[] | Pickup days inherited by this category's items |
Indices (Category::INDICES): parent_id, sort_order, path, external_ids
Transaction
Collection: transactions | Connection: tenant | Soft delete: Yes | Archive: Yes
Order/payment records.
| Field | Type | Description |
|---|---|---|
_id | ObjectId | Primary key |
idempotency_key | string | Unique key to prevent duplicate processing |
all_idempotency_keys | string[] | All idempotency keys across retries |
invalid_idempotency_keys | string[] | Invalidated keys |
order_no | string | Human-readable order number (10-char random) |
receipt_no | string | Receipt number (YYMMDD + 6 alphanumeric) |
unauthenticated_order_no | string? | Order number for unauthenticated customers |
location_id | string | Location where order was placed |
device_id | string | Kiosk device ID (empty for online orders) |
customer_id | string? | CustomerMerchant ID (model customer_vendor) |
customer_name | string | Customer display name |
customer_first_name | string | Customer first name |
customer_phone | string | Customer phone |
customer_email | string | Customer email |
customer_token | string | Customer identification token |
customer_address | object? | Delivery address |
country_code | string | Country code |
dining_option | string | DiningOptions enum -- exactly four cases, title-cased: Delivery, For Here, Pickup, Takeout. Legacy documents may also carry Dine In (deprecated DiningOptions::dineIn()) or None (DiningOptions::none()). No snake_case values are ever stored. |
channel_dining_option | string? | Dining option as supplied by the ordering channel |
order_channel | string | Channel (kiosk, online_ordering, qr_ordering) |
order_date | UTCDateTime | Scheduled order date/time |
order_status | string | Kitchen status (preparing, ready, etc.) |
status | string | Payment status (complete, pending, etc.) |
type | string | Transaction type |
priority | bool | Priority order flag |
hold | bool | Hold order flag |
table_number | string | Table number for dine-in |
table_section_id | string? | TableSection this order belongs to |
section_id | string | Section identifier on the order |
section_name | string | Section display name on the order |
pager_id | string | Pager device ID |
order_session_id | string | Cart/order session identifier |
cart_session_id | string | Cart session for stock reservations |
qty | int | Total item quantity on the order |
payment_snapshot | object | Payment provider details at time of payment |
payment_provider | string | Payment provider that handled the order |
payment_method | string? | Payment method -- set by OnlineOrderingOrchestrator::storePayment for deferred methods (COD, invoice) |
stripe_account_id | string | Stripe Connect account |
stripe_invoice_id | string? | Stripe invoice ID (invoicing flow) |
stripe_invoice_item_id | string? | Stripe invoice item ID (invoicing flow) |
vat_number | string? | Customer VAT number (invoice orders) |
company_name | string? | Customer company name (invoice orders) |
pos_relation_number | int? | POS relation number for the customer |
mplus_total_incl_at_creation | string? | MplusKassa incl-VAT total captured at creation |
all_viva_order_codes | array | Every Viva Wallet order code seen for this order |
snapshots | object | Full order snapshot (items, location, language, rewards, offers) |
reward_ids | string[] | Applied loyalty reward IDs |
item_label | string | Item source label (regular, hendrickx, etc.) |
external_ids | object | Third-party order IDs |
external_data | object | Third-party order data |
status_logs | array | History of status changes |
lifecycle_logs | array | Append-only order lifecycle timeline; entries are built by TransactionLifecycleLogService |
sent_at | UTCDateTime? | When order was sent to kitchen |
paid_at | UTCDateTime? | When payment completed. Set once, and used as the baseline for the POS-propagation-latency metric (time from paid_at to the POS create-succeeded lifecycle entry). Null on pre-existing documents |
is_pos_sync_failed | bool | Order failed to propagate to the POS |
is_test_mode | bool | Order was placed in test mode |
order_prep_time | UTCDateTime? | Estimated prep completion time |
queue_number | int? | Daily-reset receipt queue number |
queue_day | string? | Day the queue_number belongs to (reset key) |
split_timeslots | array | Split delivery timeslots |
note | string? | Order-level customer note entered once at checkout. Distinct from the per-item TransactionItem.notes, and gated by its own allow_order_notes setting (per-item notes are gated by allow_notes) |
device | string? | Device label shown in the transactions datatable |
short_address | string? | Shortened delivery address shown in the transactions datatable |
Money fields -- every row below except currency is an App\ValueObjects\Money, stored in cents: | ||
currency | string | Currency code |
subtotal | Money | Pre-tax, pre-discount total |
addons | Money | Modifier/addon charges |
discount | Money | Total discount amount |
fees | Money | Service fees |
delivery_fee | Money | Delivery fee |
pickup_transaction_fee | Money | Pickup transaction fee |
tip_amount | Money | Customer tip |
vat | Money | Total tax amount |
total | Money | Final total charged |
Indices (Transaction::INDICES): customer_id, order_date, idempotency_key, order_channel, status, location_id, payment_method, item_label
Other Tenant Models
Every row below is CONNECTION = 'tenant'. The Model discriminator column is the value to filter on, since most of these share a collection.
| Model | Collection | Model discriminator | Description |
|---|---|---|---|
Modifier | menus | modifier | Individual modifier options (extra cheese, etc.) |
ModifierGroup | menus | modifier_group | Groups of related modifiers |
DisplayGroup | menus | display_group | Visual grouping of items within a menu |
VariantGroup | menus | variant_group | Product variant groups (sizes, colors) |
VariantOption | menus | variant_option | Individual variant options |
Offer | menus | offer | Promotional offers and discounts |
GiftCard | menus | gift_card | Gift card records |
ComboDefinition | menus | combo_definition | Combo/formula definitions |
UpsellGroup | menus | upsell_group | Upsell groupings attached to items |
Language | settings | language | Supported language configurations |
DeviceProfile | settings | device_profile | Device configuration and menu assignments |
BrandingProfile | settings | branding_profile | Colors, logos, visual branding |
BillingProfile | settings | billing_profile | Billing/invoicing configuration |
TableSection | settings | table_section | Restaurant table/seating sections |
Loyalty | settings | loyalty | Loyalty program configuration |
Reward | settings | reward | Loyalty rewards definitions |
BirthdayBonus | settings | birthday_bonus | Birthday bonus configuration |
DomainProfile | settings | domain_profile | Custom-domain configuration |
GiftCardTemplate | settings | gift_card_template | Gift card templates |
InactivePaymentProfile | settings | inactive_payment_profile | Retired payment profile records |
TranslationJobProgress | settings | translation_job_progress | Bulk-translation job progress |
QrCodeJobProgress | settings | qr_code_job_progress | QR-code generation job progress |
KdsStation | settings | kds_station | Kitchen Display station configuration |
KdsProfile | settings | kds_profile | Kitchen Display profile configuration |
PrinterProfile | settings | printer_profile | Printer configuration profiles |
PosLayout | settings | pos_layout | First-party POS register layouts |
Till | pos_tills | pos_till | First-party POS till records |
PosRegister | pos_registers | pos_register | First-party POS register devices |
PosDrawer | pos_drawers | pos_drawer | Cash drawer sessions |
PosBusinessDay | pos_business_days | pos_business_day | POS business-day open/close records |
FdmDevice | pos_fdm_devices | pos_fdm_device | Fiscal data module (FDM) devices |
PosStaffCredential | pos_staff_credentials | pos_staff_credential | POS staff login credentials |
OrderSession | transactions | order_session | Active ordering sessions |
SplitPaymentSession | transactions | split_payment_session | Split-payment session state |
Invoice | transactions | invoice | Stripe Invoice Collection invoice bundling N transactions for one Mplus relation |
LoyaltyPoint | transactions | loyalty_point | Customer loyalty point records |
PointRedemption | transactions | point_redemption | Loyalty point redemption tracking |
RewardRedemption | transactions | reward_redemption | Reward claim tracking |
OfferRedemption | transactions | offer_redemption | Offer usage tracking |
Inventory | inventories | inventory | Stock levels per item per location |
InventoryHistory | inventories | inventory_history | Stock change history log |
ActivityLog | activity_logs | activity_log | Audit log entries |
Timeline | activity_logs | timeline | Activity timeline entries |
DeviceLog | activity_logs | device_log | Device-reported log entries |
QueueCounter | activity_logs | queue_counter | Daily queue numbers per location; atomic increment behind Transaction.queue_number |
Content | contents | (none) | Media/image content records |
There is no GuidedSetupProgress model. Guided setup is represented by GuidedSetupVideoSetting (central settings, model guided_setup_video).
Central Models Beyond Merchant / User / Location
All CONNECTION = 'mongodb'.
| Model | Collection | Description |
|---|---|---|
Device | users | Registered kiosk devices (central, not tenant) |
Subscription | subscriptions | Service subscriptions (central, not tenant) |
SubscriptionLog | subscription_logs | Subscription lifecycle history |
PaymentProfile | settings | Payment provider configuration |
AllowedIpSetting | settings | IP allow-list configuration |
GuidedSetupVideoSetting | settings | Guided-setup video configuration |
Role | roles | Permission roles |
TaxRate / TaxRateCustom | tax_rates | Tax rate definitions and custom overrides (central, not tenant) |
Reseller | resellers | Reseller companies |
Representative | representatives | Sales representatives |
Stand / StandBatch | stands / stand_batches | Field Ops stands and provisioning batches |
FieldInvite / FieldSession | field_invites / field_sessions | Field Ops rep invitations and sessions |
MplusRelation | mplus_relations | Read-only local mirror of Mplus POS relations (customers) |
Notification | notifications | In-app notifications |
MerchantDailyStat | merchant_daily_stats | Per-merchant daily aggregates |
MerchantAiCreditLog | vendor_ai_credit_logs | AI credit consumption log |
PurchaseLog | purchase_logs | Purchase history log |
WalletDeviceRegistration | wallet_device_registrations | Apple/Google wallet pass device registrations |
MediaAttachment | media_attachments | Photo/video captured by a field rep, stored in the R2 field-media bucket |
JobMetric | job_metrics | Queue job duration/wait metrics |
RequestLog | logs | HTTP request log entries |
KioskAppUpdate | logs | Kiosk app update records |
StuckTransaction | stuck_transactions | Transactions flagged by the stuck-order reconciler |
RestaurantSuggestion / RestaurantUpvote | restaurant_suggestions | Restaurant discovery suggestions and upvotes |
CrmLeadIntake | crm_lead_intakes | Inbound CRM lead records |
OdooErasureRequest | odoo_erasure_requests | Odoo GDPR erasure requests |
Cache | caches | MongoDB-backed cache entries |
Customer Models
Every collection name here differs from the model name -- these were consolidated. Always filter on the model discriminator.
| Model | Collection | Connection | Model discriminator | Description |
|---|---|---|---|---|
Customer | users | mongodb | customer | Global customer identity |
CustomerAddress | locations | mongodb | customer_address | Global customer addresses |
CustomerMerchant | users | mongodb | customer_vendor | Merchant-specific customer data (central, not tenant) |
CustomerMerchantAddress | locations | mongodb | customer_vendor_address | Merchant-specific delivery addresses |
CustomerMerchantToken | activity_logs | tenant | customer_vendor_token | Customer authentication tokens |
FCMToken | logs | mongodb | fcm_token | Firebase push notification tokens |
There are no customers, customer_addresses, customer_vendors, customer_vendor_addresses, customer_vendor_tokens or fcm_tokens collections, and no CustomerVendor* classes.
Integration Models
| Model | Collection | Connection | Model discriminator | Description |
|---|---|---|---|---|
ThirdPartyIntegration | locations | mongodb | third_party_integration | Integration configurations |
ThirdPartyTransaction | transactions | tenant | third_party_transaction | Synced third-party transactions |
Provider-specific integrations -- KassanetIntegration (abstract), HendrickxIntegration, VanhoutteIntegration, SquareIntegration, UberEatsIntegration, MplusKassaIntegration, LightspeedKSeriesIntegration, ShopCaisseIntegration -- are subclasses of ThirdPartyIntegration and share its collection. They declare no COLLECTION of their own, so there are no kassanet_integrations, square_integrations, hendrickx_integrations or vanhoutte_integrations collections. HendrickxIntegration and VanhoutteIntegration extend KassanetIntegration; the rest extend ThirdPartyIntegration directly. There is no UberEatsCredential class -- it is UberEatsIntegration.
Multi-Language Details Pattern
Many entities use a details object for multi-language support:
json
{
"details": {
"default": {
"name": "Frikandel",
"description": "Classic Dutch snack"
},
"nl": {
"name": "Frikandel",
"description": "Klassieke Nederlandse snack"
},
"fr": {
"name": "Fricandelle",
"description": "Snack hollandais classique"
}
}
}The default key is always present and serves as the fallback language. Additional keys correspond to ISO language codes.
Factory Defaults
Each model has a corresponding factory in app/RawFactories/ that defines default field values for new instances. Key factories:
Do not assume an enum default. Several factories default status-like fields to the empty string, not to
'active'or an enum case.
| Factory | Notable Defaults |
|---|---|
MerchantFactory | is_test: false, stripe_onboarding_completed: true, ai_photo_credits: 20, merchant_status: MerchantStatuses::Active (there is no VendorFactory) |
UserFactory | status: UserStatuses::Active, all_location_access: false, empty arrays for passkeys, trusted_devices; merchant_id is read from $data['vendor_id'] |
LocationFactory | Default business hours, empty receipt settings, is_sms_subscribed: false; merchant_id is read from $data['vendor_id'] |
ItemFactory | price: Money::fromCents(PriceConverter::normalize(0)), status: '', product_type: 'simple', use_default_prep_time: true, empty arrays for allergens/ingredients/modifiers |
MenuFactory | status: '', availability_type: '', has_draft: false |
TransactionFactory | All money fields default to Money::fromCents(PriceConverter::normalize(0)), empty arrays for snapshots/rewards |
CategoryFactory | Empty details, empty external data |
DeviceFactory | Device registration defaults |
InventoryFactory | Default stock values |
LoyaltyFactory | Loyalty program defaults |
OfferFactory | Offer configuration defaults |
SubscriptionFactory | Subscription lifecycle defaults |
Key Patterns for AI Bug Fixing
Check the connection constant.
CONNECTION = 'mongodb'means central database,CONNECTION = 'tenant'means tenant database. Querying the wrong connection is a common bug.Polymorphic collection queries must filter by model. Nearly every collection is shared. Read
COLLECTIONandMODELoff the RawModel and filter onmodel-- do not infer the collection from the class name (DisplayGroupis inmenus,PaymentProfileis insettings,Deviceis inusers,ThirdPartyIntegrationis inlocations).The
detailsobject is required for multi-language. If a model has adetailsfield, thedefaultkey must always be populated. Missingdefaultcauses null name displays.ObjectId handling. All
_idfields and relationship IDs are stored as MongoDB ObjectIds. ThetoObjectIds()helper converts string arrays. Comparing a string ID to an ObjectId will fail silently.Soft delete awareness. Models with
SOFT_DELETE = trueare never physically deleted. Queries should account fordeleted_atbeing null (non-deleted) or non-null (deleted). Repositories handle this automatically.Transaction snapshots are immutable. The
snapshotsfield captures the full state at order time (items, prices, location settings, language). Do not assume snapshot data matches current entity state.External data is provider-keyed.
external_dataandexternal_idsare objects keyed by provider name (e.g.,square,deliveroo,uber_eats). Always access viagetExternalData('provider_name').Money is a value object, not a float.
Item.price,Item.pricingand everyTransactionmoney field areApp\ValueObjects\Money, stored in cents. Never do float arithmetic onprice/subtotal/total-- construct withMoney::fromCents(PriceConverter::normalize($raw))and compare/aggregate throughMoney. TheMigratePricesToCentscommand is what moved the data off floats.Class names and collection names drifted apart.
Vendor->MerchantandCustomerVendor*->CustomerMerchant*were class-only renames; the collections (vendors), discriminators (vendor,customer_vendor) and stored foreign keys (vendor_id) still use the old name. Renaming a stored field to match a class name will silently match nothing.