Skip to content

Cross-Feature Relations

How Upvendo features depend on and interact with each other.

Scope & verification: Relationships below are verified against the backend (upvendo-backend) and back office (upvendo-backoffice), both on the production branch. The backend is MongoDB, so there are no migrations — field names come from the app/RawModels/*.php constructors, request classes, and app/Constants.php. Anything that depends on the customer-facing storefront, the kiosk Android app, or the proxy lives in separate repos and is tagged Unverified. file:line citations are relative to whichever repo the path belongs to.

Real data model (the actual menu hierarchy)

The menu hierarchy is not Menu → Category → Item. The real wiring is:

LOCATION (location_id everywhere)

   ├── Menu (location_id)  ──many-to-many──►  Display Group (menu_ids, item_ids)
   │                                                  │
   │                                                  ▼
   └── Item (location_id, category_id?, display_group_ids, modifier_group_ids, platforms)

                 └── Category (independent self-referential tree via parent_id)
  • A Menu belongs to one Location (Menu.php:34 location_id, required FK in StoreMenuRequest.php:32).
  • A Menu references Display Groups, not categories: Menu.php:33 display_group_ids. A Display Group can belong to multiple menus (DisplayGroup.php:42 menu_ids, many-to-many — DisplayGroup::getMenus() DisplayGroup.php:324). ShowMenuResource builds a menu purely from display groups, with no categories involved (ShowMenuResource.php:23,31).
  • A Display Group holds its items via DisplayGroup.php:38 item_ids; items reverse-reference via Item.php:92 display_group_ids. This is the customer-facing grouping unit, defined at menu level, independent of category.
  • A Category is an independent self-referential hierarchy (Category.php:56-58 parent_id/sort_order/path). It has no menu_id and a Menu has no category reference (grep confirmed: Category.php has zero menu refs, Menu.php has zero categor refs). Categories are used for grouping/reporting on items, not for building the customer menu.

Feature Dependencies

Locations (foundation)

Every feature is location-scoped via a location_id field (Device.php:51, DeviceProfile.php:60, Item.php:96, Menu.php:34, Inventory.php, etc.).

The Location model (Location.php constructor, lines 111-156) carries:

ProvidesField / accessor
Timezonetimezone (Location.php:142); getTimezone() Location.php:423 (fallback Europe/Brussels)
Currencycurrency (Location.php:119); getCurrency() Location.php:225
Addressaddress (Location.php:113), gmaps_address (Location.php:120); getFullAddress() Location.php:251
Business hoursbusiness_hours (Location.php:116); getBusinessHours() Location.php:207
Payment profilepayment_profile_id (Location.php:133); getPaymentProfile() Location.php:697
Online ordering settingsonline_ordering_setting (Location.php:127) and online_settings (Location.php:129) — two separate objects

Business Hours

Lives on the Location model — there is no separate Business Hours model. Field business_hours (Location.php:116), default shape Constants::$DEFAULT_BUSINESS_HOURS.

Required by:

  • Online ordering / QR ordering schedules fall back to business hours (Location::getOnlineOrderingSettingSchedule() / getQrOrderingSettingSchedule() Location.php:585-604).
  • Device schedules: Device::getDeviceSchedule() returns the location's business hours for the DEFAULT schedule type (Device.php:274-283).
  • Order Capacity time-slotting runs over the ordering schedule (which falls back to business hours).

Depends on: Locations — a menu belongs to exactly one location (Menu.php:34 location_id, required in StoreMenuRequest.php:32).

Relates to: Display Groups (many-to-many via Menu.php:33 display_group_idsDisplayGroup.php:42 menu_ids). Menus do not contain categories.

Channel visibility: a Menu carries visibility (Menu.php:38, an array of channel values). Unverified: how the storefront/kiosk apply this visibility.

Back office: managed under Menus (/menus/menu-builder).


Categories

Independent hierarchy. A Category is a self-referential tree (Category.php:56-58 parent_id/sort_order/path). It is not owned by a menu and is not part of the customer-facing menu build.

Relates to:

  • Items — an item optionally references one category (see Menu Items below). The link is item → category, not category → menu.
  • Order Capacity — capacity rules can limit specific categories (real; see Order Capacity below).
  • Advance ordering — a Category carries lead_time_days + available_pickup_days (Category.php:63-65), folded onto its items at read time. The item wins where it sets a value; the category fills in only what the item leaves unset, and the two fields are independent (Item.php:351-360). Enforced at order time (KioskOrchestrator::assertItemsAvailableForPickup() KioskOrchestrator.php:143, and the online-ordering equivalent at OnlineOrderingOrchestrator.php:235).
  • Reporting / sales-by-category grouping.

Category status cascades onto effective item visibility at render time (DisplayGroup.php:398, :476; Item::getEffectiveStatus Item.php:464) — a runtime visibility cascade, not a structural assignment.

Back office: Menus → Categories (/menus/categories).


Depends on:

  • LocationItem.php:96 location_id.
  • Category (optional) — the field is category_id (Item.php:80), not parent_id. It is effectively nullable: request rule nullable (StoreItemRequest.php:104-112), coerced to null when missing (ItemService.php:165-166), and getCategory() returns null for a falsy id (Item.php:635).
  • Tax Rate (optional) — the field is tax_rate_code (Item.php:110). Request rule nullable|string (StoreItemRequest.php:113), coerced to null when missing (ItemService.php:161-162). An item can have no tax rate.

Relates to:

  • Modifier Groups — the request field is modifier_groups (an array of {id} objects: StoreItemRequest.php:150-160), not modifier_group_ids. The persisted Mongo field is modifier_group_ids (Item.php:98); the request→model mapping happens in ItemService::syncRelations (ItemService.php:228-240).
  • Display GroupsItem.php:92 display_group_ids (the customer-facing grouping; see Menus).
  • ChannelsItem.php:101 platforms (array), values from ChannelOptions (Kiosk, Online Ordering, Table Qr Ordering, POS, etc.; ChannelOptions.php:7-13). Channel availability of an item is gated here, on the item, not on the display group.
  • Image — the request input is image (image.source, image.file: StoreItemRequest.php:102-103). The stored references are content_id (Item.php:87) and cloudflare_image_id (Item.php:88), populated by ItemService.php:138-139. (So the input field is image; content_id is a real model field, not a myth — the two are different layers.)
  • Inventory — see Inventory below (derived, not a boolean on the item).

Modifiers

Relates to Items via the item's modifier_group_ids (Item.php:98), set from the modifier_groups request array (StoreItemRequest.php:150-160).

Item references Modifier Groups (modifier_group_ids)

Customer selects options                                 (_Unverified_: storefront/kiosk)

Modifier prices contribute to the order total            (computed at checkout)

Back office: Menus → Modifiers (/menus/modifiers).


Variant Groups

A real feature with backend endpoints (routes/api/backoffice/variant-groups.php), but in the back office it is UI-gated to test environments or Square-integrated merchants:

  • isTestEnv = !['production','staging'].includes(import.meta.env.VITE_ENV) (src/config/helpers.ts:384).
  • showVariantGroups = isTestEnv || !!userData.value?.is_square_integrated (src/views/items/Items.vue:45).

So in production, only Square-integrated merchants see variant groups in the UI. The backend itself has no env/Square gate (variant-groups.php:7-11 guards only on permissions) — the gating is UI-only.


Order Capacity

Not a standalone model — it lives on the Location under online_settings.order_capacity (Location::getOnlineSettings() Location.php:456; read in OrderCapacityService.php:320). Default shape: Constants.php:190 $DEFAULT_ORDER_CAPACITY_SETTINGS.

Real fields include: enabled, time_slot_duration_min (default 15), limit_orders_per_time_slot + number_of_orders_per_time_slot, limit_items_per_time_slot + number_of_items_per_time_slot, flexible-limit fields, enable_limit_specific_categories + limit_specific_categories[] (each {category_id, number_of_items_per_time_slot}), and time_specific_rules[].

References Categories (real, not invented): category_id is validated against the Category model in StoreOnlineSettingsRequest.php:125-136 and category names are hydrated in app/RawModels/Traits/HasOrderCapacity.php:37-43.

Back office: a section inside Online Settings (/online-settings), not its own route (OrderCapacityManagement.vue).


Delivery (region + fee, on the online-ordering setting)

There is no standalone Delivery Zones back-office page — delivery region and fee are configured inside online_ordering_setting:

  • delivery_region (Constants.php:609-614): { type, postal_codes: [], radius, max_distance }. Region mode enum DeliveryRegionOptions has three values — Postal Code, Radius, Distance (DeliveryRegionOptions.php:7-9; default Radius). When type is Postal Code, delivery_region.postal_codes[] is required (StoreOnlineOrderingRequest.php:284-286) and the storefront rejects addresses whose postal code is not in the list (zestidoo-online-ordering/src/utils/helpers.ts:251-264).
  • delivery_fee (Constants.php:615-623): { type, minimum_order_amount_for_free_delivery, fee, distance_pricing_enabled, distance_pricing_type, distance_fees: [], flat_rate_per_km }, plus custom_fees: [{postal_code, fee}] — a per-postal-code fee override, validated at StoreOnlineOrderingRequest.php:302-304 and applied at checkout only when delivery_region.type === 'Postal Code' (TransactionService.php:518-531). It is not part of the default shape; it is defaulted to [] on read (Location.php:500).

Postal-code zones themselves are real. What does not exist is a per-zone minimum-order amount or per-zone delivery time — postal_codes is a flat list, and the only per-postal-code override is the fee. Back office: a DeliverySettings section inside /online/online-ordering, not a dedicated route.


Payments

Depends on Locations — a location points at a PaymentProfile via payment_profile_id (Location.php:133; getPaymentProfile() Location.php:697). The PaymentProfile (PaymentProfile.php:41-50) carries processor, country_code, external_data, stripe_account_id, is_active, is_default; reverse lookup getLocations() (PaymentProfile.php:228).

Per-device payment terminal (different layer): on the Device model, viva_wallet_terminal_id (Device.php:54) and the Square terminal via external_data (getSquareTerminalId() Device.php:213). The Stripe Terminal reader is stripe_terminal_id — a real Device constructor field with an accessor (Device.php:70, getStripeTerminalId() :182), hydrated by DeviceFactory.php:41, written when a reader is paired (DeviceService.php:762, cleared at :848) and looked up by the Stripe webhook orchestrator (StripeWebhookOrchestrator.php:241).

Back office: Settings → Payments (/settings/payments, profiles at /settings/payments/profiles/:id).


Loyalty

Depends on:

  • Locations / merchant — loyalty program per merchant; redemption availability is per-location.
  • Customers — points accrue against the customer.

Real fields (Loyalty.php:45-46, StoreLoyaltyRequest.php:110,113):

  • points_earned_each_dollar (amount-based program)
  • points_earned_each_visitnot per_visit

Program type enum LoyaltyPrograms (AMOUNT_BASED / VISIT_BASED).

POS-gated in production (default-deny): Location::supportsLoyaltyRedemption() (Location.php:1024) returns true only when no POS is integrated, otherwise reads config("pos-providers.providers.{provider}.supports_loyalty_redemption", false). Enabled for hendrickx, vanhoutte, mpluskassa, square (config/pos-providers.php); explicitly false for shopcaisse; lightspeed has no key → disabled. Gate applied at TransactionService.php:1088.

Unlike offers, loyalty discounting is not item-label gated — the loyalty block sits outside the REGULAR branch (TransactionService.php:806-814) and runs for POS-labelled carts too. Its only gate is isLoyaltySubscribed() && supportsLoyaltyRedemption() (TransactionService.php:1088), which is what keeps Shopcaisse and Lightspeed out while Square, MplusKassa and Kassanet (Hendrickx / Vanhoutte) do reach it.

Back office: Marketing → Loyalty (/marketing/loyalty).

Loyalty program (points_earned_each_dollar / points_earned_each_visit)

Order placed via a POS that supports redemption (or no POS)   (gated)

Discount calculated and applied in the checkout pipeline
(any item label — not restricted to REGULAR)

Customer can redeem on future orders                          (_Unverified_: storefront redemption UI)

Offers & Promotions

Depends on: Locations; optionally targets Items / Categories; can be customer-assigned.

Coupons are not a separate feature. A "coupon" is just an Offer with method Code:

  • OfferMethods: CODE = 'Code', AUTOMATIC = 'Automatic' (OfferMethods.php:7-8). The code-method offer carries discount_code (Offer.php:62).
  • OfferTypes: ITEMS, ORDER, BOGO (OfferTypes.php:7-9).
  • Routes live only under /offers (routes/api/backoffice/offers.php:7-16). There is no /marketing/coupons API route. The back-office page src/pages/marketing/coupons.vue is an empty stub (renders <div></div>, no logic).

Offers ↔ Loyalty: there is no "loyalty offer" type and no data link between Offers and Loyalty. They are independent systems that merely stack in the same checkout pipeline (TransactionService::calculateTotalsAndDiscounts applies offer discounts, then loyalty discounts, to the same running total; a coordinating comment is at app/Services/Loyalty/LoyaltyService.php:838).

Offers — unlike loyalty — only apply in the REGULAR item-label path (TransactionService.php:784 for the totals roll-up, offers processed at :1154); there is no per-provider config flag for offers.

Back office: Marketing → Offers (/marketing/offers).


Inventory

Depends on: Location + Item. The Inventory model (Inventory.php) has item_id, item_name, location_id, quantity, reserved_quantity, expiry_date, item_plu — stock is tracked per item per location.

There is no boolean track_inventory column on the item. "Tracked" is derived from the existence of an Inventory row: Item::getTrackQuantity() returns (bool) $this->getInventory() (Item.php:669), serialized as track_quantity (Item.php:931). getStock() is at Item.php:654.

Back office: Inventory → Overview (/inventory/overview), with history at /inventory/history.


Team

Scope is per-merchant, with per-location access. Team members are User records: User.php:57 merchant_id, User.php:60-61 role_ids + location_ids, User.php:63 all_location_access. getLocationIds() (User.php:203) returns the explicit list, or all merchant locations when all_location_access is set.

Permissions live on the merchant-scoped Role (Role.php:25 permissions; Role.php:27 merchant_id), linked to users via role_ids.

Activity Log (ActivityLog.php, tenant-scoped) records action, actor, actor_name, loggable_name, loggable_type, snapshot. It only references the actor user loosely (getActorName() ActivityLog.php:47) and has no model-level relation to Roles/Permissions and no location_id.

Back office: Settings → Team (/settings/team/users, /settings/team/roles).


Devices (Kiosk, KDS, Printer)

Depends on: Location + (optionally) a Device Profile. Device fields (Device.php constructor): name, location_id (:51), device_profile_id (:44), type (:59), viva_wallet_terminal_id (:54). Device types (DeviceTypes.php): Kiosk, KitchenDisplay (KDS), POS, Printer.

Menus reach a kiosk via the device profile, not the device. The Device has no menu field; the profile supplies it (DeviceProfile::getDefaultMenuId() DeviceProfile.php:93; Device::getProfile() Device.php:363). See Backoffice → Kiosk.

Back office: Device Management (/device-management/devices, profiles at /device-management/profiles).


Photo / AI Images

Two distinct image features — do not conflate them:

  • AI photo generation is the live, wired feature: routes/api/backoffice/ai-photo.php:6-12AiPhotoController (/ai-photo/generate, /status/{contentId}, /credits). Back office calls it via src/store/modules/aiPhoto.ts.
  • PhotoRoom background removal / enhancement ("Photo Studio") code exists (PhotoStudioController.php, PhotoRoomService.php) but is orphaned — no route references it (grep across routes/, app/, config/, bootstrap/). The back-office utility src/utils/photoroom.ts still POSTs to /photo-studio/remove-background and /photo-studio/edit-photo, which resolve to nothing. Treat PhotoRoom "Photo Studio" as not active in production.

Both paths ultimately write the item image references: images upload to Cloudflare Images (CloudflareImageService.php uploadToCloudflareImages() app/Services/Common/CloudflareImageService.php:167) and the result is stored as the item's content_id + cloudflare_image_id (Item.php:87-88, ContentTrait.php).


Passkey Authentication

Depends on: a user account — passkeys are stored on the User (User.php:73 passkeys; getPasskeys() User.php:285).

Backend: PasskeyService.php (WebAuthn), AuthController, routes routes/api.php:145-150 (/back-office/passkeys setup/list/delete) plus guest auth at routes/api/guest.php:36-37. The RP ID is the backoffice host (parse_url(config('app.backoffice_url'), PHP_URL_HOST) in PasskeyService.php:83,109,174,216).

Back office: Profile → Passkeys (/profile/passkeys, PassKeys.vue, uses @simplewebauthn/browser).

User registers passkey at Profile → Passkeys

Passkey stored on the User record (WebAuthn, RP ID = backoffice host)

On login, user can authenticate with the passkey instead of email/password

Display Groups

Defined at MENU level, independent of categories:

  • DisplayGroup.php:42 menu_ids (many-to-many with menus), DisplayGroup.php:38 item_ids.
  • Items reverse-reference via Item.php:92 display_group_ids.
  • No kiosk/online_ordering/qr/visibility/channel field exists on the DisplayGroup model. Channel gating is on the item (Item.php:101 platforms) and on the menu (Menu.php:38 visibility). The display group has channel-specific serializers (DisplayGroup::onlineOrderingSerialize() / kioskSerialize()) that filter by item platforms.

Required by (rendering is in separate apps — Unverified here): the storefront, kiosk app, and QR ordering consume the per-channel serialized menu. This doc only verifies the back-office/back-end structure.

Back office: managed within the menu builder (DisplayGroupForm.vue), not a standalone route.


Online Settings

The Location has two separate online objects:

  • online_ordering_setting (Location.php:127; getOnlineOrderingSetting() Location.php:493) — the channel config (pickup/delivery, delays, scheduling, delivery region/fee, payment methods, idle timeout, send-to-POS, snooze). Validated by StoreOnlineOrderingRequest. Edited at /online/online-ordering. See Backoffice → Online Ordering.
  • online_settings (Location.php:129; getOnlineSettings() Location.php:456) — operational settings. Validated by StoreOnlineSettingsRequest, default Constants.php:262 getDefaultOnlineSettings(). Edited at /online-settings (OnlineSettingsController.php:28).

Fields actually on online_settings (Constants::getDefaultOnlineSettings() Constants.php:262-299): order_capacity, delivery_available_days, pickup_available_days, collect_tips, allow_notes (per-item notes), allow_order_notes (order-level notes, gated independently), default_menu_id, extra_menu_ids. Idle timeout, print settings, and customer-info requirements are not on this object (idle timeout lives on online_ordering_setting).


Shopify Integration

Exists (ShopifyIntegrationService.php, ShopifyIntegrationController.php, routes/api/backoffice/shopify.php, back office src/pages/shopify/).

Verified relations:

  • Bidirectional product sync — export (syncJobAsync()) and import (importJobAsync()); endpoints /sync-menu, /import-menu.
  • Display Groups ↔ Shopify collectionssaveIntoThirdPartyDisplayGroup() / saveIntoUpvendoDisplayGroup() store the Shopify collection id into DisplayGroup.external_ids[provider] (DisplayGroup.php:44).

Removed (fabricated): "Categories ↔ Shopify collections." There is no code path mapping a Category to a Shopify collection (only TODO comments about product_type). The collection mapping is to Display Groups, not Categories.

Storefront flow of Shopify orders is Unverified here.


Kassanet Integration (Hendrickx / Vanhoutte)

Exists (AbstractKassanetService.php + HendrickxService.php / VanhoutteService.php; KassanetIntegrationService.php, KassanetIntegrationController.php, routes/api/backoffice/kassanet.php, back office src/views/hendrickx/*).

Verified relations:

  • Per-location connection — stored on ThirdPartyIntegration (ThirdPartyIntegration.php:34 location_id, :39 settings).
  • Hendrickx + Vanhoutte are two flavors of one protocol (enum KassanetProviderOptions).
  • Menu import of items, categories, and display groups — jobs ImportKassanetCategoriesJob, ImportKassanetProductsJob, ImportKassanetDisplayGroupsJob, chained from ImportKassanetMenuJob.
  • Table sections wired to Kassanet — TableSection::isKassanetSection() (TableSection.php:195); table numbers synced via integration settings (TableNumbers / GetFreeTableNumber APIs).

Corrected: Kassanet has no "floor plan" concept (unlike Lightspeed K-Series). It is table-number sync, not floor-plan linkage.

Order push to POS and KDS delivery are Unverified (downstream of the POS).


Transactions, Tips, Refunds, Gift Cards

Tips

Tip amount = a percentage of a subtotal base (PriceTrait::calculateTips() PriceTrait.php:300-318). The base is country-dependent (TransactionService.php:1225):

  • US: pre-tax — base resolves to totalExcludingVAT (VAT excluded).
  • Non-US: post-VAT — base resolves to the tax-inclusive discounted total (totalBeforeTip).

The calculate_tips setting (CalculateTipsOptions BEFORE_TAXES / AFTER_TAXES) is only a stored label — it is written/validated (Constants.php:246,287, Location.php:1505, Device.php:569) but never read in any pricing/tip calculation (grep across app/Services and app/Traits returns zero reads). So BEFORE/AFTER_TAXES is display metadata, not the actual computation base.

Refunds

Not merchant-wired. The REFUND_TRANSACTIONS permission and TransactionTypes::Refund enum exist, but there is no refund request class, back-office refund controller method, or back-office order/transaction refund route. One refund route does exist on production: the first-party POS cash refund, POST /pos/refundsPosRefundController behind pos.staff (routes/api/pos.php:34-35, cash-only v1). It is reachable only by a first-party POS register, which no merchant can currently acquire self-serve, so it changes nothing for the back office. By-amount refunds exist only via the Viva terminal (VivaWalletService::refundTerminalTransaction()) reachable through a test route (routes/api/guest.php:174). There is no by-item refund logic. Stripe is webhook-consumer only; Square/MplusKassa refund initiation is unimplemented. Treat refunds as a stub.

Gift Cards

On production, only gift-card selling is wired: gift_card_purchases are processed and added to the order total (TransactionService.php:1173-1176, added to the total at :1238; request field CreateIntentRequest.php:143-158).

Gift-card redemption is not wired: GiftCardService::redeemGiftCard() (app/Services/GiftCard/GiftCardService.php:109) has no callers, there is no redemption input field, and there is no per-tender split anywhere (POS payment builders emit a single payment line). So if/when redemption reduces an order total, the POS sees only the reduced total with no gift-card tender line — a structural revenue over-count risk for non-Square POS providers. (The Mplus/Kassanet tender-split work is not on the production branch.)

Customers

Two-level model:

  • Global Customer (Customer.php, collection users) — minimal identity.
  • Per-merchant CustomerMerchantcentral mongodb DB, collection users, the same connection and collection as the global Customer; the two are discriminated only by the MODEL value (customer_vendor vs customer) (CustomerMerchant.php:31-35, Customer.php:14-18). customer_id → global; getParentCustomer() CustomerMerchant.php:387.

Back-office edits do not propagate globally. CustomerService update (the back-office one, app/Services/BackOffice/CustomerService.php:262-272 — there are two classes with this name) writes only CustomerMerchantRepository::save(), never the global CustomerRepository. Explicit comment: edits stay merchant-specific, intentional for GDPR. (Create does link to a global Customer, but edits never write back.)


Circular / bidirectional relationships

  • Items ↔ Inventory — an item's "tracked" state is derived from an Inventory row existing; inventory in turn references the item (item_id).
  • Orders ↔ Order Capacity — capacity limits orders/items per slot; placing orders consumes capacity.
  • Customers ↔ Loyalty — points accrue against the per-merchant customer.

Configuration order

A sensible setup order for a new location (note: there is no standalone "Delivery Zones" step — delivery is part of Online Ordering; Order Capacity is part of Online Settings):

  1. Location (foundation — sets timezone, currency, address, business hours)
  2. Tax Rates (/menus/tax-rates)
  3. Categories (/menus/categories, optional grouping)
  4. Menu Items (/menus/items)
  5. Modifiers (/menus/modifiers)
  6. Menus + Display Groups (/menus/menu-builder)
  7. Payments (/settings/payments — assign a payment profile)
  8. Online Ordering (/online/online-ordering — incl. delivery region/fee)
  9. Online Settings (/online-settings — incl. order capacity)
  10. Devices (/device-management — kiosk, KDS)
  11. Loyalty / Offers (/marketing/loyalty, /marketing/offers — optional, POS-gated for loyalty)

Impact analysis

Changing Business Hours (on the Location)

  • Order-capacity slots are recalculated against the ordering schedule.
  • Online-ordering and device schedules that fall back to business hours update.

Changing a Category

  • Items linked via category_id are affected.
  • Order-capacity category limits referencing that category_id are affected.
  • Category status can cascade onto effective item visibility at render time.
  • Items inheriting the category's lead_time_days / available_pickup_days change availability.

Disabling an Item

  • The item's platforms / effective status hides it from the affected channels.
  • Offers targeting the item are affected.
  • Unverified: how an in-progress storefront cart reacts.

Changing a Price

  • New orders use the new price.
  • Unverified: cart-snapshot and channel-rendering behavior (storefront/kiosk repos).