Skip to content

Store Management (Admin) ​

Flow ID: AD-18 Module(s): eshop, Order (modern domain) Complexity: Medium Last Updated: 2026-04-04

Business Context ​

Physical store management serves two purposes: (1) administering brick-and-mortar shop locations that customers can select for in-store pickup during checkout, and (2) managing map-based location directories with coordinates for storefront display. The legacy admin controller manages the store/store_mui tables directly, while the modern layer exposes full CRUD via REST for both stores and map locations.

When a customer selects "Pickup from Store" at checkout (useAddress = ORDER_ADDRESS_ESHOP, constant value '2'), the selected store's ID is saved on the order as store_id. This bypasses shipping cost calculation (transport cost = 0) and clears shipping address fields. The store record is then used in order confirmation emails, thank-you pages, order history, SMS/Viber notifications, and admin order views.

Two Distinct Entities ​

The codebase has two separate store-related systems that should not be confused:

ConceptTablesDomainPurpose
Pickup Storesstore + store_muiOrder\StorePhysical shops for checkout pickup, order emails, admin order management
Map Locationslocation + location_mui (+ maps + maps_mui)Map\Location + Map\MapGeographic location directory with lat/lon, grouped into named maps

Both have legacy admin controllers and modern domain+REST layers, but they operate on different tables with different schemas.


Legacy Admin Controller (Pickup Stores) ​

Controller: ecommercen/eshop/controllers/Adv_stores_admin.php (226 lines) Application wrapper: application/modules/eshop/controllers/Stores_admin.php (extends Adv_stores_admin) Model: ecommercen/eshop/models/adv_stores_model.php (160 lines)

Authorization: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN (checked in constructor). Admin menu: Listed under the SETTINGS group at route eshop/stores_admin.

Operations ​

MethodPurpose
index()Lists all stores ordered by order ASC, joined with current language MUI. Includes drag-and-drop sortable JS.
add()Creates a new store with master data (country, county, postal, phone, mobile, active) and MUI data (address, city, region, opening_hours) per language.
edit($id)Updates existing store. Loads master + all MUI captions. Uses updateOrInsertMui() for upsert behavior.
markActive($storeId)Sets active = true, redirects to index.
markInactive($storeId)Sets active = false, redirects to index.
updateOrder()AJAX drag-and-drop reorder. Receives listItem[] array, updates order column sequentially.

Validation Rules ​

All rules defined in validation($isUpdate):

Master fields:

  • postal -- required, trimmed
  • country -- required, trimmed (2-char ISO code, default 'GR')
  • county -- required, trimmed
  • phone -- required, trimmed
  • mobile -- optional, trimmed
  • active -- optional, trimmed (boolean)

MUI fields (per language via set_rules_mui):

  • address_{lang} -- required, trimmed
  • city_{lang} -- required, trimmed
  • region_{lang} -- optional, trimmed
  • opening_hours_{lang} -- required, trimmed

Database Schema ​

store (master):

ColumnTypeNotes
idint(11) PKAuto-increment
activeint(1)Default 1, indexed
countryvarchar(2)Default 'GR'
countyvarchar(255)
postalvarchar(10)
phonevarchar(255)
mobilevarchar(255)
orderint(1)Display sort order, default 0

store_mui (translations):

ColumnTypeNotes
idint(11) PKAuto-increment
store_idint(11) FKIndexed, composite index with lang
addressvarchar(255)
cityvarchar(255)
regionvarchar(255)
opening_hoursvarchar(255)
langvarchar(2)Language abbreviation

Modern Domain Layer (Pickup Stores) ​

Namespace: Advisable\Domains\Order\StoreDI registration: src/Domains/Order/container.php

Entity Properties ​

Entity: id, active, country, county, postal, phone, mobile, orderMuiEntity: id, store_id, address, city, region, opening_hours, lang

Repository Relations ​

RelationTypeTarget
translationsONE_TO_MANYMuiRepository via store_id
countryResourceBELONGS_TOCountry\Country\Repository via country
countyResourceBELONGS_TOCountry\County\Repository via county

ListRequest Filters and Sorts ​

Filters: id (exact), active (exact), priority (exact, maps to store.order), country (exact), county (exact), city.{locale} (partial, MUI), address.{locale} (partial, MUI)

Sorts: id, active, priority, country, county, city.{locale}, address.{locale}

Relation sorts (translations): city, address

WriteData Fields ​

active (int), country (string), county (string), postal (string), phone (string), mobile (string), order (int)

MuiWriteData Fields ​

lang (string, required), address (string), city (string), region (string), openingHours (string, maps to opening_hours)

Validator ​

Currently minimal -- only validates that lang is present on each translation entry. No field-level required checks in the modern layer (the legacy controller enforces required fields via CI form validation).


REST API (Pickup Stores) ​

Controller: src/Rest/Order/Controllers/Store.phpDI registration: src/Rest/Order/container.php

Endpoints ​

