Skip to content

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 from Vendor, but storage was not renamed: COLLECTION = 'vendors', MODEL = 'vendor', and the foreign key stored on user and location documents is still vendor_id. There is no app/RawModels/Vendor.php and no VendorFactory -- it is MerchantFactory. The same split applies to the customer models: the classes are CustomerMerchant / CustomerMerchantAddress / CustomerMerchantToken while their model discriminators are still customer_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 subscriptions

Polymorphic 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.

Model ValueRawModel ClassDescription
itemItemProducts/menu items
menuMenuMenu definitions
modifierModifierIndividual modifiers
modifier_groupModifierGroupGroups of modifiers
display_groupDisplayGroupMenu display groupings
variant_groupVariantGroupProduct variant groups
variant_optionVariantOptionIndividual variant options
offerOfferPromotional offers
gift_cardGiftCardGift card records
combo_definitionComboDefinitionCombo/formula definitions
upsell_groupUpsellGroupUpsell groupings

DisplayGroup is in menus, not settings -- querying settings with model: 'display_group' returns nothing.

settings collection (tenant DB)

Model ValueRawModel ClassDescription
categoryCategoryItem categories
languageLanguageSupported languages
device_profileDeviceProfileDevice configuration profiles
branding_profileBrandingProfileColors, logos, visual branding
billing_profileBillingProfileBilling/invoicing configuration
table_sectionTableSectionRestaurant table/seating sections
loyaltyLoyaltyLoyalty program configuration
rewardRewardLoyalty reward definitions
birthday_bonusBirthdayBonusBirthday bonus configuration
domain_profileDomainProfileCustom-domain configuration
gift_card_templateGiftCardTemplateGift card templates
inactive_payment_profileInactivePaymentProfileRetired payment profile records
translation_job_progressTranslationJobProgressBulk-translation job progress
qr_code_job_progressQrCodeJobProgressQR-code generation job progress
kds_stationKdsStationKitchen Display station configuration
kds_profileKdsProfileKitchen Display profile configuration
printer_profilePrinterProfilePrinter configuration profiles
pos_layoutPosLayoutFirst-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)

  • users stores user, customer, customer_vendor (CustomerMerchant), device
  • locations stores location, 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.)

FieldTypeDescription
_idObjectIdPrimary key
business_namestringDisplay name of the business
business_categorystringBusiness type (restaurant, frituur, etc.)
database_namestringName of the tenant MongoDB database
slugstringURL-friendly unique identifier
emailstringPrimary contact email
default_languagestringID of the default Language in tenant DB (stored field is default_language; PHP property is default_language_id)
branding_profile_idstring?ID of the default BrandingProfile in tenant DB
is_testboolWhether this is a test/demo vendor
stripe_onboarding_completedboolStripe Connect onboarding status
guided_setup_completeboolWhether merchant finished guided setup
provider_integratedstringExternal POS provider (e.g., "square")

Key relationships:

  • Has many Users (via the vendor_id field on the user document)
  • Has many Locations (via the vendor_id field on the location document)
  • Uses TenancyTrait to switch tenant database

The stored document field is vendor_id, but the hydrated PHP property is merchant_id (UserFactory: merchant_id: $data['vendor_id'] ?? ''; LocationFactory: merchant_id: $data['vendor_id'] ?? ''). Use vendor_id in Mongo queries and getMerchantId() in PHP.

User

Collection: users | Connection: mongodb | Soft delete: Yes

Backoffice team member accounts.

