Appearance
Admin Order Management
Flow ID: AD-03 Module(s): eshop Complexity: Very High Last Updated: 2026-09-29 — 4.124.0 resync: citation drift corrections,
is_paidsemantics (#754),ip_addressrelease note, Business Rule 13 (#837); previously corrected stalePlaceOrderService.php,Adv_orders_admin.php,Adv_order_model.php, andrest_routes.phpcitations drifted by the #760 invoice-identity guard and the #688/PR#260 edit()-cancel-reporting change; documented the newInvoiceIdentityIncompleteExceptionguard as Business Rule 12
Business Context
Order management covers the full order lifecycle: viewing, status transitions, voucher generation for shipping providers, shipment closure, invoice generation, and order editing (which is actually clone + cancel). The status state machine governs stock adjustments, payment reconciliation, and loyalty point handling.
Entry Points
| Type | Path / Trigger | Controller | Method |
|---|---|---|---|
| Admin | Order listing | ecommercen/eshop/controllers/Adv_orders_admin.php | index() |
| Admin | Order detail | same | view($id) |
| Admin | Edit order | same | edit($id) |
| Admin | Batch cancel | same | setBatchCanceled() |
| Admin | Create voucher | same | setPendingWithVoucher() |
| Admin | Close shipments | same | close_pending_jobs() |
Order Status State Machine
| Status | Meaning | Next |
|---|---|---|
| PENDING | Awaiting payment/acceptance | → PENDING_ACCEPTED, CANCELED |
| PENDING_ACCEPTED | Accepted, payment received (COD) | → PENDING_ACCEPTED_VOUCHER, CANCELED |
| PENDING_ACCEPTED_VOUCHER | Voucher created, awaiting shipment | → SENT / PAID_SENT, CANCELED |
| PAID | Payment received (prepaid) | → SENT / PAID_SENT |
| SENT | COD orders shipped | → INVOICED |
| PAID_SENT | Prepaid orders shipped | → INVOICED |
| INVOICED | Completed | (terminal) |
| CANCELED | Canceled | (terminal) |
| RETURN | Returned | (terminal) |
SENT vs PAID_SENT determined by payment method: COD (delivery/byphone) → SENT; prepaid (card, paypal, etc.) → PAID_SENT.
Stock Management
| Transition | Stock Effect | ignoreStock |
|---|---|---|
| Order creation (PENDING) | Deducted (-qty) | false |
| Any → CANCELED | Returned (+qty) | false |
| PENDING_ACCEPTED_VOUCHER → SENT/PAID_SENT | No change | true |
| Edit (clone + cancel) | Cancel returns stock, new order deducts | — |
Stock is adjusted via returnOrderStock() (+qty per basket item to product_codes.stock) and removeOrderStock() (-qty).
Voucher Lifecycle
- Create:
setPendingWithVoucher()→ calls transporter API (ACS, Geniki, DHL, etc.) → savesgtcode+gtjobcode→ status = PENDING_ACCEPTED_VOUCHER - Close:
close_pending_jobs()→ calls transporter pickup/closure API → status = SENT or PAID_SENT (stock NOT adjusted) - Cancel:
cancelVoucher()→ calls transporter cancel API → clearsgtcode/gtjobcode→ reverts to PENDING_ACCEPTED or PAID
Supported transporters: ACS, Geniki (v1/v2), DHL, ELTA, Speedex, Center, EasyMail, FIS, BoxNow, DailyCourier, Taxydema (v1/v2), Skroutz, ASAP.
Order Editing
Adv_orders_admin.php:1079-1256 is actually clone + cancel:
- Cancel original order (returns stock, rolls back points/coupons)
- Create new order with modified items/prices/addresses
- Clone order notes, send new confirmation email
- Configurable:
keepProductPriceForEdit— retain old or use current prices
Cancellation Restrictions
Cannot cancel if:
- Already CANCELED
- Payway is bank card provider (eurobank, alpha, ethniki, piraeus, apcopay) — refund not automated
- PayByBank PAID orders — payment already settled
- Status = PENDING with non-PayByBank card payment
Business Rules
- Stock deducted at creation, returned at cancel -- Never double-adjusted during voucher lifecycle. Stock adjustments happen via
returnOrderStock()(+qty) andremoveOrderStock()(-qty) onproduct_codes.stock. (ecommercen/eshop/controllers/Adv_orders_admin.php) - Loyalty points rolled back on cancel -- Both spent points (returned to customer) and earned points (removed) are reversed. (
ecommercen/eshop/controllers/Adv_orders_admin.php, cancel flow) - Edit = clone + cancel -- Original order canceled, new order created. Configurable via
keepProductPriceForEditwhether to retain old or use current prices. (ecommercen/eshop/controllers/Adv_orders_admin.php,edit()(:1079-1256, cancel call at:1187)) - Voucher status blocks cancel -- Must cancel voucher before canceling order. Voucher cancellation calls transporter API and clears
gtcode/gtjobcode. (ecommercen/eshop/controllers/Adv_orders_admin.php,cancelVoucher()) - Invoice uses order barcode -- Generated via
BarcodeGeneratorPNG. (ecommercen/eshop/controllers/Adv_orders_admin.php) - Batch cancel validates each order -- Per-order validation before canceling, respects payment method and status restrictions. (
ecommercen/eshop/controllers/Adv_orders_admin.php,setBatchCanceled()) - Payment method determines SENT vs PAID_SENT -- COD/byphone orders transition to SENT; prepaid (card, paypal, etc.) transition to PAID_SENT. (
ecommercen/eshop/controllers/Adv_orders_admin.php,close_pending_jobs()) - Cancel blocked for card payments -- Cannot cancel orders with bank card payment methods (eurobank, alpha, ethniki, piraeus, apcopay) since refunds are not automated. (
ecommercen/eshop/controllers/Adv_orders_admin.php, cancellation restrictions) - Order status emails on transition -- Status changes trigger automated email and SMS notifications via the SY-24 Email Dispatch system. The email templates used (
order_created,order_update,order_on_store,notify_admin) are catalogued in AD-53 Email Template Viewer. - Coupon usage rolled back on cancel -- If order used a coupon, the coupon usage count is decremented. (
ecommercen/eshop/controllers/Adv_orders_admin.php, cancel flow) - Gift stock rolled back on cancel -- Gift product stock is returned when an order containing gift items is canceled. See AD-09 Gift Rules.
- REST-placed INVOICE orders require complete tax identity (#760) --
PlaceOrderService::placeOrder()refuses an order withwantsInvoice === truewhen any ofafm/doy/profession/company/companyAddressis missing or whitespace-only, via themissingInvoiceIdentityFields()helper (src/Domains/Checkout/PlaceOrderService.php:1026-1045), throwingInvoiceIdentityIncompleteException(error codeinvoice_identity_incomplete) at the guard (:160-187). This mirrors the legacy storefront checkout'strim|requiredvalidation on the same five fields when the choice is invoice (ecommercen/eshop/controllers/Adv_order.php:557-563). - REST place-order saves a signed-in customer's details (#837) -- for a non-guest customer,
PlaceOrderService::placeOrder()writes the checkout details onto theshop_customerrow viaCheckoutCustomerDetailsWriteronce the order row exists and before the payment step (src/Domains/Checkout/PlaceOrderService.php:571-581). Guests are skipped; the writer logs and swallows its own failure. The legacy storefront already did this throughupdate_customer()(Adv_order::previewOrder(), per the in-code comment atPlaceOrderService.php:571-573).
Order Lifecycle Hooks
The order system fires HTTP webhook notifications on key lifecycle transitions using OrderForErpHookFireTrait and OrderCancelHookFireTrait (Guzzle HTTP with Bearer token authentication).
Configuration: application/config/api.php under internalApi.apiOrderWebHooks:
erpReady-- URL for order-ready webhookcancel-- URL for order-cancel webhookreturn-- URL for order-return webhook
| Hook Method | Trigger Point | HTTP Call | Description |
|---|---|---|---|
afterOrderSuccessHooks | Order success | internalApiOrderForErpHook($orderId) | HTTP GET to configured ERP webhook URL -- notifies ERP that order is ready |
afterOrderCancelHooks | Order cancellation (controller) | internalApiOrderCancelHook($orderId) | HTTP GET to cancel webhook URL -- notifies ERP of cancellation |
afterCancelOrderHooks | Order cancellation (model-level) | internalApiOrderCancelHook($orderId) | Same cancel notification fired from model layer |
| (orphaned) | Never fired | internalApiOrderReturnHook($orderId) | Exists in code but is not called from any trigger point -- dead code |
Note:
internalApiOrderReturnHookis implemented but orphaned -- no code path currently invokes it. If return webhook notifications are needed, a trigger must be wired into the return status transition.
Data Model
shop_order Table (Primary)
| Column | Type | Description |
|---|---|---|
id | INT(11) PK AI | Internal order ID |
cancel_id | INT(11) | ID of canceled predecessor (for edit/clone). Default 0. |
store_id | INT(11) NULL | Pickup store ID (NULL for delivery orders) |
transport_id | INT(11) NULL | Transporter FK (NULL for store pickup) |
order_serial | VARCHAR(255) | Human-readable serial number (generated via createSerial()) |
webignore | INT(1) | 0 = online order, 1 = phone/admin order |
total_qty | INT(11) | Total item quantity in basket |
gen_tax_service | VARCHAR(2) | Geniki Taxydromiki's cash-on-delivery service designation — ΑΜ/ΑΝ, Geniki's own codes (default ΑΜ per database/initial/initial.sql:1312); read by AdvSetPendingWithVoucher::createVoucherGeniki()/createVoucherGenikiV2() (:145,:207) and the admin writer, never by createVoucherAcs() |
order_currency | VARCHAR(3) | ISO currency code |
currency_rate | DECIMAL(11,4) | Exchange rate at order time |
currency_id | INT(11) | Currency FK |
pricing_* | VARCHAR | Billing address fields: name, surname, address, city, region, postal, county, country, phone, mobile |
shipping_* | VARCHAR | Shipping address fields: same set as billing |
afm | VARCHAR(30) | Tax ID (VAT number) for invoices. On receipt orders (paymerch !== 'invoice') this is written as the empty string instead of the customer's submitted value, matching Adv_order_model::setUpAdminOrderDataReceiptInvoice()'s receipt arm — see docs/decisions/760-paymerch-invoice-flag.md (D7). Same on doy/company/profession/company_address below |
doy | VARCHAR(100) | Tax office for invoices (empty string on receipt orders — see afm above) |
profession | VARCHAR(255) | Profession (invoice) (empty string on receipt orders — see afm above) |
company | VARCHAR(255) | Company name (invoice) (empty string on receipt orders — see afm above) |
company_address | VARCHAR(255) | Company address (invoice) (empty string on receipt orders — see afm above) |
payway | VARCHAR(255) | Payment method slug (e.g., delivery, paypal, stripe, eurobank, paybybank) |
viva_payway | VARCHAR(255) | Viva Wallet sub-payway code |
paymerch | VARCHAR(25) | receipt or invoice |
customer_id | INT(11) | Customer FK |
customer_comments | TEXT | Customer notes |
admin_comments | TEXT | Admin notes |
courier_comments | TEXT | Courier delivery notes |
total | DECIMAL(11,2) | NET items-only accounting total — excludes shipping, coupon, points and gift packaging. The one NET column in the money contract; never a display subtotal |
total_vat | DECIMAL(11,2) | GROSS grand total (VAT + transport + delivery − reward + gift packaging) — the amount actually charged. Despite the name, this is "total with VAT included", not "the VAT amount" |
transportation_cost | DECIMAL(11,2) | Shipping cost |
delivery_cost | DECIMAL(11,2) | Cash-on-delivery surcharge, charged only for payway === 'delivery' and waived once the cart reaches the transporter's DELIVERY_COST_MIN_FREE. Written by both the legacy checkout and, since #568, PlaceOrderService (value resolved at src/Domains/Checkout/PlaceOrderService.php:342, written at :481) — REST-placed orders previously always left this column at its default. Added to total_vat only, never to the NET total column above |
status | VARCHAR(30) | Order status (see state machine above) |
gtcode | VARCHAR(30) | Transporter voucher code |
gtjobcode | VARCHAR(255) | Transporter job/tracking code |
gtstatus | VARCHAR(255) | Voucher status from transporter API |
gtflag | TINYINT(1) | Voucher processing flag |
gtdatetime | DATETIME | Voucher creation timestamp |
gtcount | INT(3) | Voucher request attempt counter |
notes | MEDIUMTEXT | Legacy order notes |
charge_to | TINYINT(1) | Charge recipient flag |
entry_date | VARCHAR(30) | UNIX timestamp string of order creation |
entry_datetime | DATETIME | Order creation datetime |
canceled_date | DATETIME NULL | Cancellation timestamp |
ip_address | VARCHAR(45) | Client IP at order time. Written by the storefront checkout only (Adv_order_model::create_order()), resolved through MY_Input::ip_address() (CloudFlare CF-Connecting-IP, then X-Forwarded-For, then REMOTE_ADDR). NULL for admin, public, Shopflix and Skroutz orders, which go through create_order_admin() and are covered by the admin audit log instead, and NULL for REST-placed orders (PlaceOrderService). Was never written at all before 4.124 (write at ecommercen/eshop/models/Adv_order_model.php:448-453; only create_order() writes it, PlaceOrderService does not) |
user_agent | TEXT | Browser user agent |
admin_order_check | INT(1) | 1 = admin has viewed this order |
is_email_sent | TINYINT(1) | Status email sent flag |
is_paid | TINYINT(1) | "Someone has checked this order" flag (0/1), not a reliable paid indicator. Set by the legacy gateway handlers via set_is_paid() (ecommercen/eshop/models/Adv_order_model.php:1916-1918), by the admin action (ecommercen/eshop/controllers/Adv_orders_admin.php:766) and by a cron (per the PaymentConfirmationService comment). REST PaymentConfirmationService::confirmPayment() deliberately does not write it (#754, product-owner ruling, src/Domains/Checkout/PaymentConfirmationService.php:58-81), so REST-confirmed orders stay is_paid = 0 and drop out of admin filter[isPaid] and the isPaid sort |
paid | TINYINT(1) | Admin-toggled paid flag (differs from is_paid) |
is_visible | TINYINT(1) | Customer-facing visibility |
invoised_date_time | DATETIME NULL | Invoice generation timestamp |
printed_date_time | DATETIME NULL | Print timestamp |
sent_date_time | DATETIME NULL | Shipment closure timestamp |
sms_status | TINYINT(1) NULL | null=no SMS, 1=requested, 2=sent success, 3=sent fail, 4=pending callback |
coupon_id | INT(11) NULL | Applied coupon FK |
coupon_value | DECIMAL(11,2) | Coupon discount amount |
points_spend | INT(11) | Loyalty points redeemed |
points_reward | DECIMAL(11,2) | Cash value of points reward |
points_added | TINYINT(1) | 0=product points not added to customer, 1=added |
skroutz_referer | TINYINT(1) NULL | Referrer tracking code |
delivery_warning | TINYINT(1) | Delivery delay warning flag |
delivery_warning_msg_status | TINYINT(1) | 0=not sent, 1=sent, 2=admin opted out |
meta_data | VARCHAR(255) | Serialized extra data |
remind | TINYINT(1) | Reminder flag |
remind_at | DATETIME NULL | Reminder scheduled time |
pbb_payment_code | VARCHAR(255) | PayByBank payment reference |
tran_ticket | VARCHAR(32) | Viva Wallet transaction ticket |
provider | VARCHAR(50) | Marketplace provider tag (skroutz_smart_cart, shopflix_marketplace, etc.) |
invoice_pdf | VARCHAR(255) | Uploaded invoice PDF filename |
gift_packaging | TINYINT(1) | Gift wrapping enabled |
gift_packaging_message | TEXT | Gift wrapping message |
gift_packaging_cost | DECIMAL(11,2) | Gift wrapping cost |
json_invoice | LONGTEXT | JSON invoice data (e-invoicing) |
Indexes (27 non-PRIMARY KEY definitions for query optimization):
order_serial-- order lookup (not unique;database/initial/initial.sql:1396)status,entry_datetime,status_entry_datetime-- listing and filteringcustomer_id,customer_id_status_is_visible-- customer order historycancel_id-- edit chain lookupgtcode_gtflag_gtcount-- voucher processing queriespaid,sms_status,coupon_id,skroutz_referer-- filteringstore,store_payway_customer,store_customer-- store-based queriestransport_id-- transporter filteringdelivery_warning_msg-- composite for delivery delay jobremind_remind_at-- reminder jobpoints_added-- loyalty points jobstatus_invoised_date_time-- invoice reporting
shop_order_basket Table
| Column | Type | Description |
|---|---|---|
id | INT(11) PK AI | Basket item ID |
order_id | INT(11) | FK to shop_order.id |
product_code_id | INT(11) | FK to product_codes.id (SKU) |
product_vat | DECIMAL(11,2) | Effective VAT percentage at time of order — the adjusted rate from VatForOrder::vat() (legacy vat_rate_captured), not the raw catalogue vat.value |
price | DECIMAL(11,2) | Unit price (VAT-inclusive) |
item_points | INT(11) NULL | Loyalty points per unit |
qty | INT(11) | Quantity ordered |
subtotal | DECIMAL(11,2) | Line total, VAT-inclusive (price * qty) |
discount_string | VARCHAR(10) | Discount label (e.g., 10%) |
discount_price | DECIMAL(11,2) | Discount amount taken off the unit (original_price - price), VAT-inclusive — legacy save_price, not the discounted price itself |
original_price | DECIMAL(11,2) | Pre-discount unit price (VAT-inclusive) |
gift_id | INT(11) NULL | Gift rule FK (non-NULL = gift item) |
track_type | TINYINT(1) | 0=normal, smart/AI recommendation types |
added_at | INT(11) NULL | UNIX timestamp when added to cart |
track_id | VARCHAR(255) NULL | Recommendation tracking ID |
options | TEXT NULL | Serialized product options (bundles, variations) |
Supporting Tables
| Table | Purpose |
|---|---|
shop_order_dhl_vouchers | DHL-specific voucher data: dispatch confirmation, tracking number, product code |
shop_order_smart_point | Smart point/locker delivery data: transporter_id, shop_id, json_data |
shop_order_tags | Order tag definitions (id, tag name, active flag) |
shop_order_tags_lp | Order-to-tag linking table (order_id, tag_id) |
shop_order_basket_options_applied_bundles | Bundle application records for basket items |
Complete Batch Operations Reference
The batch actions system in doBatchActions() dispatches through three method chains: doBatchActionsGeniki() for per-transporter operations, doBatchActionsSystem() for system operations, and doBatchActionsClient() for client-repo overrides.
Per-Transporter Batch Actions (dynamic, one per active transporter)
| Action Key | Method | Description |
|---|---|---|
set_pending_with_voucher_{transporterId} | setPendingWithVoucher() | Create vouchers via transporter API for selected orders |
cancel_jobs_{transporterId} | cancelVoucher() | Cancel vouchers via transporter API for selected orders |
print_vouchers_pdf_{transporterId} | printVouchersPdf() | Print voucher PDFs for selected orders |
System Batch Actions
| Action Key | Method | Description |
|---|---|---|
set_status_canceled | setBatchCanceled() | Cancel selected orders with per-order validation, stock return, points rollback |
set_paid | setPaid() | Mark selected orders as paid |
set_unpaid | unsetPaid() | Unmark selected orders as paid |
print_selected | printSelected() | Print invoices for selected orders (HTML or PDF via DomPDF) |
print_selected_xls | exportXls() | Export selected orders to Excel (XLSX via PhpSpreadsheet) |
print_selected_products_xls | exportProductsXls() | Export product breakdown from selected orders to Excel |
addPoints | addPointsSelected() | Add earned loyalty points to customers for selected orders |
removePoints | removePointsSelected() | Remove earned loyalty points from customers for selected orders |
markInformDelayOrder | markInformDelayOrderSelected() | Mark delivery delay notification sent |
unMarkInformDelayOrder | unMarkInformDelayOrderSelected() | Clear delivery delay notification flag |
set_order_tags | batch_action() | Assign tags to selected orders (redirect to tag selection UI) |
unset_order_tags | batch_action() | Remove tags from selected orders (redirect to tag selection UI) |
Client Extension Point
doBatchActionsClient() is an empty hook method that client repos can override to add custom batch actions (e.g., ERP sync, custom exports).
extendJsonState() (ecommercen/eshop/controllers/Adv_orders_admin.php:3314) is a template-method seam allowing client repos to inject Vue jsonState keys into order forms without copying the entire action. Invoked before jsonEncodeForVue() on three view actions: add() (:605), edit() (:1234), and repeat() (:3160). The $action parameter is a string identifying the caller ('add', 'edit', or 'repeat').
Close Shipments (Transporter Integration)
The close_pending_jobs($transporterId) method implements the shipment closure flow for 15 supported transporter providers:
| Provider Class | Closure Method | API Integration |
|---|---|---|
ACS | closePendingJobsAcs() | ACS issuePickupList() API -- remote pickup request |
GT (Geniki v1) | closePendingJobsGeniki() | Geniki closeOpenJobs() API -- remote closure |
GTV2 (Geniki v2) | closePendingJobsGenikiV2() | GenikiV2 closeOpenJobs() API -- remote closure |
DHL | closePendingJobsDhl() | DHL API -- remote closure with dispatch confirmation |
ELTA | closePendingJobsLocally() | Local status update only (no remote API) |
SPEEDEX | closePendingJobsLocally() | Local status update only |
CENTER | closePendingJobsLocally() | Local status update only |
EASYMAIL | closePendingJobsLocally() | Local status update only |
FIS | closePendingJobsLocally() | Local status update only |
BOXNOW | closePendingJobsLocally() | Local status update only |
DAILYCOURIER | closePendingJobsLocally() | Local status update only |
TAXYDEMA | closePendingJobsLocally() | Local status update only |
SKROUTZ | closePendingJobsLocally() | Local status update only |
ASAP | closePendingJobsLocally() | Local status update only |
TAXYDEMAV2 | closePendingJobsLocally() | Local status update only |
Closure flow:
- Fetch all orders with status
PENDING_ACCEPTED_VOUCHERfor the transporter - Call transporter API (for ACS/Geniki/DHL) or update locally
- For each successfully closed order: set status to
SENTorPAID_SENTviaorderGetSentStatusForPayWayVoucher(), a thin wrapper overisOrderPaidAtDeliveryByPayWay()'s landing-status classification (see AD-34 Voucher Generation for the full semantics) --SENTfor the three payways that land atPENDING_ACCEPTED(delivery,bank_transfer,paid_at_store),PAID_SENTfor every other payway.stripe,xpay, andethniki_nbgpaywere previously misclassified asSENTuntil Advisable-com/ecommercen#530; the same helper drivesorderGetRevertedStatusForPayWayVoucher()on voucher cancellation (AD-34's Voucher Cancellation section), which now likewise reverts those three toPAIDinstead ofPENDING_ACCEPTED - Send shipment email via
adv_mailer->order_has_been_updated() - Set
is_email_sent = true
Invoice Generation
The invoice($orderSerial) method generates printable invoices:
- Loads full order data via
getRecordForInvoice() - Generates barcode image using
BarcodeGeneratorPNG(Code 128 format) - Optionally includes Geniki voucher barcode if GT provider is configured
- Supports batch printing via
printSelected()which can output HTML or PDF (DomPDF, A4 format)
Export Capabilities
| Export | Method | Format | Content |
|---|---|---|---|
| Order XLS | exportXls() | XLSX (PhpSpreadsheet) | Customer ID, address, phone, postal, weight, COD amount, courier notes |
| Products XLS | exportProductsXls() | XLSX | Product ID, name, quantity, order count |
| Orders CSV | exportOrders() | CSV (semicolon, UTF-8 BOM) | 28 columns: order ID/serial, billing/shipping addresses, payway, totals, status |
| Searched Orders CSV | exportSearchedOrders() | CSV (semicolon, UTF-8 BOM) | Product-level: product code, shelf code, barcodes, VAT, price, qty, vendor, order serial |
Order Repeat (Without Cancellation)
The repeat($orderId) method creates a new order from an existing order without canceling the original:
- Pre-fills form with original order's customer/address/products data
- Creates new order with fresh
entry_datetimebut preservesentry_dateandskroutz_referer - Supports SmartPoint (locker) delivery selection
- Fires ERP hooks and sends confirmation email/SMS for the new order
- Does NOT cancel the original order (unlike
edit()which does clone+cancel)
Admin Order Creation (POS / Phone)
Manual order creation via Adv_orders_admin::add() — including the fake cart mechanism, 5 AJAX product-search endpoints, POS-specific field defaults (webignore=1, status=PENDING_ACCEPTED, order_currency=EUR, unconditional stock decrement), and customer 3-tier resolution — is documented in AD-54 POS / Phone Orders.
SMS Provider Integration
Status notification SMS is sent via multiple provider integrations (sendSmsForOrder() helper):
| Provider | SMS Status Mapping |
|---|---|
plivo | 2 (sent) or 3 (fail) |
routeeViber | Viber first, fallback to SMS; status from provider |
routee | 2 (delivered), 4 (pending), 3 (fail) |
bulker | 4 (pending) or 3 (fail) |
yuboto_omni | 4 (pending) or 3 (fail) |
omni_messaging | 4 (pending) or 3 (fail) |
yuboto | Direct status from provider |
ERP Integration (Farmakon)
The postErpOrdersJob() method queues a FarmakonPostOrders job for ERP synchronization. This is available when FARMAKON.IS_ENABLED registry flag is set. The job processes orders with status PENDING_ACCEPTED or PAID via getOrdersForFarmakon().
REST API Endpoints
All REST endpoints require JWT authentication. Routes support an optional (\w{2})/ language prefix (e.g., /el/rest/...). POST is used for both create and update operations (not PUT).
Order (8 operations)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/order/order | JWT | List orders (paginated, filterable) |
| GET | /rest/order/order/item | JWT | Get order field metadata (schema introspection) |
| GET | /rest/order/order/{id} | JWT | Get single order by ID |
| POST | /rest/order/order | JWT | Create new order |
| POST | /rest/order/order/{id} | JWT | Update existing order |
| DELETE | /rest/order/order/{id} | JWT | Delete order |
| POST | /rest/order/order/{id}/cancel | JWT | Cancel order (triggers stock return, points rollback) |
| GET | /rest/order/order/{id}/tracking | JWT | Get order tracking information |
Note (6 operations)
Notes are polymorphic -- the entity_type field supports customer, product, and order scoping. Stored in shop_notes table.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/order/note | JWT | List notes (paginated, filterable by entity_type) |
| GET | /rest/order/note/item | JWT | Get note field metadata |
| GET | /rest/order/note/{id} | JWT | Get single note by ID |
| POST | /rest/order/note | JWT | Create new note |
| POST | /rest/order/note/{id} | JWT | Update existing note |
| DELETE | /rest/order/note/{id} | JWT | Delete note |
Vat (6 operations)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/order/vat | JWT | List VAT rates (paginated, filterable) |
| GET | /rest/order/vat/item | JWT | Get VAT rate field metadata |
| GET | /rest/order/vat/{id} | JWT | Get single VAT rate by ID |
| POST | /rest/order/vat | JWT | Create new VAT rate |
| POST | /rest/order/vat/{id} | JWT | Update existing VAT rate |
| DELETE | /rest/order/vat/{id} | JWT | Delete VAT rate |
For the canonical
shop_product_vatsschema, delete-guard gap in the modern REST layer, validator gaps, and all related security issues, see AD-50 VAT Management.
Basket (3 operations -- read-only)
Write routes exist in code but are commented out in route config. Basket items are managed through the order lifecycle, not directly via REST.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/order/basket | JWT | List order basket items (paginated, filterable) |
| GET | /rest/order/basket/item | JWT | Get basket item field metadata |
| GET | /rest/order/basket/{id} | JWT | Get single basket item by ID |
DhlVoucher (3 operations -- read-only)
Write routes exist in code but are commented out in route config. DHL vouchers are created through the transporter integration flow, not directly via REST.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/order/dhl-voucher | JWT | List DHL vouchers (paginated, filterable) |
| GET | /rest/order/dhl-voucher/item | JWT | Get DHL voucher field metadata |
| GET | /rest/order/dhl-voucher/{id} | JWT | Get single DHL voucher by ID |
SmartPoint (3 operations -- read-only)
Write routes exist in code but are commented out in route config. Smart point/locker delivery records are managed through the order creation flow, not directly via REST.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/order/smart-point | JWT | List smart point delivery records (paginated, filterable) |
| GET | /rest/order/smart-point/item | JWT | Get smart point field metadata |
| GET | /rest/order/smart-point/{id} | JWT | Get single smart point record by ID |
Route config: application/config/rest_routes.php -- read routes at lines 745-863 (Note write routes are interleaved at :825-831); Order write routes at :1414-1424, Vat write routes at :1434-1440. Controllers: src/Rest/Order/Controllers/ -- Order, Note, Vat, Basket, DhlVoucher, SmartPoint.
Known Issues & Security Gaps
REST checkout truncates— Write fixed (#760).gen_tax_serviceand corrupts courier codessrc/Domains/Checkout/PlaceOrderService.php:536now writes'paymerch' => $data->wantsInvoice ? 'invoice' : 'receipt'and omitsgen_tax_servicefrom the INSERT payload entirely (:471-545), so MySQL applies the column's own default,varchar(2) NOT NULL DEFAULT 'ΑΜ'(database/initial/initial.sql:1312), instead of a truncatedin/re. The column is Geniki Taxydromiki's cash-on-delivery service designation — not ACS's, as an earlier version of this entry claimed — read only insideAdvSetPendingWithVoucher::createVoucherGeniki()(ecommercen/libraries/vouchers/AdvSetPendingWithVoucher.php:106-166, the read at:145) andcreateVoucherGenikiV2()(:168-229, the read at:207);createVoucherAcs()(:231-330) never reads it. The admin editor's dropdown (application/views/admin/orders/update.php:197-201) offersΑΜ/ΑΝ— Geniki's own two service codes — and now matches the stored value again, andpaymerchcarries the customer's actual choice instead of NULL. Data repair still open. The 30 rows already written before this fix keep their truncatedgen_tax_service(in/re) and a NULLpaymerch— #760 ships no patcher (docs/decisions/760-paymerch-invoice-flag.md, decision D4): no client currently uses the REST checkout, so the corrupted rows have no live consumer today, even though the repair itself was found safe (D4 retains the analysis for reuse). Revisit before any client is ever pointed at the REST checkout, not after — that is the point at which the corrupted rows would gain a live reader.- Order serial collision vulnerability —
Adv_order_model::createSerial()(ecommercen/eshop/models/Adv_order_model.php:924-953) resolves collisions with acount_all_results()check-then-retry (:942-949), butshop_order.order_serialcarries only a plain non-UNIQUEKEY(database/initial/initial.sql:1396). Two concurrent checkouts can both pass the count and commit the same serial.
Tests
Coverage gaps noted. No comprehensive test suite specifically exercises the order lifecycle state machine, batch operations, or concurrent order creation. Unit tests for Adv_orders_admin and Adv_order_model::createSerial() are absent or incomplete.
Related Flows
Customer Flows
- CF-05 Cart Management -- cart contents become order basket items
- CF-06 Order Preview -- checkout flow that creates orders
- CF-07 Order Confirmation -- order confirmation page and email
- CF-08 Payment Processing -- payment creates orders in PENDING status
- CF-09 Payment Webhooks -- async payment status updates
- CF-13 Coupons -- coupon usage tracked in orders, rolled back on cancel
- CF-14 Gift Rules -- gift items added to order basket
- CF-32 Loyalty Points -- points earned/spent per order
Admin Flows
- AD-04 Customer Management -- customer linked to orders
- AD-06 Transporter Management -- transporter configuration and voucher lifecycle
- AD-08 Coupon Management -- coupon usage tracking and rollback
- AD-09 Gift Rules -- gift stock decremented on order creation, rolled back on cancel
- AD-10 Reporting -- order-based sales reports
- AD-11 Marketplace Orders -- marketplace orders accepted into internal order flow
- AD-15 Attributes & Tags -- order tag assignment (order tags section)
- AD-23 Gift Cards -- gift card orders in unified order timeline
- AD-34 Voucher Generation -- bulk voucher generation workflow
- AD-33 Multi-Carrier Tracking -- tracking URL resolution for shipped orders
- AD-51 Shelf Codes -- shelf code labels surface on order detail, packing slip, build summary, and in the searched-orders CSV export
- AD-54 POS / Phone Orders -- manual order creation from admin panel
Integration Flows
- IN-09 Transporter Integrations -- voucher API calls to courier providers
- IN-20 Order Webhooks -- external webhook notifications on order events
System Flows
- SY-02 Order Email Notifications -- status change triggers emails
- SY-03 Incomplete Order Cancellation -- automated cancellation of unpaid orders
- SY-10 Loyalty Points Jobs -- points awarded on delivery
- SY-15 Delivery Delay -- delivery delay notifications
- SY-24 Email Dispatch -- email sending infrastructure for order notifications
- SY-27 Deferred Tasks -- deferred processing for order-related background tasks
Wiki Guides: Job Manager Guide | DHL Guide | Stripe Guide