Appearance
Loyalty Points Jobs
Flow ID: SY-10 | Module(s): job, libraries | Complexity: Medium | Last Updated: 2026-06-04
Business Overview
Two background jobs automatically award loyalty points to customers after order fulfillment. They handle two distinct fulfillment channels:
- AdvAddPointsToCustomerDeliver: Awards points for home-delivery orders that have been marked as delivered by the courier.
- AdvAddPointsToCustomerFromStore: Awards points for store-pickup orders that have been invoiced.
Both jobs are gated by the POINT_SYSTEM.IS_ENABLED registry flag and delegate to the shared Loyalty library for point calculation and persistence. Points are computed from the item_points * qty sum across order basket items.
Architecture
AdvAddPointsToCustomerDeliver AdvAddPointsToCustomerFromStore
(delivered courier orders) (invoiced store-pickup orders)
| |
v v
Query shop_order Query shop_order
gtflag=1 status='INVOICED'
store_id IS NULL store_id IS NOT NULL
points_added=false points_added=false
| |
+------------+---------------------------+
|
v
Loyalty::saveProductPointToCustomers($orderSerials)
|
v
SUM(item_points * qty) per order
|
v
savePointsToCustomer() per customer
setOrdersMark() -> points_added=trueKey Components
| Component | Path | Role |
|---|---|---|
AdvAddPointsToCustomerDeliver | ecommercen/job/libraries/AdvAddPointsToCustomerDeliver.php | Job: points for delivered orders |
AdvAddPointsToCustomerFromStore | ecommercen/job/libraries/AdvAddPointsToCustomerFromStore.php | Job: points for store-pickup orders |
Adv_loyalty | ecommercen/libraries/Adv_loyalty.php | Library: point calculation, persistence, cash conversion |
| Client stubs | application/modules/job/libraries/AddPointsToCustomerDeliver.php, AddPointsToCustomerFromStore.php | Empty extension points |
Code Flow
AdvAddPointsToCustomerDeliver
- Gate check: Reads
POINT_SYSTEM.IS_ENABLEDfrom registry. Returns immediately if disabled. - Order query: Selects
order_serialfromshop_orderwhere:points_added = falsegtflag = 1(delivery terminal-state flag set by the tracking poller)store_id IS NULL(not a store pickup)status NOT IN ('CANCELED', 'RETURN')(excludes canceled/returned orders)
- Point award: Passes collected order serials to
loyalty->saveProductPointToCustomers().
AdvAddPointsToCustomerFromStore
- Gate check: Same
POINT_SYSTEM.IS_ENABLEDcheck. - Order query: Selects
order_serialfromshop_orderwhere:points_added = falsestatus = 'INVOICED'store_id IS NOT NULL(store pickup order)
- Point award: Passes collected order serials to
loyalty->saveProductPointToCustomers().
Loyalty::saveProductPointToCustomers (shared logic)
- Point calculation: Joins
shop_order_basketwithshop_orderon the provided serials wherepoints_added=false. ComputesSUM(item_points * qty)grouped by order. - Customer update: For each order, calls
savePointsToCustomer($customerId, $productPoints)to add points to the customer'stotal_pointsbalance. - Order marking: Calls
setOrdersMark($orderSerials, true)to setpoints_added = trueon processed orders, preventing re-processing. - Result tracking: Returns an array with
success(processed serials) anderror(unprocessed serials from the input).
Data Model
Tables
| Table | Relevant Columns | Role |
|---|---|---|
shop_order | order_serial, customer_id, status, gtstatus, gtflag, store_id, points_added | Order records; points_added flag prevents double-awarding |
shop_order_basket | order_id, item_points, qty | Line items with per-item point values |
shop_customer | total_points | Customer's accumulated point balance |
Key Fields
| Field | Table | Description |
|---|---|---|
points_added | shop_order | Boolean flag; set to true after points are awarded |
gtflag | shop_order | Delivery terminal-state flag; set to 1 by the tracking poller when the carrier confirms delivery |
gtstatus | shop_order | Carrier tracking status text; written by the tracking poller, not used as a filter by this job |
store_id | shop_order | NULL for delivery orders; populated for store-pickup |
item_points | shop_order_basket | Points value per unit of each line item |
total_points | shop_customer | Running total of customer's loyalty points |
Configuration
Registry Settings
| Group | Key | Description |
|---|---|---|
POINT_SYSTEM | IS_ENABLED | Master toggle for the entire loyalty points system |
Order Status Values
| Status Field | Value | Meaning |
|---|---|---|
gtflag | 1 | Delivery terminal-state flag; set by the tracking poller when the carrier confirms delivery |
status | INVOICED | Order has been invoiced (used for store pickup trigger) |
status | CANCELED | Order canceled (excluded from delivery points) |
status | RETURN | Order returned (excluded from delivery points) |
Job Options
Both jobs accept no options. They rely entirely on registry configuration and order data.
Client Extension Points
- Job override: Create
AddPointsToCustomerDeliverorAddPointsToCustomerFromStoreinapplication/modules/job/libraries/extending theAdv*base class to modify query conditions (e.g., different status values for custom courier integrations). - Loyalty library override: Override
Adv_loyaltyinapplication/libraries/to customize point calculation formulas, add multipliers, or implement tiered point systems. - Where conditions: The delivery job's
getWhereOptions()andgetWhereNotInOptions()methods areprotected, allowing subclass overrides to add or modify query filters.
Business Rules
- Feature gate: Both jobs are complete no-ops when
POINT_SYSTEM.IS_ENABLEDis false. No queries are executed. - Idempotent: The
points_addedflag on each order ensures points are awarded exactly once per order, even if the job runs multiple times. - Delivery vs. Store: The two jobs use mutually exclusive conditions: delivery requires
store_id IS NULLand courier status, while store pickup requiresstore_id IS NOT NULLand invoice status. - Canceled/returned exclusion: The delivery job explicitly excludes
CANCELEDandRETURNstatus orders. The store job implicitly excludes them by requiringstatus = 'INVOICED'. - Batch processing: All eligible orders are collected in a single query, then processed together through the loyalty library. The library marks orders as processed only after successfully awarding points.
- Point source: Points are defined per product (
item_pointson the basket) and multiplied by quantity. The job does not apply bonus multipliers; those are handled by other flows (e.g., birthday points). - Delivery selector: The delivery job filters on
gtflag = 1, the canonical terminal-state flag set by the tracking poller when the carrier confirms delivery.gtstatuscarries the raw carrier text but is not used as a filter by this job.
Known Issues & Security Gaps
No automated test coverage for the gtflag delivery filter: Neither
AdvAddPointsToCustomerDelivernorCronjob::addPointsToCustomerDeliver()has a unit or integration test asserting that thegtflag = 1criterion captures all carriers correctly. The fix for issue #276 was validated by a new integration test (tests/Integration/Legacy/Jobs/AdvAddPointsToCustomerDeliverTest.php), but broader coverage of edge cases (mixed carriers, NULLgtflag, store-pickup orders) is absent.Cronjob inline path is a duplicate:
Cronjob::addPointsToCustomerDeliver()(application/controllers/Cronjob.php) duplicates the logic ofAdvAddPointsToCustomerDeliver. Both were updated for thegtflagfix (#276), but the duplication means future changes must be applied to both sites.
Related Flows
- SY-01 Cron Framework -- Job registration and scheduling infrastructure
- CF-32 Loyalty Points -- Full loyalty points system (earning, spending, display)
- SY-12 Birthday & Points Emails -- Birthday bonus points and remaining points notifications
- SY-09 Customer Data Jobs -- Sanitization clears tags/campaigns for GDPR compliance
- CF-07 Order Confirmation -- Order lifecycle that leads to point eligibility
- AD-52 Review Moderation -- single-approval path awards review reward via
savePointsToCustomer(); documents the bulk-approval gap and non-idempotent award defect