FieldTypeDescription
_idObjectIdPrimary key
emailstringLogin email
first_namestringFirst name
last_namestringLast name
usernamestringUsername
passwordstringHashed password
phonestring?Phone number
vendor_idstringPrimary merchant association (PHP property: merchant_id)
vendor_idsstring[]Extra merchant associations for partner administrators / reseller users -- written by TeamService and ResellerService, not a User constructor property
reseller_idstring?Reseller company this user belongs to
role_idsstring[]Assigned role IDs
location_idsstring[]Accessible location IDs
all_location_accessboolIf true, has access to all vendor locations
statusstringAccount status (active, pending, etc.)
otpstring?Current one-time password
last_otp_requestint?Timestamp of last OTP request
resend_counterintOTP resend rate limiter
trusted_devicesarrayList of trusted device fingerprints
remember_meboolRemember me preference
challenge_tokenstring?Active challenge token for sensitive ops
challenge_token_expires_atint?Challenge token expiry timestamp
webauthn_challengestring?Current WebAuthn challenge
passkeysarrayRegistered passkey credentials
photo_templatesarrayPhoto studio templates
invite_tokenstringAccount invitation token
invite_token_expires_atUTCDateTime?Invitation expiry
forget_password_tokenstringPassword reset token
forget_password_atintPassword reset timestamp
forget_password_counterintReset attempt counter
must_change_passwordboolForces a password change on next login
email_verified_atUTCDateTime?Email verification timestamp
email_verification_tokenstring?Email verification token
email_verification_sent_atint?When the verification email was sent
google_idstring?Google account identifier (Google sign-in)

Location

Collection: locations | Connection: mongodb | Soft delete: Yes

Physical store/restaurant locations belonging to a vendor.

FieldTypeDescription
_idObjectIdPrimary key
namestringLocation display name
descriptionstringLocation description
vendor_idstringParent merchant ID (PHP property: merchant_id)
slugstringURL-friendly identifier
statusstringLocation status
categorystringBusiness category override
location_typestringLocation type classification
addressobjectStructured address fields
gmaps_addressobjectGoogle Maps address data with components
pinpointobjectLatitude/longitude coordinates
country_codestringTwo-letter country code (BE, FR, NL, etc.)
currencystringCurrency code (EUR, USD, GBP, etc.)
timezonestringIANA timezone identifier
preferred_languagestringPreferred language for this location
business_hoursobjectWeekly business hours per day
restricted_datesarrayDates when location is closed
contact_informationobjectPhone, email, social media links
average_prep_timeintAverage 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_idstringBrandingProfile ID in tenant DB
payment_profile_idstringPaymentProfile ID
stripe_customer_idstringStripe customer identifier
viva_wallet_physical_source_codestringViva Wallet physical terminal source
viva_wallet_online_source_codestringViva Wallet online payment source
online_ordering_settingobjectOnline ordering configuration
online_settingsobjectGeneral online platform settings
in_house_settingobjectIn-house/dine-in configuration
qr_ordering_settingobject?QR table ordering configuration
receipt_settingobjectReceipt formatting and display settings
loyalty_subscription_idstring?Loyalty subscription reference
online_ordering_subscription_idstring?Online ordering subscription reference
is_sms_subscribedboolWhether SMS notifications are enabled
landing_page_cloudflare_image_idstring?Cloudflare image ID for landing page
external_dataobjectThird-party integration data
upvote_countintRestaurant suggestion upvotes

Tenant Database Models

Item

Collection: menus | Connection: tenant | Model discriminator: item | Soft delete: Yes

Products/menu items sold by the merchant.

