Skip to content

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=true

Key Components

ComponentPathRole
AdvAddPointsToCustomerDeliverecommercen/job/libraries/AdvAddPointsToCustomerDeliver.phpJob: points for delivered orders
AdvAddPointsToCustomerFromStoreecommercen/job/libraries/AdvAddPointsToCustomerFromStore.phpJob: points for store-pickup orders
Adv_loyaltyecommercen/libraries/Adv_loyalty.phpLibrary: point calculation, persistence, cash conversion
Client stubsapplication/modules/job/libraries/AddPointsToCustomerDeliver.php, AddPointsToCustomerFromStore.phpEmpty extension points

Code Flow

AdvAddPointsToCustomerDeliver

  1. Gate check: Reads POINT_SYSTEM.IS_ENABLED from registry. Returns immediately if disabled.
  2. Order query: Selects order_serial from shop_order where:
    • points_added = false
    • gtflag = 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)
  3. Point award: Passes collected order serials to loyalty->saveProductPointToCustomers().

AdvAddPointsToCustomerFromStore

  1. Gate check: Same POINT_SYSTEM.IS_ENABLED check.
  2. Order query: Selects order_serial from shop_order where:
    • points_added = false
    • status = 'INVOICED'
    • store_id IS NOT NULL (store pickup order)
  3. Point award: Passes collected order serials to loyalty->saveProductPointToCustomers().

Loyalty::saveProductPointToCustomers (shared logic)

  1. Point calculation: Joins shop_order_basket with shop_order on the provided serials where points_added=false. Computes SUM(item_points * qty) grouped by order.
  2. Customer update: For each order, calls savePointsToCustomer($customerId, $productPoints) to add points to the customer's total_points balance.
  3. Order marking: Calls setOrdersMark($orderSerials, true) to set points_added = true on processed orders, preventing re-processing.
  4. Result tracking: Returns an array with success (processed serials) and error (unprocessed serials from the input).

Data Model

Tables

TableRelevant ColumnsRole
shop_orderorder_serial, customer_id, status, gtstatus, gtflag, store_id, points_addedOrder records; points_added flag prevents double-awarding
shop_order_basketorder_id, item_points, qtyLine items with per-item point values
shop_customertotal_pointsCustomer's accumulated point balance

Key Fields

FieldTableDescription
points_addedshop_orderBoolean flag; set to true after points are awarded
gtflagshop_orderDelivery terminal-state flag; set to 1 by the tracking poller when the carrier confirms delivery
gtstatusshop_orderCarrier tracking status text; written by the tracking poller, not used as a filter by this job
store_idshop_orderNULL for delivery orders; populated for store-pickup
item_pointsshop_order_basketPoints value per unit of each line item
total_pointsshop_customerRunning total of customer's loyalty points

Configuration

Registry Settings

GroupKeyDescription
POINT_SYSTEMIS_ENABLEDMaster toggle for the entire loyalty points system

Order Status Values

Status FieldValueMeaning
gtflag1Delivery terminal-state flag; set by the tracking poller when the carrier confirms delivery
statusINVOICEDOrder has been invoiced (used for store pickup trigger)
statusCANCELEDOrder canceled (excluded from delivery points)
statusRETURNOrder 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 AddPointsToCustomerDeliver or AddPointsToCustomerFromStore in application/modules/job/libraries/ extending the Adv* base class to modify query conditions (e.g., different status values for custom courier integrations).
  • Loyalty library override: Override Adv_loyalty in application/libraries/ to customize point calculation formulas, add multipliers, or implement tiered point systems.
  • Where conditions: The delivery job's getWhereOptions() and getWhereNotInOptions() methods are protected, allowing subclass overrides to add or modify query filters.

Business Rules

  1. Feature gate: Both jobs are complete no-ops when POINT_SYSTEM.IS_ENABLED is false. No queries are executed.
  2. Idempotent: The points_added flag on each order ensures points are awarded exactly once per order, even if the job runs multiple times.
  3. Delivery vs. Store: The two jobs use mutually exclusive conditions: delivery requires store_id IS NULL and courier status, while store pickup requires store_id IS NOT NULL and invoice status.
  4. Canceled/returned exclusion: The delivery job explicitly excludes CANCELED and RETURN status orders. The store job implicitly excludes them by requiring status = 'INVOICED'.
  5. 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.
  6. Point source: Points are defined per product (item_points on the basket) and multiplied by quantity. The job does not apply bonus multipliers; those are handled by other flows (e.g., birthday points).
  7. Delivery selector: The delivery job filters on gtflag = 1, the canonical terminal-state flag set by the tracking poller when the carrier confirms delivery. gtstatus carries the raw carrier text but is not used as a filter by this job.

Known Issues & Security Gaps

  1. No automated test coverage for the gtflag delivery filter: Neither AdvAddPointsToCustomerDeliver nor Cronjob::addPointsToCustomerDeliver() has a unit or integration test asserting that the gtflag = 1 criterion 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, NULL gtflag, store-pickup orders) is absent.

  2. Cronjob inline path is a duplicate: Cronjob::addPointsToCustomerDeliver() (application/controllers/Cronjob.php) duplicates the logic of AdvAddPointsToCustomerDeliver. Both were updated for the gtflag fix (#276), but the duplication means future changes must be applied to both sites.