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