FieldTypeDescription
_idObjectIdPrimary key
category_idstringParent category ID
location_idstringLocation this item belongs to
detailsobjectMulti-language name and description ({lang: {name, description}})
kitchen_namestring?Short name for kitchen display
priceMoneyBase price -- App\ValueObjects\Money, stored in cents
pricingobjectPlatform-specific pricing ({platform: Money}), also in cents
plustringPLU/barcode code
statusstringItem status (active, disabled, etc.)
tax_rate_codestringTax rate identifier
content_idstringContent/media reference
cloudflare_image_idstringCloudflare Images ID
variant_group_idstring?Variant group (e.g., sizes) reference
modifier_group_idsstring[]Associated modifier group IDs
display_group_idsstring[]Display groups this item appears in
upsell_group_idsstring[]Upsell groups this item belongs to
offer_idsstring[]Applied offer IDs
platformsstring[]Platforms where item is visible (kiosk, online, etc.)
ingredientsstring[]Ingredient labels
allergensstring[]Allergen labels
dietary_preferencesstring[]Dietary preference tags (vegan, vegetarian, etc.)
dietary_supplementsstring[]Dietary supplement tags
contains_alcoholboolWhether item contains alcohol
alcohol_typestringType of alcohol if applicable
minimum_ageintMinimum age requirement
calorie_countfloatCalorie information
prep_time_secondsintPreparation 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_timeboolWhether 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_daysintAdvance-order lead time in days (kiosk & online ordering)
available_pickup_daysstring[]Days this item may be picked up on
max_order_limitintMaximum quantity per order (0 = unlimited)
is_globalboolWhether this item is a global (multi-location) item
global_group_keystring?Key linking the per-location copies of a global item
product_typestringProduct type -- ItemFactory default 'simple'; Item::isCombo() matches 'combo' or 'pack'
combo_definition_idstring?Linked ComboDefinition when the item is a combo/formula
external_dataobjectThird-party integration data (keyed by provider)
external_idsobjectThird-party IDs (keyed by provider)
birthday_bonus_idstringLinked birthday bonus
reward_idsstring[]Linked loyalty reward IDs
raw_valueobjectRaw/unprocessed value data

Indices (Item::INDICES): location_id, status, variant_group_id, plu

Key traits: HasExternalIds, HasLocation, HasModifierGroup, PriceTrait

Collection: menus | Connection: tenant | Model discriminator: menu | Soft delete: Yes

Menu definitions that group display groups and items for a location.

FieldTypeDescription
_idObjectIdPrimary key
namestringMenu name
pos_namestringPOS-specific name
descriptionstringMenu description
location_idstringLocation this menu belongs to
statusstringMenu status
availability_typestringWhen menu is available -- AvailabilityOptions enum: location-default, specific-day-time, always-available (hyphenated; there is no Custom case)
availabilityobjectCustom availability schedule per day
visibilitystring[]Channels where menu is visible (kiosk, online_ordering, etc.)
device_profile_idsstring[]Device profiles that use this menu
display_group_idsstring[]Display groups in this menu
external_dataobjectThird-party integration data
published_hashstring?Hash of the last published menu state
has_draftboolWhether unpublished draft changes exist

Category

Collection: settings | Connection: tenant | Model discriminator: category | Soft delete: Yes

Item categories for organizing products.

FieldTypeDescription
_idObjectIdPrimary key
detailsobjectMulti-language name/description ({lang: {name, description}})
namestring?Legacy single-language name
content_idstring?Content/media reference
image_urlstringCategory image URL
item_countint?Cached count of items in this category
external_idsobjectThird-party IDs
external_dataobjectThird-party integration data
parent_idstring?Parent category (categories are hierarchical)
sort_orderintSort position among siblings (default 0)
pathstring[]Ancestor chain, path-to-self convention
statusstringOrdering-channel visibility -- CategoryStatuses, default Active. getEffectiveStatus() walks path so a restricted ancestor cascades to the whole subtree
lead_time_daysintAdvance-order lead time inherited by this category's items (default 0)
available_pickup_daysstring[]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.

