Appearance
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 theproductionbranch. The backend is MongoDB, so there are no migrations — field names come from theapp/RawModels/*.phpconstructors, request classes, andapp/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:linecitations 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:34location_id, required FK inStoreMenuRequest.php:32). - A Menu references Display Groups, not categories:
Menu.php:33display_group_ids. A Display Group can belong to multiple menus (DisplayGroup.php:42menu_ids, many-to-many —DisplayGroup::getMenus()DisplayGroup.php:324).ShowMenuResourcebuilds a menu purely from display groups, with no categories involved (ShowMenuResource.php:23,31). - A Display Group holds its items via
DisplayGroup.php:38item_ids; items reverse-reference viaItem.php:92display_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-58parent_id/sort_order/path). It has nomenu_idand a Menu has no category reference (grep confirmed:Category.phphas zeromenurefs,Menu.phphas zerocategorrefs). 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:
| Provides | Field / accessor |
|---|---|
| Timezone | timezone (Location.php:142); getTimezone() Location.php:423 (fallback Europe/Brussels) |
| Currency | currency (Location.php:119); getCurrency() Location.php:225 |
| Address | address (Location.php:113), gmaps_address (Location.php:120); getFullAddress() Location.php:251 |
| Business hours | business_hours (Location.php:116); getBusinessHours() Location.php:207 |
| Payment profile | payment_profile_id (Location.php:133); getPaymentProfile() Location.php:697 |
| Online ordering settings | online_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).
Menus
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_ids ↔ DisplayGroup.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 atOnlineOrderingOrchestrator.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).
Menu Items
Depends on:
- Location —
Item.php:96location_id. - Category (optional) — the field is
category_id(Item.php:80), notparent_id. It is effectively nullable: request rulenullable(StoreItemRequest.php:104-112), coerced tonullwhen missing (ItemService.php:165-166), andgetCategory()returnsnullfor a falsy id (Item.php:635). - Tax Rate (optional) — the field is
tax_rate_code(Item.php:110). Request rulenullable|string(StoreItemRequest.php:113), coerced tonullwhen 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), notmodifier_group_ids. The persisted Mongo field ismodifier_group_ids(Item.php:98); the request→model mapping happens inItemService::syncRelations(ItemService.php:228-240). - Display Groups —
Item.php:92display_group_ids(the customer-facing grouping; see Menus). - Channels —
Item.php:101platforms(array), values fromChannelOptions(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 arecontent_id(Item.php:87) andcloudflare_image_id(Item.php:88), populated byItemService.php:138-139. (So the input field isimage;content_idis 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 enumDeliveryRegionOptionshas three values —Postal Code,Radius,Distance(DeliveryRegionOptions.php:7-9; defaultRadius). WhentypeisPostal 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 }, pluscustom_fees: [{postal_code, fee}]— a per-postal-code fee override, validated atStoreOnlineOrderingRequest.php:302-304and applied at checkout only whendelivery_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_visit— notper_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 carriesdiscount_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/couponsAPI route. The back-office pagesrc/pages/marketing/coupons.vueis 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-12→AiPhotoController(/ai-photo/generate,/status/{contentId},/credits). Back office calls it viasrc/store/modules/aiPhoto.ts. - PhotoRoom background removal / enhancement ("Photo Studio") code exists (
PhotoStudioController.php,PhotoRoomService.php) but is orphaned — no route references it (grep acrossroutes/,app/,config/,bootstrap/). The back-office utilitysrc/utils/photoroom.tsstill POSTs to/photo-studio/remove-backgroundand/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/passwordDisplay Groups
Defined at MENU level, independent of categories:
DisplayGroup.php:42menu_ids(many-to-many with menus),DisplayGroup.php:38item_ids.- Items reverse-reference via
Item.php:92display_group_ids. - No
kiosk/online_ordering/qr/visibility/channelfield exists on the DisplayGroup model. Channel gating is on the item (Item.php:101platforms) and on the menu (Menu.php:38visibility). 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 byStoreOnlineOrderingRequest. Edited at/online/online-ordering. See Backoffice → Online Ordering.online_settings(Location.php:129;getOnlineSettings()Location.php:456) — operational settings. Validated byStoreOnlineSettingsRequest, defaultConstants.php:262getDefaultOnlineSettings(). 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 collections —
saveIntoThirdPartyDisplayGroup()/saveIntoUpvendoDisplayGroup()store the Shopify collection id intoDisplayGroup.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:34location_id,:39settings). - Hendrickx + Vanhoutte are two flavors of one protocol (enum
KassanetProviderOptions). - Menu import of items, categories, and display groups — jobs
ImportKassanetCategoriesJob,ImportKassanetProductsJob,ImportKassanetDisplayGroupsJob, chained fromImportKassanetMenuJob. - Table sections wired to Kassanet —
TableSection::isKassanetSection()(TableSection.php:195); table numbers synced via integrationsettings(TableNumbers/GetFreeTableNumberAPIs).
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/refunds → PosRefundController 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, collectionusers) — minimal identity. - Per-merchant
CustomerMerchant— centralmongodbDB, collectionusers, the same connection and collection as the globalCustomer; the two are discriminated only by theMODELvalue (customer_vendorvscustomer) (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):
- Location (foundation — sets timezone, currency, address, business hours)
- Tax Rates (
/menus/tax-rates) - Categories (
/menus/categories, optional grouping) - Menu Items (
/menus/items) - Modifiers (
/menus/modifiers) - Menus + Display Groups (
/menus/menu-builder) - Payments (
/settings/payments— assign a payment profile) - Online Ordering (
/online/online-ordering— incl. delivery region/fee) - Online Settings (
/online-settings— incl. order capacity) - Devices (
/device-management— kiosk, KDS) - 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_idare affected. - Order-capacity category limits referencing that
category_idare affected. - Category status can cascade onto effective item visibility at render time.
- Items inheriting the category's
lead_time_days/available_pickup_dayschange 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).