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: vendors, users, locations, roles, payment profiles, global customers - Tenant databases (one per vendor, e.g.,
vendor_acme_123) -- Vendor-specific data: menus, items, transactions, settings, inventories, etc.
The Vendor.database_name field determines which tenant database a vendor's data lives in.
Core Entity Hierarchy
Vendor (central DB)
|
+-- User (central DB) -- team members who manage the vendor
|
+-- 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 (tenant 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 (tenant DB) -- active service subscriptionsPolymorphic Collections
Two collections use a model discriminator field to store multiple entity types:
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 |
settings collection (tenant DB)
| Model Value | RawModel Class | Description |
|---|---|---|
category | Category | Item categories |
language | Language | Supported languages |
display_group | DisplayGroup | Menu display groupings |
Always include a model filter when querying these collections to avoid returning the wrong entity type.
Central Database Models
Vendor
Collection: vendors | Connection: mongodb | Soft delete: Yes | Archive: Yes
The top-level entity representing a merchant/business.
| 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_id | string | ID of the default Language in tenant DB |
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
user.vendor_id) - Has many Locations (via
location.vendor_id) - Uses TenancyTrait to switch tenant database
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 vendor association |
vendor_ids | string[] | All vendor associations (multi-vendor access) |
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 |
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 vendor 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 | string | Average order preparation time in minutes |
snooze_time_out_of_stock_items | string | Duration to snooze out-of-stock items |
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 | float | Base price |
pricing | object | Platform-specific pricing ({platform: price}) |
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 |
category_id | string | Category reference |
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 |
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 |
use_default_prep_time | bool | Whether to use location's default prep time |
max_order_limit | int | Maximum quantity per order (0 = unlimited) |
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: 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 (LocationDefault, AlwaysAvailable, Custom) |
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 |
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 |
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? | CustomerVendor ID |
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 | Dining option (dine_in, take_out, delivery, for_here, takeout, pickup) |
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 |
pager_id | string | Pager device ID |
order_session_id | string | Cart/order session identifier |
cart_session_id | string | Cart session for stock reservations |
payment_snapshot | object | Payment provider details at time of payment |
stripe_account_id | string | Stripe Connect account |
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 |
sent_at | UTCDateTime? | When order was sent to kitchen |
order_prep_time | UTCDateTime? | Estimated prep completion time |
split_timeslots | array | Split delivery timeslots |
| Money fields: | ||
currency | string | Currency code |
subtotal | float | Pre-tax, pre-discount total |
addons | float | Modifier/addon charges |
discount | float | Total discount amount |
fees | float | Service fees |
delivery_fee | float | Delivery fee |
tip_amount | float | Customer tip |
vat | float | Total tax amount |
total | float | Final total charged |
Indices: customer_id, order_date, idempotency_key
Other Tenant Models
| Model | Collection | Description |
|---|---|---|
Modifier | menus | Individual modifier options (extra cheese, etc.) |
ModifierGroup | menus | Groups of related modifiers |
VariantGroup | tenant | Product variant groups (sizes, colors) |
VariantOption | tenant | Individual variant options |
DisplayGroup | settings | Visual grouping of items within a menu |
Language | settings | Supported language configurations |
Device | tenant | Registered kiosk devices |
DeviceProfile | tenant | Device configuration and menu assignments |
Inventory | tenant | Stock levels per item per location |
InventoryHistory | tenant | Stock change history log |
BrandingProfile | tenant | Colors, logos, visual branding |
BillingProfile | tenant | Billing/invoicing configuration |
Subscription | tenant | Service subscriptions (loyalty, online ordering) |
PaymentProfile | central | Payment provider configuration |
TableSection | tenant | Restaurant table/seating sections |
Offer | tenant | Promotional offers and discounts |
OfferRedemption | tenant | Offer usage tracking |
Loyalty | tenant | Loyalty program configuration |
LoyaltyPoint | tenant | Customer loyalty point records |
PointRedemption | tenant | Loyalty point redemption tracking |
Reward | tenant | Loyalty rewards definitions |
RewardRedemption | tenant | Reward claim tracking |
BirthdayBonus | tenant | Birthday bonus configuration |
GiftCard | tenant | Gift card records |
Content | tenant | Media/image content records |
TaxRate | tenant | Tax rate definitions |
TaxRateCustom | tenant | Custom/override tax rates |
Timeline | tenant | Activity timeline entries |
OrderSession | tenant | Active ordering sessions |
GuidedSetupProgress | tenant | Guided setup wizard progress |
Customer Models
| Model | Collection | Connection | Description |
|---|---|---|---|
Customer | customers | central | Global customer identity |
CustomerAddress | customer_addresses | central | Global customer addresses |
CustomerVendor | customer_vendors | tenant | Vendor-specific customer data |
CustomerVendorAddress | customer_vendor_addresses | tenant | Vendor-specific delivery addresses |
CustomerVendorToken | customer_vendor_tokens | tenant | Customer authentication tokens |
FCMToken | fcm_tokens | tenant | Firebase push notification tokens |
Integration Models
| Model | Collection | Description |
|---|---|---|
ThirdPartyIntegration | third_party_integrations | Integration configurations |
ThirdPartyTransaction | third_party_transactions | Synced third-party transactions |
KassanetIntegration | kassanet_integrations | Kassanet POS config |
SquareIntegration | square_integrations | Square POS config |
UberEatsCredential | uber_eats_credentials | UberEats API credentials |
HendrickxIntegration | hendrickx_integrations | Hendrickx supplier config |
VanhoutteIntegration | vanhoutte_integrations | Vanhoutte supplier config |
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:
| Factory | Notable Defaults |
|---|---|
VendorFactory | is_test: false, stripe_onboarding_completed: false |
UserFactory | status: 'active', all_location_access: false, empty arrays for passkeys, trusted_devices |
LocationFactory | Default business hours, empty receipt settings, is_sms_subscribed: false |
ItemFactory | price: 0, status: 'active', empty arrays for allergens/ingredients/modifiers |
MenuFactory | status: 'active', availability_type: 'location_default' |
TransactionFactory | All money fields default to 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. When querying the
menuscollection, always includemodel: 'item'(ormenu,modifier,modifier_group). Same forsettings.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').