FieldTypeDescription
_idObjectIdPrimary key
idempotency_keystringUnique key to prevent duplicate processing
all_idempotency_keysstring[]All idempotency keys across retries
invalid_idempotency_keysstring[]Invalidated keys
order_nostringHuman-readable order number (10-char random)
receipt_nostringReceipt number (YYMMDD + 6 alphanumeric)
unauthenticated_order_nostring?Order number for unauthenticated customers
location_idstringLocation where order was placed
device_idstringKiosk device ID (empty for online orders)
customer_idstring?CustomerMerchant ID (model customer_vendor)
customer_namestringCustomer display name
customer_first_namestringCustomer first name
customer_phonestringCustomer phone
customer_emailstringCustomer email
customer_tokenstringCustomer identification token
customer_addressobject?Delivery address
country_codestringCountry code
dining_optionstringDiningOptions 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_optionstring?Dining option as supplied by the ordering channel
order_channelstringChannel (kiosk, online_ordering, qr_ordering)
order_dateUTCDateTimeScheduled order date/time
order_statusstringKitchen status (preparing, ready, etc.)
statusstringPayment status (complete, pending, etc.)
typestringTransaction type
priorityboolPriority order flag
holdboolHold order flag
table_numberstringTable number for dine-in
table_section_idstring?TableSection this order belongs to
section_idstringSection identifier on the order
section_namestringSection display name on the order
pager_idstringPager device ID
order_session_idstringCart/order session identifier
cart_session_idstringCart session for stock reservations
qtyintTotal item quantity on the order
payment_snapshotobjectPayment provider details at time of payment
payment_providerstringPayment provider that handled the order
payment_methodstring?Payment method -- set by OnlineOrderingOrchestrator::storePayment for deferred methods (COD, invoice)
stripe_account_idstringStripe Connect account
stripe_invoice_idstring?Stripe invoice ID (invoicing flow)
stripe_invoice_item_idstring?Stripe invoice item ID (invoicing flow)
vat_numberstring?Customer VAT number (invoice orders)
company_namestring?Customer company name (invoice orders)
pos_relation_numberint?POS relation number for the customer
mplus_total_incl_at_creationstring?MplusKassa incl-VAT total captured at creation
all_viva_order_codesarrayEvery Viva Wallet order code seen for this order
snapshotsobjectFull order snapshot (items, location, language, rewards, offers)
reward_idsstring[]Applied loyalty reward IDs
item_labelstringItem source label (regular, hendrickx, etc.)
external_idsobjectThird-party order IDs
external_dataobjectThird-party order data
status_logsarrayHistory of status changes
lifecycle_logsarrayAppend-only order lifecycle timeline; entries are built by TransactionLifecycleLogService
sent_atUTCDateTime?When order was sent to kitchen
paid_atUTCDateTime?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_failedboolOrder failed to propagate to the POS
is_test_modeboolOrder was placed in test mode
order_prep_timeUTCDateTime?Estimated prep completion time
queue_numberint?Daily-reset receipt queue number
queue_daystring?Day the queue_number belongs to (reset key)
split_timeslotsarraySplit delivery timeslots
notestring?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)
devicestring?Device label shown in the transactions datatable
short_addressstring?Shortened delivery address shown in the transactions datatable
Money fields -- every row below except currency is an App\ValueObjects\Money, stored in cents:
currencystringCurrency code
subtotalMoneyPre-tax, pre-discount total
addonsMoneyModifier/addon charges
discountMoneyTotal discount amount
feesMoneyService fees
delivery_feeMoneyDelivery fee
pickup_transaction_feeMoneyPickup transaction fee
tip_amountMoneyCustomer tip
vatMoneyTotal tax amount
totalMoneyFinal 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.

