Appearance
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:
| Concept | Tables | Domain | Purpose |
|---|---|---|---|
| Pickup Stores | store + store_mui | Order\Store | Physical shops for checkout pickup, order emails, admin order management |
| Map Locations | location + location_mui (+ maps + maps_mui) | Map\Location + Map\Map | Geographic 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
| Method | Purpose |
|---|---|
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, trimmedcountry-- required, trimmed (2-char ISO code, default'GR')county-- required, trimmedphone-- required, trimmedmobile-- optional, trimmedactive-- optional, trimmed (boolean)
MUI fields (per language via set_rules_mui):
address_{lang}-- required, trimmedcity_{lang}-- required, trimmedregion_{lang}-- optional, trimmedopening_hours_{lang}-- required, trimmed
Database Schema
store (master):
| Column | Type | Notes |
|---|---|---|
id | int(11) PK | Auto-increment |
active | int(1) | Default 1, indexed |
country | varchar(2) | Default 'GR' |
county | varchar(255) | |
postal | varchar(10) | |
phone | varchar(255) | |
mobile | varchar(255) | |
order | int(1) | Display sort order, default 0 |
store_mui (translations):
| Column | Type | Notes |
|---|---|---|
id | int(11) PK | Auto-increment |
store_id | int(11) FK | Indexed, composite index with lang |
address | varchar(255) | |
city | varchar(255) | |
region | varchar(255) | |
opening_hours | varchar(255) | |
lang | varchar(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
| Relation | Type | Target |
|---|---|---|
translations | ONE_TO_MANY | MuiRepository via store_id |
countryResource | BELONGS_TO | Country\Country\Repository via country |
countyResource | BELONGS_TO | Country\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
| Method | Path | Action | Auth |
|---|---|---|---|
| GET | /rest/order/store | index() -- paginated collection | guest |
| GET | /rest/order/store/item | item() -- single by filter | guest |
| GET | /rest/order/store/{id} | show($id) -- single by ID | guest |
| POST | /rest/order/store | store() -- create | backend (ADMIN) |
| POST | /rest/order/store/{id} | update($id) -- update | backend (ADMIN) |
| DELETE | /rest/order/store/{id} | destroy($id) -- delete | backend (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
| Method | Path | Action | Notes |
|---|---|---|---|
| GET | /rest/map/map | Collection | Filters: id, active, title.{locale}, slug.{locale} |
| GET | /rest/map/map/{id} | Single | Includes locations and translations relations |
| POST | /rest/map/map | Create | Supports multipart for image upload |
| POST | /rest/map/map/{id} | Update | Supports multipart for image upload |
| DELETE | /rest/map/map/{id} | Delete | |
| GET | /rest/map/location | Collection | Filters: id, active, country, city.{locale}, postalCode.{locale} |
| GET | /rest/map/location/{id} | Single | |
| POST | /rest/map/location | Create | Requires 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
Store list loading:
Adv_order::getStoresToPick()callsstores_model->getStores()(cached via PSCache). Returns all stores joined with current language MUI.Active store filtering:
filterActiveStores()helper (ecommercen/helpers/store_helper.php) removes inactive stores.getArrayOfStores()builds anid => labelmap usingbuildStoreLabel()with a configurable pattern (default:address city, country - postal county).Checkout validation:
generateStoreRule()dynamically builds validation rules:- Compiles list of active store IDs
- If
payway = 'paid_at_store'ORuseAddress = ORDER_ADDRESS_ESHOP: store is required and must bein_list[]of active IDs - Also validates
callback_validateMinimumCartTotalForPaidAtStoreandcallback_paywayCheckValidation
Order record creation (
Adv_order_model):store_id = orderData['store']whenuseAddress == ORDER_ADDRESS_ESHOP, otherwisenulltransport_idis set tonull(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']viagetDbStoreCached($storeId)(PSCache wrapper aroundstores_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_idis set and status isSENT, admin shows special labelSENT+storewith 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 storeRelated Jobs
| Job | Purpose |
|---|---|
AdvSendEmailAndSMSBasedOnFromStoreStatus | Sends "order ready at store" email + SMS for orders with store_id and matching status. Supports Plivo, Yuboto, Bulker, OmniMessaging, Routee providers. |
AdvAddPointsToCustomerFromStore | Awards loyalty points for invoiced store-pickup orders (status = 'INVOICED' and store_id IS NOT NULL). |
Business Rules
| Rule | Description |
|---|---|
| Active/inactive visibility | Only active stores appear in checkout pickup selection |
| Order priority | order column determines display sequence in checkout dropdown |
| Pickup = zero shipping | When useAddress = ORDER_ADDRESS_ESHOP, transport cost is forced to 0 and no transporter is assigned |
| Store required for pickup | Store ID is required when payway is paid_at_store or delivery mode is store pickup |
| Store on order is immutable | Once store_id is saved on the order, it persists for email, SMS, reporting, and order history |
| Reporting grouping | Admin reporting can group orders by store_id to show per-store order volumes |
| No deletion guard | Legacy controller has no delete method; modern REST allows DELETE but no foreign key check against shop_order.store_id |
Related Flows
- CF-06 Order Preview -- store selection in checkout
- CF-28 Maps -- map/location system for geographic directories (separate entity from pickup stores)
- AD-03 Order Management -- store display on orders, store-based address handling
- AD-10 Reporting -- store-based order grouping
- AD-39 Country Management -- country/county references for store addresses
- CF-11 Customer Account -- order history with store display
- SY-23 MUI Translation Pattern --
store_mui,location_mui, andmaps_muicompanion tables provide per-language address and description fields - SY-25 File Upload & Storage -- Map entity uses
HandlesUploadActionsfor image uploads tofiles/maps/