Appearance
POS / Phone Order Creation
Flow ID: AD-54 | Module(s): eshop | Complexity: Medium Last Updated: 2026-05-29
Business Context
The "POS" (Point of Sale) / phone order screen lets administrators create orders on behalf of customers who place them by phone, in person, or via any channel outside the storefront. Despite the label, POS is not a separate module — it is the exact same add() method of Adv_orders_admin used for any manual or walk-in order. Only the ScribeHow tooltip, the dashboard quick tile, and an admin menu label tie the URL to the "POS" name.
The admin selects or creates a customer, searches for products via AJAX, picks shipping/billing addresses, chooses a payment method, and submits. On submit, the controller rebuilds a "fake cart" from POST inputs, runs a relaxed version of the checkout pipeline, and persists the order with a distinct set of default values (webignore=1, status=PENDING_ACCEPTED, order_currency=EUR, immediate stock decrement).
API Reference
REST Endpoints
No REST API. POS order creation is a legacy admin-only feature.
Legacy Admin Routes
| Route | Controller | Method | HTTP | Description |
|---|---|---|---|---|
orders_admin/add | Adv_orders_admin | add() | GET/POST | Manual / phone / walk-in order creation form and submission (Adv_orders_admin.php:474-582) |
orders_admin/repeat/{id} | Adv_orders_admin | repeat($orderId) | GET | Pre-fill the add form from an existing order (Adv_orders_admin.php:2913) |
orders_admin/edit/{id} | Adv_orders_admin | edit($id) | GET/POST | Edit a persisted order (Adv_orders_admin.php:1131) |
- Application wrapper:
application/modules/eshop/controllers/Orders_admin.php(class Orders_admin extends Adv_orders_admin {}). - Form view:
application/views/admin/orders/create.php(534 lines). - ScribeHow embed lives at
application/views/admin/orders/create.php:7-14. The tooltip label "Τηλεφωνική Παραγγελία (POS)" comes fromscribe.how.orders.pos.title/scribe.how.orders.pos.iframe.linkatecommercen/language/greek/adv_scribe_lang.php:10,111. - Admin side menu entry at
application/config/admin_menu.php:393-399(groupORDERS). - Admin dashboard quick tile ("manual") at
application/views/admin/panel.php:79-86links toorders_admin/add.
AJAX Search Endpoints
The add-order form (create.php) uses route #1 for its live product picker. The other four exist for related admin pickers or Vue admin pages and are documented here so future work avoids confusing them.
| # | Route | File:Line | POST / Body Params | Response | Used By |
|---|---|---|---|---|---|
| 1 | orders_admin/searchOrdersProduct | Adv_orders_admin.php:1311-1338 | queryString, shippingCountry, invoice | HTML fragment application/views/ajaxparts/admin_search_order_product.php — tile grid; tiles call select_this(productCodeId); tiles disabled for inactive / soft-deleted / out-of-stock (unless negative stock allowed) products | POS create form (primary) |
| 2 | orders_admin/productAjaxOrderItem | Adv_orders_admin.php:1343-1356 | product_code_id, shippingCountry, invoice | HTML fragment ajaxparts/admin_add_order_item appended into .insert-item; contains hidden inputs product[<productId>][price], product[<productId>][weight], etc. | POS create form (append single item) |
| 3 | orders_admin/productOrderItemsValue | Adv_orders_admin.php:1358-1375 | productCodeIds[] (array), shippingCountry, invoice | JSON [{productCodeId, finalPrice}, ...] | POS create form (refresh prices when address / invoice toggles) |
| 4 | adminrun/search_append_order | application/controllers/Adminrun.php:1140-1159 | queryString | HTML fragment via Adv_admin_live_search_model::liveSearchAppendOrder($term, 21) (Adv_admin_live_search_model.php:9-47). Does not rebind VatForOrder | Related products / bundle editor pickers — not the POS form |
| 5 | adminrun/apiSearchProducts | application/controllers/Adminrun.php:959-985 | JSON body {"data": "<term>"} via inputStream() | JSON | Vue admin pages — assets/admin/js/common/components/CoreAdminProductsSearch.vue:115 via assets/admin/js/api.js:10. Not the POS form |
Endpoints #1–#3 rebind the VatForOrder singleton on every request (Adv_orders_admin.php:1317, :1350, :1366) so subsequent price computations reflect the country + invoice flag currently selected in the form.
The DB-level helper Adv_admin_live_search_model::liveSearchAppendOrder() at Adv_admin_live_search_model.php:9-47 matches shop_product_mui.name via LIKE, or product_codes.product_code, shop_product_barcodes.barcode, product_id exactly. The POS create form uses the sibling helper product_model->searchProductsForOrder($term), which returns the same shape but lives on Adv_product_model.
Code Flow
High-Level Sequence
GET orders_admin/add -- render form (create.php)
POST orders_admin/add -- Adv_orders_admin::add() at :474-582
|
+--> Read POST product[] array
+--> $this->validation() -- :481, sets form rules
| (validation rules defined at :611-671 — relaxed)
|
+--> Derive $deliveryCountry from 'sameaddress': -- :485-497
| ORDER_ADDRESS_SHIPPING (0) -> sendto_country
| ORDER_ADDRESS_BILLING (1) -> country
| ORDER_ADDRESS_ESHOP (2) -> 'GR'
|
+--> VatForOrder::getInstance() -- :498-500
| ->setInvoice($paymerch === 'invoice')
| ->setDeliverAreaType($deliveryCountry)
|
+--> setOrderAddressData() -- :502, normalise address fields
+--> $this->fakeCart = order_model->createAdminFakeCart($products) -- :503
|
+--> form_validation->run() -- :507, evaluate the rules set by :481
+--> order_model->recheckCart($this->fakeCart, true) -- :509, admin_negative_stock_qty
|
+--> Customer resolution -- :513-531
| 1) customer_id POSTed -> orderAddUpdateCustomerData()
| 2) lookup by email (or synthesized uniqid() + order.invalid.email)
| 3) create via add_customer_front()
|
+--> processGiftSelections($otherPostElements) -- :533
+--> setDeliveryWarningMessage($this->fakeCart, true) -- :535
|
+--> $orderSerial = order_model->create_order_admin($this->fakeCart, $otherPostElements) -- :541
+--> addOrderNote() -- :544
+--> If payway === 'paybybank': -- :546-548
| createAdminPayByBankOrder($order)
| +--> PayByBank::createOrder()
| +--> order_model->set_is_paid($serial) on success
+--> afterOrderSuccessHooks($order->id) -- :550, ERP push + cancel traits
+--> sendOrderCompleteEmail() -- :553, gated by registry
+--> sendSmsSuccess() -- :554, empty stub
+--> redirect(site_url('orders_admin')) -- :556Fake Cart Builder
The "fake cart" is literal — there is no shared admin cart session, no Fake_cart library, and no AdminCart class. Line items live as DOM hidden inputs in the admin's browser until submit:
html
<!-- application/views/admin/orders/js_add_item.php:13-14 -->
<input name="product[<productId>][price]" ...>
<input name="product[<productId>][weight]" ...>On submit, Adv_orders_admin::add() at Adv_orders_admin.php:476-503 reads $products = $this->input->post('product') and calls $this->order_model->createAdminFakeCart($products).
The builder is Adv_order_model::createAdminFakeCart($productsData) at ecommercen/eshop/models/Adv_order_model.php:1471-1571. It:
- Reads registry keys
OTHER / ORDER_EDIT_KEEP_PRODUCT_VALUESandPOINT_SYSTEM / ORDER_EDIT_KEEP_PRODUCT_POINTS. - Loads live product data, VAT, product codes, images, and MUI rows.
- Applies Rule 13 "cheapest free" gift adjustments to paid quantity.
- Builds per-item arrays:
productCodeId, productId, quantity, price, name, subtotal, original_price, discount_price, discount_string, vat_rate_captured, vendor_code, points, options, shelfcode(shelfcode viaproduct_shelfcode()). - Aggregates
total_weight, total_items, cart_total, cart_total_vat. - Returns the array.
Storage: the fake cart exists only as:
- Hidden inputs in the admin's browser DOM until submit.
- The in-memory PHP array
$this->fakeCartfor the duration of theadd()request. - After
create_order_admin(),serialize()d intoshop_order.cart_contents, then unserialized intoshop_order_basketrows byAdv_order_model::processOrder()atAdv_order_model.php:801-843.
The customer's storefront session cart is never read or touched.
Cart Validation
Adv_orders_admin::add() calls $this->order_model->recheckCart($this->fakeCart, true). The $forAdmin = true flag at Adv_order_model.php:1214-1253 switches to the admin_negative_stock_qty config key, which is more permissive than the storefront's negative_stock_qty. It still rejects items where !$product->active || $product->soft_delete.
VAT Rebinding
VatForOrder is a singleton rebuilt multiple times per request so product and cart prices always reflect the currently-selected delivery country and invoice flag.
Files
- Application stub:
application/modules/eshop/libraries/VatForOrder.php(class VatForOrder extends AdvVatForOrder {}). - Real implementation:
ecommercen/eshop/libraries/AdvVatForOrder.php(extendsSingleton).
Rebinding Sites
| Site | File:Line | Trigger |
|---|---|---|
Start of add() on submit | Adv_orders_admin.php:498-500 | POST submit |
| Product search tile grid | Adv_orders_admin.php:1317 | AJAX #1 |
| Append single product item | Adv_orders_admin.php:1350 | AJAX #2 |
| Refresh item prices | Adv_orders_admin.php:1366 | AJAX #3 |
| Gifts-for-products recalc | Adv_orders_admin.php:1553-1555 | Gift rule recompute |
| Edit flow | Adv_orders_admin.php:1144,1172-1174 | setVatForOrderBasedOnOrder($record) at :3339 |
enableOrderVatManipulation — default FALSE
AdvVatForOrder::__construct() reads config_item('enableOrderVatManipulation'), which defaults to FALSE in application/config/app.php:524.
When false (the default), VatForOrder is a pass-through. POS orders use the standard product VAT rates regardless of delivery country. The country-specific branches below are only reachable in deployments that explicitly enable the flag.
For the full OrderDeliverAreaType enum mapping, dead-code analysis (GreeceReduced unreachable from POS, invoiceVat() TODO), and the complete AdvVatForOrder pipeline, see AD-50 VAT Management.
Order Finalization & Field Defaults
create_order_admin
Signature: Adv_order_model::create_order_admin($fakeCart, $otherPostElements, $calculateTransportationCost = true) at Adv_order_model.php:1581-1821.
Forced field values:
| Field | Value | Notes |
|---|---|---|
cart_contents | serialize($fakeCart) | Unserialized into shop_order_basket rows later by processOrder() at :801-843 |
webignore | $otherPostElements['webignore'] ?? 1 | Default 1 for manual/phone. See below. |
total_qty | $totalitems | |
status | 'PENDING_ACCEPTED' | Default. Exception: payway === 'paybybank' → 'PENDING' |
entry_date | $otherPostElements['entry_date'] ?? time() | |
entry_datetime | date('Y-m-d H:i:s', $entry_date) | |
order_currency | 'EUR' | Hard-coded — POS ignores session currency |
After persisting, it calls _proccess_admin_order($adminOrderData), then SmartPoint and DHL/ASAP external-rate handling. Finally it calls removeOrderStock($processedOrder->order_serial) at Adv_order_model.php:1819 — stock decrement is unconditional at creation time regardless of payway.
php
// Adv_order_model.php:783-793
public function removeOrderStock($orderSerial)
{
$orderCart = $this->order_basket_model->getQuantitiesByOrderSerial([$orderSerial]);
foreach ($orderCart as $item) {
$this->db->set('stock', "stock - {$item->qty}", false)
->where('id', $item->product_code_id)
->update($this->tableProductCodes);
}
}Combined with admin_negative_stock_qty, an admin can push stock arbitrarily negative.
webignore — the POS / manual flag
Schema: shop_order.webignore int(1) NOT NULL DEFAULT 0 at database/initial/initial.sql:1310, with index at :1396.
- Storefront default: 0 (DB default, never overridden).
create_order_admindefault: 1.
Labels: 'eshop.admin.order.webignore.true' => 'Manual entry', 'eshop.admin.order.webignore.false' => 'Online entry' at ecommercen/language/english/adv_advisable_lang.php:492-493.
Used as a filter discriminator in the order list dropdown at application/views/admin/orders/list.php:286-303 with options Online(0) / Telephone(1) / Marketplace(2). The filter is applied at Adv_order_model.php:2187-2195:
sql
WHERE shop_order.webignore = filters.typeSemantically it means "ignore on web" — it flags rows so any downstream storefront-sync module can skip them. Note that Webrun.php:217,219 uses payway-based logic (COD formatting), not webignore.
Payment / paid status
statusdefaultsPENDING_ACCEPTED(vs storefrontPENDING).- Exception:
payway = 'paybybank'→status = 'PENDING', thenAdv_orders_admin::createAdminPayByBankOrder()at:714-741callsPayByBank::createOrder()and, on success,order_model->set_is_paid($order->serial). paid/is_paidis not set bycreate_order_adminitself. It stays at the DB default 0 except for PayByBank success.- No auto-mark-paid for
delivery,byphone,paid_at_store, orbank_transferat creation time. Admins mark orders paid later via the batch actionset_batch_paid_2atAdv_order_model.php:1887. - COD detection in views:
($payway == 'delivery' || $payway == 'byphone') && $status !== 'PAID'→ displayed as "ΑΝΤΙΚΑΤΑΒΟΛΗ ΜΕΤΡΗΤΟΙΣ" (cash on delivery).
Admin payways
Admin payways come from the METHODS.PAYWAY_ADMIN registry key (ecommercen/eshop/libraries/AdvPaymentsRegistry.php:11,367), rendered as radio buttons at application/views/admin/orders/create.php:55-77. This is a different key from METHODS.PAYWAY used by the storefront, so admins can expose payways that are not available online.
Post-submit hooks
php
// Adv_orders_admin.php:541-557
$orderSerial = $this->order_model->create_order_admin($this->fakeCart, $otherPostElements);
$order = $this->order_model->getRecord(['conditions' => ['order_serial' => $orderSerial]]);
$this->addOrderNote($otherPostElements, $order->id);
if ($order->payway === 'paybybank' && !$this->createAdminPayByBankOrder($order)) {
return;
}
$this->afterOrderSuccessHooks($order->id); // ERP + cancel traits
$otherPostElements = $this->getDataForOrderCompleteEmail($order->id, $otherPostElements);
$this->sendOrderCompleteEmail($otherPostElements);
$this->sendSmsSuccess($otherPostElements);
redirect(site_url('orders_admin'));- The order-complete email is gated by the registry key
EMAIL.SEND_EMAIL_TO_CLIENT_FOR_ADMIN_ORDERatAdv_orders_admin.php:2283-2290. sendSmsSuccess()at:1914-1917is an empty stub kept for client overrides.- ERP push runs via
OrderForErpHookFireTrait(Adv_orders_admin.php:27).
Customer Linking & Walk-in Creation
Resolution logic at Adv_orders_admin.php:513-531:
php
if (empty($this->input->post('customer_id'))) {
$mail = $this->input->post('mail') ?: uniqid() . $this->config->item('order.invalid.email');
$customerId = $this->customer_model->getRegisteredCustomerIdByEmail($mail);
if ($customerId) {
$otherPostElements = $this->orderAddUpdateCustomerData($customerId, $otherPostElements);
} else {
$customerData = $this->setUpCustomerData(true, $otherPostElements['useAddress'], $mail);
$otherPostElements['customer_id'] = $this->customer_model->add_customer_front($customerData);
$otherPostElements = array_merge($otherPostElements, $customerData);
}
} else {
$otherPostElements = $this->orderAddUpdateCustomerData($this->input->post('customer_id'), $otherPostElements);
}3-tier resolution
- Explicit
customer_idPOSTed (from the dropdown) →orderAddUpdateCustomerData()updates the master customer record with the form data. - No
customer_id, email provided → look up by email. If found, master-record update as above. - No match (or no email) → synthesize a placeholder email
uniqid() . $this->config->item('order.invalid.email')and create a new customer viacustomer_model->add_customer_front()(ecommercen/eshop/models/Adv_customer_model.php:79-91), which generates an encrypted password fromuniqid().
Master-record overwrite gotcha
Selecting an existing customer silently overwrites that customer's master record with the form data, via orderAddUpdateCustomerData → customer_model->updateCustomerAdmin(). There is no UI warning. Administrators need to be careful when reusing an existing customer for a one-off address — any differences they enter will persist on the customer.
Customer dropdown data
The dropdown is populated client-side via adminrun/getCustomerData at Adminrun.php:1184-1202, which calls Adv_admin_live_search_model::liveSearchCustomerData() at :138-157.
is_admin checkbox → guest vs registered
setUpNewCustomerData() at Adv_orders_admin.php:2384-2391:
php
return [
'mail' => $mail,
'password' => uniqid(),
'is_guest' => !$this->input->post('is_admin')
];The create form has a pre-checked is_admin checkbox at application/views/admin/orders/create.php:265 ('checked' => true, label eshop.admin.order.isAdmin):
- Checked (default) →
is_guest = 0— creates a real account. - Unchecked →
is_guest = 1— creates a guest record.
Differences vs Storefront Checkout
| Aspect | Storefront (CF-06 / CF-07) | POS / Phone (AD-54) |
|---|---|---|
| Validation | Full — required mail, postal, phones, region, AFM/ΔΟΥ/company format, VIES | Relaxed (Adv_orders_admin.php:611-671) — only trim/required on name, surname, address, city, county, country, payway. No mail, postal, phones, region required. AFM/ΔΟΥ/company only trim, no format check, no VIES |
| Stock config key | negative_stock_qty | admin_negative_stock_qty (more permissive) |
| Coupon checks | Full (checkCoupon() at :2529-2558) | Full for existing customers; new POS customers have no audiences yet so audience-restricted coupons fail. checkCouponClone() at :2564-2590 relaxes for clone case |
| Loyalty points spend | Automatic | Only for existing customers via redeemOtherPostElementsPointsAdd() at :2235-2248 → loyalty->removePointsFromCustomer($customer_id). New POS customers ignored |
| Loyalty points earn | Automatic at order completion | NOT automatic — requires orders_admin/addPointsToCustomer/{serial} at :1824-1841 (manual per-order) or addPointsSelected batch at :1872-1886. Gated by POINT_SYSTEM.IS_ENABLED |
| Delivery cost calc | transfer_cost() | transfer_cost_admin(); zero when useAddress == ORDER_ADDRESS_ESHOP |
| Default status | PENDING | PENDING_ACCEPTED (except paybybank → PENDING) |
| Stock decrement timing | Payway-dependent (deferred for many paywais) | Unconditional at creation |
order_currency | Session currency | Hard-coded 'EUR' |
webignore | 0 | 1 |
| Delivery warning | setDeliveryWarningMessage($cart, false) | setDeliveryWarningMessage($this->fakeCart, true) |
| Payway source | METHODS.PAYWAY registry | METHODS.PAYWAY_ADMIN registry (different key) |
| Order-complete email | Always (subject to notification config) | Gated by EMAIL.SEND_EMAIL_TO_CLIENT_FOR_ADMIN_ORDER |
Data Model
Phone / POS orders are stored in the same shop_order table as storefront orders. The distinguishing flag is:
shop_order.webignore— default 1 for POS/manual, 0 for storefront. See thewebignoresection above.
Unserialized line items land in shop_order_basket exactly like storefront orders.
Architecture
| Component | Path | Purpose |
|---|---|---|
Adv_orders_admin | ecommercen/eshop/controllers/Adv_orders_admin.php | Admin orders controller — add() at :474-582, AJAX product endpoints at :1311-1375 |
Orders_admin (wrapper) | application/modules/eshop/controllers/Orders_admin.php | Thin app-level wrapper, overrideable in client repos |
Adv_order_model | ecommercen/eshop/models/Adv_order_model.php | createAdminFakeCart() at :1471-1571, create_order_admin() at :1581-1821, recheckCart() at :1214-1253, removeOrderStock() at :783-793 |
Adv_customer_model | ecommercen/eshop/models/Adv_customer_model.php | add_customer_front() at :79-91, customer lookup and update |
Adv_admin_live_search_model | ecommercen/eshop/models/Adv_admin_live_search_model.php | liveSearchAppendOrder() at :9-47, productItem() at :87-123, liveSearchCustomerData() at :138-157 |
VatForOrder (stub) | application/modules/eshop/libraries/VatForOrder.php | App-level stub class VatForOrder extends AdvVatForOrder {} |
AdvVatForOrder | ecommercen/eshop/libraries/AdvVatForOrder.php | Singleton VAT calculator — reads enableOrderVatManipulation (default FALSE → pass-through) |
| Admin menu | application/config/admin_menu.php:393-399 | Sidebar entry for orders_admin/add, group ORDERS |
| Dashboard tile | application/views/admin/panel.php:79-86 | "manual" quick-tile link |
| Form view | application/views/admin/orders/create.php | 534-line form; AJAX driven |
| Line-item template | application/views/admin/orders/js_add_item.php:13-14 | Hidden-input template for fake cart rows |
Configuration
- Admin menu entry:
application/config/admin_menu.php:393-399(groupORDERS). - VAT flag:
enableOrderVatManipulationinapplication/config/app.php:524(default FALSE — POS VAT is pass-through). - Admin-only payway registry key:
METHODS.PAYWAY_ADMIN(AdvPaymentsRegistry.php:11,367). - Invalid email domain:
$this->config->item('order.invalid.email')— appended touniqid()when no mail is provided. - Stock permissiveness:
admin_negative_stock_qtyconfig key (vs storefrontnegative_stock_qty). - Email gate: registry
EMAIL.SEND_EMAIL_TO_CLIENT_FOR_ADMIN_ORDER(Adv_orders_admin.php:2283-2290). - Loyalty gate:
POINT_SYSTEM.IS_ENABLED. - Edit-mode preservation:
OTHER.ORDER_EDIT_KEEP_PRODUCT_VALUES,POINT_SYSTEM.ORDER_EDIT_KEEP_PRODUCT_POINTS(read bycreateAdminFakeCart).
RBAC
All POS access gates are enforced in the constructor at Adv_orders_admin.php:39-47:
php
[AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_ORDERS]Role constants from application/config/constants.php:99-108:
| Constant | Value |
|---|---|
AUTH_ROLE_ADVISABLE | 1 |
AUTH_ROLE_ADMIN | 3 |
AUTH_ROLE_ORDERS | 6 |
The same gate is applied at the menu level (admin_menu.php:397). There are no per-method overrides — the constructor covers all methods including the AJAX endpoints. Users whose only roles are PRODUCTS, CMS, MARKETING, REPORTING, MEDIA, or DEVELOPER cannot place phone orders.
Receipts / Printing
There is no thermal printer, ESC-POS, PrintNode, Epson / Star, or receipt-printer integration anywhere in the codebase. Searches for escpos, ESC/POS, receipt_printer, PrintNode, star_printer, and thermal print return only 'transporters.label.thermal.printing' => 'Thermal Printing' at ecommercen/language/english/adv_advisable_lang.php:2263, which labels a transporter voucher option — not a POS receipt printer.
All "printing" is browser-side: HTML or PDF rendering followed by the browser's native Print dialog.
| Method | File:Line | Purpose |
|---|---|---|
invoice($orderSerial) | Adv_orders_admin.php:777-829 | HTML page with Picqer barcode and optional voucher embed (application/views/admin/orders/invoice*) |
printSelected($selected) | Adv_orders_admin.php:1763-1790 | invoice_batch view; if printSelectedAsPdf, renders via Dompdf and streams as orders_<datetime>.pdf |
download_invoice($orderSerial) | Adv_orders_admin.php:3427 | Dompdf PDF download |
publicDownloadPdf($orderCode) | Adv_orders_admin.php:3350 | Public PDF download |
printVoucher($providerId, $voucher) | Adv_orders_admin.php:2087 | Transporter voucher |
printVouchersPdf(...) | Adv_orders_admin.php:2065 | Batch voucher PDFs |
printAcsPrickUpList(...) | Adv_orders_admin.php:2438 | ACS pickup list |
If a client deployment needs a thermal receipt printer, it has to be added from scratch in the client repo — no upstream scaffolding exists.
Client Extension Points
- Override controller: extend
Orders_admininapplication/modules/eshop/controllers/(or the client repo equivalent) to customise anyadd()step. - Override fake-cart builder: override
Adv_order_model::createAdminFakeCart()in a client order model to inject custom line-item fields. - Custom validation: override
validation()atAdv_orders_admin.php:611-671for stricter field requirements. - Custom address handling: override
setOrderAddressData(). - Custom SMS on success: override the empty
sendSmsSuccess()stub at:1914-1917. - Custom VAT logic: override the
VatForOrderapplication stub or flipenableOrderVatManipulationtoTRUEand rely onAdvVatForOrder::vat()/invoiceVat()(note the latter is// TODO not yet implemented).
Business Rules
- POS is not a separate module. It is the same
Adv_orders_admin::add()used for phone, walk-in, and manual entry. Only the ScribeHow embed, dashboard tile, and menu label tie the URL to the "POS" name. - Fake cart is literal. No session cart, no
AdminCart, no persistence until submit. Items live as hidden DOM inputs, then as an in-memory PHP array, then serialized intoshop_order.cart_contents. webignore = 1is the POS/manual flag and drives the Telephone vs Online filter in the order list.- VAT is pass-through by default.
enableOrderVatManipulationdefaults FALSE, so POS orders always use standard product VAT rates regardless of delivery country. Greek-island reduced VAT, VIES, and digital-product branches are coded but unreachable from POS. invoiceVat()is TODO — marked// TODO not yet implementedinAdvVatForOrder.- Status defaults
PENDING_ACCEPTED, notPENDING(except whenpayway === 'paybybank'). - Stock is reserved at creation, regardless of payway — the intentional anti-overselling model shared with the legacy storefront (restored on abandonment by the
AdvCancelIncompleteOrderscron). The per-product negative-stock allowance is the deliberate opt-out: with it enabled, an admin can place orders into negative stock. - Loyalty points are not auto-awarded for POS orders — an admin must click "Add points" per order or run the batch action.
- Selecting an existing customer overwrites their master record with the form data. There is no UI warning (silent gotcha).
- No thermal printer integration. Printing is HTML/PDF via the browser's Print dialog; no ESC-POS, no PrintNode, no Star/Epson driver.
- Currency is hard-coded
EUR— POS ignores session currency. - Order-complete email is gated by the registry key
EMAIL.SEND_EMAIL_TO_CLIENT_FOR_ADMIN_ORDER.
Known Issues & Security Gaps
enableOrderVatManipulationdefaultsfalse— VatForOrder is a pass-through -- POS orders always use standard product VAT rates. The country-aware and invoice-aware VAT branches are unreachable unless a deployment explicitly enables the flag (application/config/app.php:524).invoiceVat()is// TODO: not yet implemented-- invoice-specific VAT calculation never landed. The method stub exists but returns nothing (ecommercen/eshop/libraries/AdvVatForOrder.php).- Greek island reduced VAT branch is dead code from POS -- no caller in the POS path passes the
$greekAreaargument required to reach theGreeceReducedbranch inAdvVatForOrder. - No thermal/ESC-POS/PrintNode receipt printer integration -- the
transporters.label.thermal.printinglanguage string atecommercen/language/english/adv_advisable_lang.php:2263labels a transporter voucher option, not a POS receipt printer. No upstream scaffolding for receipt printing exists. - Loyalty points are NOT auto-awarded for POS orders -- admin must manually click per-order via
orders_admin/addPointsToCustomer/{serial}(Adv_orders_admin.php:1824-1841) or use theaddPointsSelectedbatch action (Adv_orders_admin.php:1872-1886). - Selecting an existing customer silently overwrites their master record --
orderAddUpdateCustomerData()callscustomer_model->updateCustomerAdmin()with the form data, with no UI warning or confirmation dialog (Adv_orders_admin.php:595-606). - Status defaults to
PENDING_ACCEPTEDnotPENDING-- surprising for anyone comparing POS and storefront order rows. Storefront orders start asPENDING; POS orders skip that state (Adv_order_model.php:1690-1694). - Stock is reserved at creation regardless of payway -- POS calls
removeOrderStock()immediately (Adv_order_model.php:1819). This is consistent with the legacy storefront, which also reserves stock at placement for every payway (Adv_checkout.phpset_status(..., stockMode='-')); abandonedPENDINGorders are restored by theAdvCancelIncompleteOrderscron, and the per-product negative-stock allowance is the opt-out. This reserve-at-placement model is the intentional anti-overselling design — POS conforms to it. (The modern REST checkout deviates by deferring online-payway stock to confirmation; tracked in Advisable-com/ecommercen#282, not a POS issue.) - Currency hard-coded to
'EUR'-- POS ignores the session currency entirely (Adv_order_model.php:1704). sendSmsSuccess()is an empty stub -- no SMS ever fires for POS orders. The method exists only as a client extension point (Adv_orders_admin.php:1914-1917).- Email gated by
EMAIL.SEND_EMAIL_TO_CLIENT_FOR_ADMIN_ORDER-- if the registry key is unset or false, POS customers receive no order confirmation email (Adv_orders_admin.php:2283-2290).
Related Flows
- AD-03 Order Management Admin — Order list (source of the Telephone/Online/Marketplace filter) and detail management.
- AD-04 Customer Management Admin — Customer lookup, creation, and
updateCustomerAdmin(). - AD-50 VAT Management — VAT rates and the
enableOrderVatManipulationtoggle that decides whetherVatForOrderdoes anything. - CF-06 Order Preview — Storefront checkout preview that this flow parallels but relaxes.
- CF-07 Order Confirmation — Storefront order finalisation against which POS defaults (status,
webignore, stock decrement) are compared.