ModelCollectionModel discriminatorDescription
ModifiermenusmodifierIndividual modifier options (extra cheese, etc.)
ModifierGroupmenusmodifier_groupGroups of related modifiers
DisplayGroupmenusdisplay_groupVisual grouping of items within a menu
VariantGroupmenusvariant_groupProduct variant groups (sizes, colors)
VariantOptionmenusvariant_optionIndividual variant options
OffermenusofferPromotional offers and discounts
GiftCardmenusgift_cardGift card records
ComboDefinitionmenuscombo_definitionCombo/formula definitions
UpsellGroupmenusupsell_groupUpsell groupings attached to items
LanguagesettingslanguageSupported language configurations
DeviceProfilesettingsdevice_profileDevice configuration and menu assignments
BrandingProfilesettingsbranding_profileColors, logos, visual branding
BillingProfilesettingsbilling_profileBilling/invoicing configuration
TableSectionsettingstable_sectionRestaurant table/seating sections
LoyaltysettingsloyaltyLoyalty program configuration
RewardsettingsrewardLoyalty rewards definitions
BirthdayBonussettingsbirthday_bonusBirthday bonus configuration
DomainProfilesettingsdomain_profileCustom-domain configuration
GiftCardTemplatesettingsgift_card_templateGift card templates
InactivePaymentProfilesettingsinactive_payment_profileRetired payment profile records
TranslationJobProgresssettingstranslation_job_progressBulk-translation job progress
QrCodeJobProgresssettingsqr_code_job_progressQR-code generation job progress
KdsStationsettingskds_stationKitchen Display station configuration
KdsProfilesettingskds_profileKitchen Display profile configuration
PrinterProfilesettingsprinter_profilePrinter configuration profiles
PosLayoutsettingspos_layoutFirst-party POS register layouts
Tillpos_tillspos_tillFirst-party POS till records
PosRegisterpos_registerspos_registerFirst-party POS register devices
PosDrawerpos_drawerspos_drawerCash drawer sessions
PosBusinessDaypos_business_dayspos_business_dayPOS business-day open/close records
FdmDevicepos_fdm_devicespos_fdm_deviceFiscal data module (FDM) devices
PosStaffCredentialpos_staff_credentialspos_staff_credentialPOS staff login credentials
OrderSessiontransactionsorder_sessionActive ordering sessions
SplitPaymentSessiontransactionssplit_payment_sessionSplit-payment session state
InvoicetransactionsinvoiceStripe Invoice Collection invoice bundling N transactions for one Mplus relation
LoyaltyPointtransactionsloyalty_pointCustomer loyalty point records
PointRedemptiontransactionspoint_redemptionLoyalty point redemption tracking
RewardRedemptiontransactionsreward_redemptionReward claim tracking
OfferRedemptiontransactionsoffer_redemptionOffer usage tracking
InventoryinventoriesinventoryStock levels per item per location
InventoryHistoryinventoriesinventory_historyStock change history log
ActivityLogactivity_logsactivity_logAudit log entries
Timelineactivity_logstimelineActivity timeline entries
DeviceLogactivity_logsdevice_logDevice-reported log entries
QueueCounteractivity_logsqueue_counterDaily queue numbers per location; atomic increment behind Transaction.queue_number
Contentcontents(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'.

ModelCollectionDescription
DeviceusersRegistered kiosk devices (central, not tenant)
SubscriptionsubscriptionsService subscriptions (central, not tenant)
SubscriptionLogsubscription_logsSubscription lifecycle history
PaymentProfilesettingsPayment provider configuration
AllowedIpSettingsettingsIP allow-list configuration
GuidedSetupVideoSettingsettingsGuided-setup video configuration
RolerolesPermission roles
TaxRate / TaxRateCustomtax_ratesTax rate definitions and custom overrides (central, not tenant)
ResellerresellersReseller companies
RepresentativerepresentativesSales representatives
Stand / StandBatchstands / stand_batchesField Ops stands and provisioning batches
FieldInvite / FieldSessionfield_invites / field_sessionsField Ops rep invitations and sessions
MplusRelationmplus_relationsRead-only local mirror of Mplus POS relations (customers)
NotificationnotificationsIn-app notifications
MerchantDailyStatmerchant_daily_statsPer-merchant daily aggregates
MerchantAiCreditLogvendor_ai_credit_logsAI credit consumption log
PurchaseLogpurchase_logsPurchase history log
WalletDeviceRegistrationwallet_device_registrationsApple/Google wallet pass device registrations
MediaAttachmentmedia_attachmentsPhoto/video captured by a field rep, stored in the R2 field-media bucket
JobMetricjob_metricsQueue job duration/wait metrics
RequestLoglogsHTTP request log entries
KioskAppUpdatelogsKiosk app update records
StuckTransactionstuck_transactionsTransactions flagged by the stuck-order reconciler
RestaurantSuggestion / RestaurantUpvoterestaurant_suggestionsRestaurant discovery suggestions and upvotes
CrmLeadIntakecrm_lead_intakesInbound CRM lead records
OdooErasureRequestodoo_erasure_requestsOdoo GDPR erasure requests
CachecachesMongoDB-backed cache entries

Customer Models

Every collection name here differs from the model name -- these were consolidated. Always filter on the model discriminator.

ModelCollectionConnectionModel discriminatorDescription
CustomerusersmongodbcustomerGlobal customer identity
CustomerAddresslocationsmongodbcustomer_addressGlobal customer addresses
CustomerMerchantusersmongodbcustomer_vendorMerchant-specific customer data (central, not tenant)
CustomerMerchantAddresslocationsmongodbcustomer_vendor_addressMerchant-specific delivery addresses
CustomerMerchantTokenactivity_logstenantcustomer_vendor_tokenCustomer authentication tokens
FCMTokenlogsmongodbfcm_tokenFirebase 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

ModelCollectionConnectionModel discriminatorDescription
ThirdPartyIntegrationlocationsmongodbthird_party_integrationIntegration configurations
ThirdPartyTransactiontransactionstenantthird_party_transactionSynced 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.

FactoryNotable Defaults
MerchantFactoryis_test: false, stripe_onboarding_completed: true, ai_photo_credits: 20, merchant_status: MerchantStatuses::Active (there is no VendorFactory)
UserFactorystatus: UserStatuses::Active, all_location_access: false, empty arrays for passkeys, trusted_devices; merchant_id is read from $data['vendor_id']
LocationFactoryDefault business hours, empty receipt settings, is_sms_subscribed: false; merchant_id is read from $data['vendor_id']
ItemFactoryprice: Money::fromCents(PriceConverter::normalize(0)), status: '', product_type: 'simple', use_default_prep_time: true, empty arrays for allergens/ingredients/modifiers
MenuFactorystatus: '', availability_type: '', has_draft: false
TransactionFactoryAll money fields default to Money::fromCents(PriceConverter::normalize(0)), empty arrays for snapshots/rewards
CategoryFactoryEmpty details, empty external data
DeviceFactoryDevice registration defaults
InventoryFactoryDefault stock values
LoyaltyFactoryLoyalty program defaults
OfferFactoryOffer configuration defaults
SubscriptionFactorySubscription lifecycle defaults

Key Patterns for AI Bug Fixing

  1. Check the connection constant. CONNECTION = 'mongodb' means central database, CONNECTION = 'tenant' means tenant database. Querying the wrong connection is a common bug.

  2. Polymorphic collection queries must filter by model. Nearly every collection is shared. Read COLLECTION and MODEL off the RawModel and filter on model -- do not infer the collection from the class name (DisplayGroup is in menus, PaymentProfile is in settings, Device is in users, ThirdPartyIntegration is in locations).

  3. The details object is required for multi-language. If a model has a details field, the default key must always be populated. Missing default causes null name displays.

  4. ObjectId handling. All _id fields and relationship IDs are stored as MongoDB ObjectIds. The toObjectIds() helper converts string arrays. Comparing a string ID to an ObjectId will fail silently.

  5. Soft delete awareness. Models with SOFT_DELETE = true are never physically deleted. Queries should account for deleted_at being null (non-deleted) or non-null (deleted). Repositories handle this automatically.

  6. Transaction snapshots are immutable. The snapshots field captures the full state at order time (items, prices, location settings, language). Do not assume snapshot data matches current entity state.

  7. External data is provider-keyed. external_data and external_ids are objects keyed by provider name (e.g., square, deliveroo, uber_eats). Always access via getExternalData('provider_name').

  8. Money is a value object, not a float. Item.price, Item.pricing and every Transaction money field are App\ValueObjects\Money, stored in cents. Never do float arithmetic on price / subtotal / total -- construct with Money::fromCents(PriceConverter::normalize($raw)) and compare/aggregate through Money. The MigratePricesToCents command is what moved the data off floats.

  9. Class names and collection names drifted apart. Vendor -> Merchant and CustomerVendor* -> 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.