MethodPathActionAuth
GET/rest/order/storeindex() -- paginated collectionguest
GET/rest/order/store/itemitem() -- single by filterguest
GET/rest/order/store/{id}show($id) -- single by IDguest
POST/rest/order/storestore() -- createbackend (ADMIN)
POST/rest/order/store/{id}update($id) -- updatebackend (ADMIN)
DELETE/rest/order/store/{id}destroy($id) -- deletebackend (ADMIN)

Locale-prefixed variants (/{lang}/rest/order/store/...) are also registered.

REST policies (application/config/rest_policies.php): Default auth: backend, roles: [AUTH_ROLE_ADMIN]. Read methods (index, show, item) are auth: guest.

Resource Schema ​

OrderStoreResource: id, active (bool), country, county, postal, phone, mobile, priority (int, from order column). Includes optional translations (array of OrderStoreMuiResource), countryResource, countyResource.

OrderStoreMuiResource: id, storeId, address, city, region, openingHours, lang.


Map / Location System (Separate Entity) ​

Namespace: Advisable\Domains\Map\Location and Advisable\Domains\Map\MapDI registration: src/Domains/Map/container.php

Database Schema ​

location: id, map_id (FK), country (varchar(2)), latitude, longitude, image, activelocation_mui: id, location_id (FK), title, address, city, zip_code, description, langmaps: id, activemaps_mui: id, map_id (FK), title, slug, lang

Relations ​

Map: translations (ONE_TO_MANY MUI), locations (ONE_TO_MANY Location) Location: translations (ONE_TO_MANY MUI), map (BELONGS_TO Map)

REST Endpoints ​

MethodPathActionNotes
GET/rest/map/mapCollectionFilters: id, active, title.{locale}, slug.{locale}
GET/rest/map/map/{id}SingleIncludes locations and translations relations
POST/rest/map/mapCreateSupports multipart for image upload
POST/rest/map/map/{id}UpdateSupports multipart for image upload
DELETE/rest/map/map/{id}Delete
GET/rest/map/locationCollectionFilters: id, active, country, city.{locale}, postalCode.{locale}
GET/rest/map/location/{id}Single
POST/rest/map/locationCreateRequires mapId
POST/rest/map/location/{id}Update
DELETE/rest/map/location/{id}Delete

The Map controller uses HandlesUploadActions for image upload support (files/maps/ path). The Location controller uses HandlesWriteActions (no file uploads).


Checkout Integration (Pickup Stores) ​

The store selection integrates into the checkout flow at multiple points:

Order Placement ​

  1. Store list loading: Adv_order::getStoresToPick() calls stores_model->getStores() (cached via PSCache). Returns all stores joined with current language MUI.

  2. Active store filtering: filterActiveStores() helper (ecommercen/helpers/store_helper.php) removes inactive stores. getArrayOfStores() builds an id => label map using buildStoreLabel() with a configurable pattern (default: address city, country - postal county).

  3. Checkout validation: generateStoreRule() dynamically builds validation rules:

    • Compiles list of active store IDs
    • If payway = 'paid_at_store' OR useAddress = ORDER_ADDRESS_ESHOP: store is required and must be in_list[] of active IDs
    • Also validates callback_validateMinimumCartTotalForPaidAtStore and callback_paywayCheckValidation
  4. Order record creation (Adv_order_model):

    • store_id = orderData['store'] when useAddress == ORDER_ADDRESS_ESHOP, otherwise null
    • transport_id is set to null (no transporter needed for pickup)
    • Transport cost is forced to 0
    • Shipping address fields are cleared (empty strings)

Post-Order Processing ​

  • Order confirmation email: Store record attached as $co_data['store'] via getDbStoreCached($storeId) (PSCache wrapper around stores_model->getRecord())
  • Thank-you page: Store data passed to view for display
  • SMS/Viber notifications: Store-specific message templates (payway.from.store.sms.order.message.*, viber.order.from_store.message)
  • Order status display: When store_id is set and status is SENT, admin shows special label SENT+store with distinct styling

Address Constants ​

php
define('ORDER_ADDRESS_SHIPPING', '0');  // Ship to separate address
define('ORDER_ADDRESS_BILLING', '1');   // Ship to billing address
define('ORDER_ADDRESS_ESHOP', '2');     // Pickup from store

JobPurpose
AdvSendEmailAndSMSBasedOnFromStoreStatusSends "order ready at store" email + SMS for orders with store_id and matching status. Supports Plivo, Yuboto, Bulker, OmniMessaging, Routee providers.
AdvAddPointsToCustomerFromStoreAwards loyalty points for invoiced store-pickup orders (status = 'INVOICED' and store_id IS NOT NULL).

Business Rules ​

RuleDescription
Active/inactive visibilityOnly active stores appear in checkout pickup selection
Order priorityorder column determines display sequence in checkout dropdown
Pickup = zero shippingWhen useAddress = ORDER_ADDRESS_ESHOP, transport cost is forced to 0 and no transporter is assigned
Store required for pickupStore ID is required when payway is paid_at_store or delivery mode is store pickup
Store on order is immutableOnce store_id is saved on the order, it persists for email, SMS, reporting, and order history
Reporting groupingAdmin reporting can group orders by store_id to show per-store order volumes
No deletion guardLegacy controller has no delete method; modern REST allows DELETE but no foreign key check against shop_order.store_id