Skip to content

05 - Business rules and conditions ​

Every business rule of TMS v2 (BigC last-mile delivery, NestJS rewrite) in one list, with what the code does about it today. Use it in the team meeting to see which rules already hold, which are only on paper, and which still need an answer from BigC.

Source: the v2 discovery design (module SRS and design pages, the open rules list in the work scope, the auth and permission design) checked against wt-v2-prototype-core at commit 3d70475a (branch main-v2-prototype). Field and status names follow 03-schema.md and 06-master-data.md.

How to read this document ​

StatusMeaning
EnforcedThe code refuses or handles the case today. The cell gives the file and line under src/.
DesignedThe rule is agreed in the design, but no code exists for it yet. Most of execution, COD and workforce are in this state, because those modules are not built.
Waiting on BigCThe design has a working assumption, and BigC still has to confirm or change it before that phase's spec is approved.
Code differsCode for this area exists, but it does something the rule does not allow. The cell says how. Fix these before that area is called done.

Status names in the rules are the v2 design names: UNASSIGNED (in the pool), ASSIGNED (on a route that has not left), OUT_FOR_DELIVERY, HOLD (a failed attempt waiting for the store's decision), DELIVERED, FAILED_DELIVERY, CANCELLED. The prototype code still uses the v1-style list PENDING, PLANNED, IN_TRANSIT, DELIVERED, FAILED, CANCELLED (rule I10).

Times are Bangkok time (Asia/Bangkok). Store codes, DO numbers and amounts in the examples are made up but have the real format.

Count per group ​

GroupEnforcedDesignedWaiting on BigCCode differsTotal
Intake / DO753520
Planning961723
Execution0187126
COD0112114
Workforce1142118
Master data1020113
Admin / reporting171110
Platform / auth19311134
Total47661728158

Rules waiting on BigC ​

BigC confirms each of these before the spec of the phase that uses it is approved. The rule number points to the row below.

RuleOur assumptionWhat BigC decides
I8An OMS cancel is applied while the order is in the pool or on a route not yet turned into a task; once in a task it is refused and ops handle itWhether cancelling an order already loaded is allowed in the store
I9A failed delivery stays on the same DO with its attempt count + 1; an OMS redeliver or an admin partial re-delivery opens a new DO linked to the old oneWhether this split matches how stores work
I11The DO number is "DO" + 8 digits; v1's date inside the number is droppedWhether OMS or ops read the date out of the number
P19When several drivers fit one slot, the one with the fewest booked hours that day gets it, then by driver codeThe fairness rule
X2Store check-in is blocked beyond 300 m (today's PROD value)The new check-in distance rule (a change is expected)
X13At most 3 counted failed attempts per DO, a settingThe default number
X14Refused, cannot contact and wrong address count toward the cap; customer-rescheduled and incidents do notThe flag per failure reason
X16A task not started 24 h after its date is cancelled automatically and its orders go back to the poolAuto-cancel, or only list them for ops
X18Goods that cannot be scanned back get a "not returned" mark with a reason; that DO can only failWho carries the loss; whether a second approver is needed
X19A HOLD the store never decides goes on the ops follow-up list after 3 days and stays HOLDAccept, or set a deadline that fails it
X21Raw GPS points are kept 90 days; the driven route stays with the taskThe retention period (privacy policy)
C6Every task that collected cash needs one handover record in TMS; tasks without one are listed, never closed automaticallyWhether some cash is handed over outside TMS
C7The cashier PIN must belong to staff of the task's own storeWhether staff covering another store may sign
W13Only an approved leave keeps a driver out of planning; a pending leave only warnsAccept the change from v1
W18A driver who clocked out cannot clock in again that dayAccept, or allow a second clock-in
R6Reports read live data, no scheduled refreshAccept; whether the "DO Transaction" report is still needed
A15Wrong staff PINs per store: at most 30 in 15 minutes from all devices, then that store's PIN checks pause 15 minutes and ops get an alertThe numbers

Rules that the older work-scope list still shows as open but that are now decided: history retention and token lifetimes (rules A6 and A17), retry limits for events and jobs (rule A2), and the list of import/export configs to port. One more topic is open inside the team, not with BigC: whether on-demand orders should also go through planning in v2. No rule below depends on it yet.


1. Intake / DO ​

Order messages from OMS (BigC's order system) arrive over Kafka and become delivery orders (DOs).

#RuleStatusExample
I1When Kafka delivers the same order message again (same message id), the system must apply nothing and keep the first result.Enforced — delivery_order/delivery_order.service.ts:204, :118, :150 (unique key on the history row, activity_log/activity_log.schema.ts:46); consumer skip kafka/kafka.consumer.ts:98, kafka/consumers/kafka.consumes.schema.ts:10A consumer restart at 09:05 replays message m-7781 for DO00012345: still one DO and one history row.
I2When an order message has no message id, the system must refuse it and keep it in the dead-letter table with the reason.Enforced — delivery_order/delivery_order.service.ts:203, delivery_order/delivery_order.kafka_consumer.ts:31A test message without message_id lands in the dead-letter table with "missing message_id".
I3When a message cannot be applied for any reason, the system must keep a dead-letter row with the reason; no message is lost silently.Enforced — kafka/kafka.consumer.ts:133-152A message that breaks the DTO is stored with its partition, offset and error text for ops to read.
I4When a message names a store or channel TMS does not know, the system must park it, let ops replay it after the master data is added, and hold the later messages of that order behind it so they apply in order.Code differs — the store lookup throws not_found (store/store.service.ts:24), the message goes to the dead-letter table, later messages of the same order are applied anyway, and there is no replay endpointStore 1088 is not set up yet: its order for 14:00 must wait, not be lost, and apply once ops add store 1088.
I5When a message's source version is not newer than the DO's, the system must skip it as stale, even if it is a cancel.Code differs — there is no version check; every new message id overwrites the DO (delivery_order/delivery_order.service.ts:206-208)An old "address changed" message arriving after a newer one must not undo the newer address.
I6When the DO is already DELIVERED, the system must skip any later OMS message for that attempt.Code differs — the DO is updated in place whatever its status (delivery_order/delivery_order.service.ts:139)A late OMS update at 18:40 for DO00012345, delivered at 15:10, must not change it.
I7When OMS sends a cancel for an order TMS has never seen, the system must record a dead row "cancel for unknown order" and create nothing.Code differs — a new DO is created with the message's status, so a cancelled DO appears (delivery_order/delivery_order.service.ts:107, :211)A cancel for order 99001 that never reached TMS must not create a DO.
I8When OMS cancels an order, the system must apply it only while the DO is UNASSIGNED, or ASSIGNED and not yet in a task, and has no failed attempt; otherwise refuse it and keep it on the ops follow-up list until an admin cancels the DO with a reason or marks it handled. (Code today just overwrites the status in any state.)Waiting on BigCDO00012345 is already loaded on task TK00000456 at 10:30: the cancel is refused and shows on the ops list.
I9When OMS redelivers a CANCELLED or FAILED_DELIVERY order, or an admin opens a partial re-delivery, the system must open a new attempt (a new DO linked to the old one), once per message or per attempt; a field failure never opens one.Waiting on BigCA customer refused 2 of 5 items; the admin re-delivers the 2 items as a new DO linked to DO00012345.
I10When a DO is created, the system must start it UNASSIGNED with no approval time.Code differs — new DOs start PENDING (delivery_order/delivery_order.service.ts:107) and the code status list has no UNASSIGNED, ASSIGNED, HOLD or FAILED_DELIVERY (common/enums/delivery_order.enum.ts:1-8)A new order for store 1021 shows in the pool as UNASSIGNED until the assign rules approve it.
I11When the system numbers a DO, the number must be generated, unique, and "DO" + 8 digits. (Code generates it only when the message has none, delivery_order/delivery_order.service.ts:99-105.)Waiting on BigCDO00012345, not v1's DO + date + 4 digits.
I12When two creates of the same DO number race, the system must keep one DO and apply the second as an update.Enforced — unique delivery_no (delivery_order/delivery_order.schema.ts:14), fallback to update delivery_order/delivery_order.service.ts:212-215Two pods consume the same new order at 08:00:01; one DO exists.
I13When an order ships from a store, the system must copy that store's identity and coordinates onto the DO.Enforced — delivery_order/delivery_order.service.ts:80-92An order from store 1021 takes store 1021's latitude and longitude as its pickup point.
I14When a DO has no customer coordinates, the system must geocode it from the address cache, else by a live call; a geocode failure is recorded on the DO and does not stop the create.Designed (code only reads coordinates from the message, delivery_order/delivery_order.service.ts:55-78)An address in Lat Phrao with no lat/long is found in the cache of 9,725 known addresses.
I15When a DO is created, its weight and volume must be the sums of its item and package lines as sent.Designed (code stores the totals given in the message)3 lines of 4.5 kg, 2 kg and 1.25 kg make a 7.75 kg DO.
I16When a DO is completed, attempted or failed, the system must stamp who did it once; the first stamp wins and later attempts are history rows.DesignedThe first failed attempt at 11:20 by driver D0042 stays the "attempted by" stamp after a second attempt next day.
I17When a DO is created or changed, the system must write one history row in the same database step as the change.Enforced — delivery_order/delivery_order.service.ts:112-122, :137-156The admin changes the delivery note at 13:02: the DO and its history row are saved together or not at all.
I18When a DO's status changes, the system must queue the OMS and staff-app update events with the change (channels DONJAI, CHOKCHAI, HLH to OMS; SPO, OD, HLH to the staff app), and a worker sends them.Enforced — delivery_order/delivery_order.service.ts:152-154, :168-195An HLH order delivered at 15:10 queues both do_update_oms and do_update_staffapp.
I19When ops create a DO by hand or by Excel import, the system must apply the same rules as for a Kafka order.Designed40 DOs imported by Excel for store 1021 get the same numbering, store copy and checks as Kafka DOs.
I20When ops change the Kafka field mapping, the system must store only valid mappings, never executable code, and activate a version only after a preview with 0 errors; every Kafka DO records its mapping version.DesignedMapping version 7 cannot go live until its preview of 50 sample messages shows no error.

2. Planning ​

One plan per store and date; route optimisation with Google; manual edits; generate tasks.

#RuleStatusExample
P1When intake accepts an order as ready to plan, the system must choose its partner and assignment type by the assign rules, tried in priority order, first match wins.DesignedRule 1 "HLH channel → partner Lalamove" wins over rule 2 "weight ≤ 20 kg → own fleet".
P2When a weight rule has only a maximum, the system must apply it when the order's total weight in kg is at or below that maximum.Designed (v1 never fires such rules)A "max 20 kg" rule applies to an 18.4 kg order.
P3When the optimiser plans a route, it must never exceed the slot's max orders or max load, and never plan a stop outside the slot's time window.Code differs — weight, volume and time windows are sent to Google (plan/plan_optimize.service.ts:537-539, :587); a max-orders cap per slot is notA 4-wheel slot of 1,000 kg and 25 orders gets at most 25 orders and 1,000 kg.
P4When an ops admin assigns or moves a stop past a cap or window, the system must show the excess, require a confirmation, and store the override on the stop with who approved it and why.Code differs — manual move and assign accept any overrun with no confirmation; limits are not sent (plan/plan_optimize.service.ts:313-316) and the overrun only shows as a violation; no override recordMoving a 150 kg stop onto a truck at 950 / 1,000 kg asks "exceeds by 100 kg, confirm?" and records the reason.
P5When a plan splits orders by channel, each channel must only use slots that serve it.DesignedHLH orders only go to the slots marked for HLH.
P6When an order is left unassigned, the system must give it a reason.Enforced — plan/plan_optimize.service.ts:237-240, plan/plan_operation.service.ts:78-82DO00012399 shows "no round meets the promised time" on the unassigned list.
P7When planning places an order, it must use the coordinates already on the DO and geocode nothing itself; an order without coordinates stays unassigned, and a manual assign of it is refused.Enforced — plan/plan_optimize.service.ts:201, :239; plan/plan_operation.service.ts:111-117DO00012400 has no lat/long: it is listed as "no destination coordinate".
P8When planning selects orders, it must take only orders that are UNASSIGNED and approved, not on ops hold, on no other route, and from the plan's store.Code differs — candidates are store + delivery date + status PENDING only (plan/plan_optimize.service.ts:433-441); no approval, hold or other-plan checkA DO on ops hold at store 1021 must not be planned for tomorrow's 09:00 round.
P9When orders are added to a route, the route must not have started; capacity, delivery type and shift mismatches are shown in a preview and the admin confirms.DesignedAdding 3 orders to route 2 of store 1021 at 08:40, before it leaves at 09:00, shows "over by 40 kg" first.
P10When a plan is saved, the orders it holds must equal the orders in its routes plus its unassigned list, and no order may be in two routes or two plans.Code differs — inside one plan, assign takes only orders from its unassigned list (plan/plan_operation.service.ts:100-106) and unassign returns them (:77-83); there is no claim, so one order can sit in two plans of the same store and dateDO00012345 cannot be on both the morning plan and a second plan for 12 Nov.
P11When a plan is created, there must be only one plan per store and date.Code differs — createPlan has no check and the store + date index is not unique (plan/plan.service.ts:30-58, plan/plan.schema.ts:180)A second "store 1021, 12 Nov" plan is refused.
P12When a driver is attached to shifts, no two of his shift windows may overlap on any date; a window crossing midnight counts on both dates.DesignedDriver D0042 on 18:00-02:00 cannot also take 01:00-05:00 next day.
P13When a plan's date ends (store local time), the system must release the orders still unassigned in it so the next plan can select them.DesignedDO00012400 left unassigned on 12 Nov is free for the 13 Nov plan from 00:00.
P14When a stop is moved, assigned or unassigned by hand, the plan must be in status planned.Enforced — plan/plan_operation.service.ts:149-151A plan still pending (never optimised) refuses a drag-and-drop.
P15When a plan is generated, the system must refuse to optimise it again.Enforced — plan/plan_optimize.service.ts:165-167Re-plan on a generated plan returns "Plan is already generated".
P16When two people save the same plan at once, the second save must be refused with "reload and try again" instead of overwriting.Enforced — plan/plan_optimize.service.ts:909-916Two supervisors drag stops on the store 1021 plan at 08:15; the second gets a 409.
P17When tasks are generated from a plan, the system must create exactly one task per route, all or nothing, even if the run is stopped and repeated.Code differs — the endpoint answers "Task generated successfully" without creating any task (plan/plan.service.ts:121-132)6 routes generate 6 tasks TK00000451 to TK00000456; a rerun creates none extra.
P18When ops re-plan the whole plan, manual edits are dropped; when the system recalculates after a manual edit, pinned stops and hand-removed orders are kept.Enforced — plan/plan_optimize.service.ts:196-202An order taken out by hand stays out after a recalculate, but returns after a full re-plan.
P19When several drivers fit one slot, the system must attach the one with the fewest booked hours that day, then by driver code.Waiting on BigCD0042 (4 h booked) gets the slot before D0017 (6 h booked).
P20When a plan is created or optimised, every vehicle type in its template must still exist and the store must have a usable location.Enforced — plan/plan.service.ts:69-73, plan/plan_optimize.service.ts:516-517Template "Weekday 1021" naming a deleted vehicle type "4W-OLD" is refused.
P21When the system logs a Google request, it must not store the auth header.Enforced — google/google.service.ts:63The Google log row shows the authorization header masked.
P22When ops press optimise, the system must run it as a background job, answer within 1 s, run one job per plan, and never leave a plan stuck optimising more than 11 minutes.Code differs — the Google call runs inside the request (plan/plan_optimize.service.ts:215-216)Pressing optimise for 300 orders returns at once and the result appears about 1 minute later.
P23When a plan changes (re-plan, recalculate, move, assign, unassign), the system must write one plan history row, once.Enforced — plan_history/plan_history.schema.ts:72, plan/plan_optimize.service.ts:260Moving DO00012345 from route 1 to route 3 at 08:20 adds one row "MOVE_STOP by supervisor S01".

3. Execution ​

The driver's run: accept, check in, load, depart, deliver with proof, failed attempts, return and close-out, GPS. No execution code exists yet; the task module holds only the schema and a unique task number.

#RuleStatusExample
X1When a DO, task or stop changes status, the change must go through a guarded transition that checks the current status.Code differs — the admin update endpoint writes any status directly (delivery_order/delivery_order.controller.ts:49-55, delivery_order/delivery_order.service.ts:139)Setting DO00012345 from UNASSIGNED straight to DELIVERED must be refused.
X2When a driver checks in at the pickup store, the system must reject it beyond the store radius (default 300 m, today's PROD value).Waiting on BigCDriver D0042 at 450 m from store 1021 cannot check in.
X3When a driver arrives at a customer beyond 2,000 m, the system must warn, not block, and set the out-of-location flag.DesignedArrival 2.4 km from the pin in Bang Na is allowed and flagged.
X4When a driver checks in back at the store beyond the store radius (300 m), the system must warn, never block.DesignedReturn check-in at 350 m from store 1021 at 17:30 is recorded with a warning.
X5When a driver departs, a staff PIN is optional for a full load (if given it must match an employee and sets the dispatch time), required for a short load and to complete unload, and not needed for an ops close-out.DesignedDeparting with 48 of 50 boxes needs the PIN of store 1021 staff.
X6When goods are scanned, the system must match the package barcode first, then the item barcode, then the product, and never count a line above its ordered quantity. (Load scan is parked for now.)DesignedA fourth scan of an item ordered 3 times is refused.
X7When a driver confirms delivery, the system must require: the task not held, no open ops hold on the DO, the DO OUT_FOR_DELIVERY at an ARRIVED stop, every proof type its delivery method needs (default one photo), and for COD the payment photo.DesignedCOD DO00012345 cannot be confirmed at 15:10 until the payment photo is uploaded.
X8When a delivery attempt fails, the system must move the DO to HOLD whatever the reason.Designed"Customer not home" at 11:20 moves DO00012345 to HOLD.
X9When ops hold a DO, it must be UNASSIGNED or ASSIGNED with its task PENDING or ACCEPTED; the hold is released when the DO reaches DELIVERED, FAILED_DELIVERY, CANCELLED or UNASSIGNED, or leaves its task before departure.DesignedOps can hold DO00012345 at 08:30 before task TK00000456 departs, not after.
X10When delivery starts, a held DO must not move to OUT_FOR_DELIVERY and cannot be delivered until released.DesignedDO00012345 on ops hold stays ASSIGNED when the truck leaves at 09:00.
X11When a stop is DEPARTED or CANCELLED, it is final.DesignedStop 3 departed at 10:05 cannot be re-opened by the driver.
X12When a DO is redelivered, it must leave its task and stop and be planned again from its new date.DesignedDO00012345 failed on 12 Nov returns to the 13 Nov pool.
X13When a DO is redelivered, its counted attempts must be below max_delivery_attempts (setting, default 3).Waiting on BigCA fourth counted attempt is refused; the DO can only fail.
X14When an attempt fails, attempt_count goes up by one and never down; only reasons flagged "counts toward the cap" count for rule X13 (assumed on for refused, cannot contact, wrong address).Waiting on BigCTwo "customer rescheduled" holds do not use up the 3-attempt cap.
X15When a task is closed out, each DISPATCHED, OUT_FOR_DELIVERY or HOLD DO gets one decision: redeliver (back to UNASSIGNED with a new date, default next day same window) or fail with a reason; an on-demand DO can only fail.DesignedAt 18:00 close-out, held DO00012345 is redelivered for 13 Nov 10:00-12:00.
X16When a task is still PENDING or ACCEPTED 24 h after the end of its date (task_start_grace_hours), the system must cancel it and return its DOs to the pool; DISPATCHED or IN_TRANSIT tasks past that time are listed for ops close-out.Waiting on BigCTask TK00000456 for 12 Nov never accepted is cancelled on 14 Nov at 00:00.
X17When the driver opens the next stop or checks in at the store, every DO in the current stop must be settled (DELIVERED, HOLD, CANCELLED, or held or cancelled upstream); otherwise the next stop is refused, and a return check-in leaves the stop ARRIVED for close-out.DesignedStop 4 with one DO still out for delivery blocks opening stop 5.
X18When goods cannot be scanned back at return, the system must take a "not returned" mark with a mandatory reason, and that DO can only fail.Waiting on BigCA missing 5 kg rice bag on DO00012345 is marked "not returned: damaged" and fails.
X19When a HOLD DO is never decided, the system must put it on the ops follow-up list 3 days after its task date and keep it HOLD.Waiting on BigCA HOLD from 12 Nov appears on the ops list on 15 Nov.
X20When a driver has an ACCEPTED, DISPATCHED or IN_TRANSIT task, the app samples GPS every 30 s and keeps a point only 25 s and 30 m after the last kept one; upload interval is per vehicle type, default 10 min.DesignedNo GPS is collected while driver D0042 has no active task.
X21When GPS points are stored, raw points are kept 90 days and the driven route stays with the task.Waiting on BigCPoints from 12 Nov are removed after 10 Feb.
X22When a visit (a DO on a task) changes, it must change only through a command that writes one history row per DO touched, in the same database step.DesignedDeparting with 12 DOs writes 12 history rows sharing one action id.
X23When a visit is closed, the values it used are stamped once and never re-synced; a closed visit changes only by an audited ops override with a reason.DesignedThe address delivered to on 12 Nov stays on the visit even if OMS later edits the DO.
X24When a DO is on a task, it has at most one open visit, and current_task_id names that task.DesignedDO00012345 cannot be live on TK00000456 and TK00000460 at once.
X25When a DO with an open visit on another task is added to a task, the old visit is closed as "reassign" in the same step; refused while that stop is ARRIVED unless ops give a reason; a late proof from the old task is kept as a conflict event.DesignedMoving DO00012345 to another truck at 09:40 closes its visit on TK00000456.
X26When ops add a DO ad hoc, the task must be PENDING or ACCEPTED.DesignedAn ad-hoc add to a task that left at 09:00 is refused.

4. COD ​

Cash on delivery: collection at the door, cash handover to the store cashier, adjustments. No COD code exists yet; the DO only has is_cod and cod_amount.

#RuleStatusExample
C1When an order is created, the amount due (cod_amount) is set from OMS; collection and handover never write it, and a later OMS update changes it only while the COD is UNPAID (after PAID it is logged for ops).Code differs — every OMS update and the admin update overwrite cod_amount in any state (delivery_order/delivery_order.service.ts:139, delivery_order/delivery_order.dto.ts:64-66)OMS changes DO00012345 from ฿1,250 to ฿1,100 after the driver collected ฿1,250: the amount stays and ops see the request.
C2When the driver collects COD, the collected amount must equal the amount due; no part payment at the door.DesignedA ฿1,250 order cannot be closed with ฿1,000 collected.
C3When COD status changes, it must move only UNPAID → PAID → RECEIVED, each step once.DesignedA RECEIVED order cannot go back to PAID.
C4When a handover is recorded, expected = the sum collected on the task's PAID orders, and 0 ≤ received ≤ expected; more than expected is refused.DesignedTask TK00000456 collected ฿8,430; ฿8,500 received is refused.
C5When a task has at least one PAID order, it gets one handover, only after close-out; corrections only by adjustment.DesignedA second handover for TK00000456 is refused; a ฿100 correction is an adjustment.
C6When a task collected cash, it must have one handover record in TMS; tasks without one are listed per store by age and never closed automatically.Waiting on BigCTK00000456 with ฿8,430 and no handover by 13 Nov shows on the store 1021 open-cash list.
C7When a handover is signed, the cashier PIN must belong to an employee of the task's store, and PINs are unique per store.Waiting on BigCA cashier of store 1033 cannot sign for a store 1021 task.
C8When the system shows a driver's COD day, the day is the date of cod_collected_at in Asia/Bangkok.DesignedCash collected at 00:30 on 13 Nov counts for 13 Nov, not 12 Nov.
C9When a handover is short (variance below 0), it must carry a reason from the store's list.Designed฿8,330 received of ฿8,430 needs reason "change given short".
C10When OMS sends a payment method, the system stores it as sent and never validates it against a list.Designed"PROMPTPAY_QR" is stored even if the list does not have it.
C11When an adjustment is recorded, it is signed and not 0, keeps 0 ≤ net received ≤ expected, never changes order statuses, and the shortfall owed is minus the net variance.DesignedA +฿100 adjustment brings ฿8,330 up to the expected ฿8,430.
C12When the app sends a COD write (confirm, handover, adjustment), it carries an idempotency key: the same key returns the first result with no new write, and the same key with a different body is refused (409).DesignedA double tap on "confirm ฿1,250" at 15:10 records one payment.
C13When any module changes COD handover, adjustment or the DO's COD fields, it must call the COD commands; no generic create, update or delete.DesignedIntake changes the amount due only through "set amount".
C14When the optional setting is on, a driver who still holds unsettled cash cannot be dispatched on a new task (off by default).DesignedWith the setting on, D0042 holding ฿8,430 from yesterday cannot depart at 09:00.

5. Workforce ​

Drivers, vehicles, partners, leave, bookings, duty records (one driver's working day), clock-in and clock-out. No workforce code exists yet except user login.

#RuleStatusExample
W1When a booking is made, its vehicle must be the driver's home vehicle, and its store and channel must be among the driver's.DesignedD0042 based at store 1021 cannot be booked for store 1033.
W2When a booking is made, its partner must be the driver's partner.DesignedA Lalamove driver cannot be booked under partner "own fleet".
W3When a window is added for a driver, his windows on one date must never overlap, whether from bookings or the plan template; the check is in the same update.DesignedD0042 booked 09:00-13:00 cannot get 12:00-16:00 the same day.
W4When a booking is created, booking_no must be unique.DesignedBK00001234 cannot exist twice.
W5When planning or workforce work with a driver's day, there is at most one duty record per driver per date, shared by both.DesignedD0042 has one duty record for 12 Nov.
W6When work is offered or assigned, only ON_DUTY drivers qualify.DesignedD0042 not yet clocked in gets no on-demand offer at 08:50.
W7When a driver clocks in or out, the system needs at least one photo and a position; out-of-range only warns.DesignedClock-in 600 m from the store at 08:55 is allowed with a warning.
W8When a driver clocks in, it is marked against the first window start, to the minute: before = early, same minute = on time, after = late with minutes late stored.DesignedWindow 09:00, clock-in 09:07: late by 7 minutes.
W9When a driver has a task in progress, clock-out and break start are refused.DesignedD0042 on a run at 12:30 cannot start a break.
W10When ops hold a driver, his not-started tasks for that date are unassigned; started tasks stay with him.DesignedHold at 11:00 frees D0042's 14:00 task, not the 09:00 one in progress.
W11When an on-demand task is offered, the acceptance timeout is the store's on_demand_sla_accept_minutes, else the route strategy value, else 15 minutes; on timeout the task is re-offered and the driver held.DesignedStore 1021 sets 10 min; D0042 does not accept by 10:10, the task goes to the next driver.
W12When a booking is cancelled, its duty record must be SCHEDULED, or ON_DUTY with no task in progress; with no other window left that day, SCHEDULED becomes CANCELLED and ON_DUTY becomes OFF_DUTY at the cancel time.DesignedOps cancel D0042's 13:00-17:00 booking at 13:30 with no task: he goes OFF_DUTY at 13:30.
W13When a driver has leave, only APPROVED leave makes him unavailable for attach, booking confirm and clock-in; PENDING leave only warns; two live leaves of one driver may not overlap.Waiting on BigCD0042's pending leave for 14 Nov shows a warning but does not block planning.
W14When a user logs in, the account must be ACTIVE.Enforced — user/user.service.ts:104-112A driver account set inactive on 10 Nov cannot log in on 11 Nov.
W15When a driver is attached to a window or clocks in, he must be ACTIVE; only ACTIVE vehicles can fill a slot.DesignedVehicle 1กข-2345 marked in repair cannot fill tomorrow's slot.
W16When a driver calls the API, the command acts only on his own records; the driver id comes from the token, never from the request.DesignedD0042 asking for D0017's task by id is refused.
W17When a second-factor or reset code is used, it is 6 digits, expires after otp_setting.sms_ttl, can be re-sent only after otp_setting.rate_limit, and is locked after 5 wrong entries.Code differs — 6 digits is enforced (common/utils/otp.ts:13, :25), but expiry is a fixed 10 minutes (common/utils/otp.ts:15), there is no re-send limit and wrong entries are never counted (common/utils/otp.ts:68-98)A sixth wrong code at 08:12 is refused even if correct.
W18When a driver has clocked out, he cannot clock in again that day.Waiting on BigCD0042 clocked out at 15:00 cannot clock in at 16:00 the same day.

6. Master data ​

Stores, vehicles, vehicle types, plan templates, templates, export configs, files, settings.

#RuleStatusExample
M1When a store is saved, its code is required and unique.Enforced — store/store.schema.ts:13A second store "1021" is refused.
M2When a vehicle is saved, its code is required and unique.Enforced — vehicle/vehicle.schema.ts:13Vehicle code V-0101 cannot be used twice.
M3When a vehicle is saved, its plate number must be unique.Code differs — plate_number has no unique index (vehicle/vehicle.schema.ts:19-20); v1 has 2 plates used twice that must be fixed before the movePlate 1กข-2345 on two vehicles is refused.
M4When a vehicle type is saved, its code is unique.Enforced — vehicle_type/vehicle_type.schema.ts:31Vehicle type "4W" exists once.
M5When a plan template is saved, its code is unique.Enforced — plan_template/plan_template.schema.ts:25Template "WEEKDAY-1021" exists once.
M6When a print, email or SMS template is saved, its code is unique and an email template must have a subject.Enforced — template/template.schema.ts:84, template/template.service.ts:65, :70An email template "otp_email" without a subject is refused.
M7When an export config is saved, its entity must be available, every field column must be a real field of it, a footer sum needs a field column, and a script column needs a script.Enforced — export_config/export_config.service.ts:54-67A column destination_address.zip_code_x that does not exist is refused.
M8When two people edit the same template or export config, the second save must be refused with "reload and try again".Enforced — export_config/export_config.service.ts:45, template/template.service.ts:53Two admins edit the "POD print" template at 10:00; the second gets a 409.
M9When templates are rendered in bulk, the number of records is capped by the setting max_bulk_records.Enforced — template/template.render.service.ts:50Printing 600 PODs at once is refused above the cap.
M10When a user asks to upload a file, the purpose must be known, the extension allowed for that purpose and by the storage setting, the size within the smaller limit, and the user under 30 upload links a minute and 20 pending uploads.Enforced — attachment/attachment.service.ts:64-96A 30 MB .exe as a POD photo is refused.
M11When a file is read, it must have finished uploading.Enforced — attachment/attachment.service.ts:289A POD photo still uploading returns "not ready".
M12When drivers, vehicles and partners move from v1, a driver on leave with no dates becomes ACTIVE with an approved open-ended leave for ops to close, and every driver-vehicle link recorded on one side only is confirmed by ops first.Designed77 v1 drivers on leave with no dates arrive in v2 as ACTIVE plus open leave.
M13When a store moves to v2, every store and channel code its orders use must exist in master data first (v1 drops orders for 8 missing store codes today).DesignedStore 1088 is added before its first v2 order arrives.

7. Admin / reporting ​

Admin screens, reports, export and import, roles and permissions. Reports are not built yet.

#RuleStatusExample
R1When a report is run, it must have a date range of at most 31 days; no unbounded report read.DesignedA POD report for 1 Oct to 15 Nov is refused.
R2When a report is built, rows are unique per delivery_no (POD, Order Info) and per delivery_no + transaction date (DO Transaction).DesignedDO00012345 shows once in the POD report.
R3When a partner user reads any data, partner scope is applied in the data layer, not by screen; a partner user with no partner id sees nothing.DesignedA Lalamove user never sees an own-fleet task from any endpoint.
R4When internal or partner users log in, it is by password only; Cognito and social login are removed.Enforced — auth/auth.service.ts:88-91 (password is the only login path)A partner user signs in with username and password, no Google button.
R5When an export is started, it is streamed and capped at 150,000 rows per file; above that it is refused before it starts with "narrow the range".DesignedA 31-day all-store export of 210,000 rows asks for a narrower range.
R6When a report is opened, it reads live data, with no scheduled refresh.Waiting on BigCThe Order Info report at 10:15 shows a DO delivered at 10:14.
R7When any endpoint is called, the system must check a permission key held by one of the user's roles; roles are editable data.Code differs — controllers check a fixed list of 4 roles in code (common/guards/auth.guard.ts:34), no permission keysRemoving plan.re_plan from role "Supervisor" stops re-plan for those users.
R8When roles are edited, the editor needs role.update; the system_admin role cannot be edited or deleted; the last system_admin cannot be removed (409); nobody may change their own roles or stores (403); a seeded role or one still held cannot be deleted.DesignedSupervisor S01 cannot add store 1033 to his own account.
R9When a key is removed from a role, users of that role lose it from their next request, with no new login.DesignedRe-plan is refused at 10:01 for a key removed at 10:00.
R10When a role is saved, or a user's roles or stores change, the system writes an activity log row with the keys added or removed.DesignedAdding store 1033 to user ops02 at 09:30 is in the history.

8. Platform / auth ​

Events, jobs, schedules, history and settings, login, tokens, scope. This is the most built part of the prototype.

#RuleStatusExample
A1When an outbound event is sent, it carries an event id header; delivery is per topic, default at most once, so an event in flight when its pod crashed goes DEAD for a person to resend and is never resent automatically.Enforced — kafka/kafka.target.ts:70, :83; jobs/fast_lane.worker.ts:105-112, :256-261The do_update_oms for DO00012345 in flight during a pod crash at 15:10 shows DEAD on the jobs screen.
A2When a job fails with an answer from the target, it is retried after 1, 2, 4, 8 and 16 minutes, then goes DEAD.Enforced — jobs/jobs.constants.ts:21-22, jobs/fast_lane.worker.ts:110-112An OMS update failing from 15:10 is retried until about 15:41, then DEAD.
A3When the same business event is queued twice, only one job exists.Enforced — jobs/jobs.schema.ts:92Two pods queue do_update_oms:DO00012345:4; one job is sent.
A4When a schedule slot is due, it runs once across all pods.Enforced — schedules/schedules.service.ts:34, :103-110The 00:00 day-close slot runs once with 4 pods up.
A5When a DEAD job is retried by a person, a second click on the same job is refused (409).Enforced — jobs/jobs.controller.ts:45-57Two admins press retry on the same DEAD job; one retry happens.
A6When a history row is written, it expires after the retention in settings (60 days by default); a changed value applies to new rows only.Enforced — activity_log/activity_log.schema.ts:40, log/expire_at.ts:33-35Ops set 90 days on 11 Nov; rows written from then expire after 90 days.
A7When a retention value is saved, it must be 1 to 365 days; an invalid stored value falls back to the code default.Enforced — log/log.constants.ts:15-16, settings/settings.service.ts:280-285Saving 0 or 400 days is refused.
A8When a new release is deployed, no stored setting value changes: boot only inserts a missing setting record; a key never saved reads the current code default.Enforced — settings/settings.service.ts:253-265, :302-304Ops set Kafka log retention to 30 days; the next deploy keeps 30.
A9When activity history is kept, every row has the same retention; no module overrides it.Enforced — activity_log/activity_log.schema.ts:36-40DO and plan history rows of 12 Nov both expire on the same day.
A10When a setting is saved, one activity row records the actor and the before and after of changed keys; a secret is stored encrypted, always shown masked and logged only as "changed".Enforced — settings/settings.service.ts:130, :179-185, :231-237Changing the universal OTP shows changed in history, never the code.
A11When two people save the same setting record at once, the second save is refused with "reload and try again".Enforced — settings/settings.service.ts:222-230Two admins edit Kafka settings at 14:00; the second gets a 409.
A12When a command changes records, the history is one row per record sharing one action id, written in one insert; changed values are masked and capped at 16 KB.Enforced — activity_log/activity_log.service.ts:44, :68; activity_log/changes.ts:6, :18-25A bulk change of 100 DOs writes 100 rows in one insert.
A13When history is searched, the date window is at most 31 days.Enforced — activity_log/activity_log.query.ts:41-42Searching user ops02's actions for 1 Sep to 15 Nov is refused.
A14When a staff PIN is entered wrong, at most 5 tries per device per 15 minutes are allowed; then that device gets "locked" (423) for 15 minutes.DesignedThe sixth wrong PIN at 09:10 on the store 1021 tablet locks it until 09:25.
A15When wrong PINs pile up for one store, at most 30 from all devices in 15 minutes are allowed; then PIN checks for that store pause 15 minutes and ops get an alert.Waiting on BigC31 wrong PINs at store 1021 by 09:14 pause its PIN checks until 09:29.
A16When staff PINs are issued, only staff in the roles of staff.generate_pin_code_roles get one, and the PIN email uses the configured template.DesignedA cashier role gets a PIN by email; a driver does not.
A17When a user logs in, the access token lives 24 hours (with a per-application override) and a refresh token 30 days.Code differs — the access token lives a fixed 1 year (common/constants/auth.constant.ts:2) and there is no refresh tokenA token issued 11 Nov 08:00 is refused on 12 Nov 08:01 and renewed by the refresh token.
A18When a user logs out or is revoked, both tokens of the session end; revoke can end every session of a user.Code differs — /auth/revoke is a public endpoint that deletes one token without waiting for it (auth/auth.controller.ts:31-32, auth/auth.service.ts:173); no per-user revokeRevoking ops02 ends his phone and laptop sessions at once.
A19When a password is wrong 10 times within 60 minutes, the account locks for 5 minutes; a success resets the count; an admin can unlock at once.Code differs — login accepts unlimited wrong passwords (user/user.service.ts:114-122); the locked_out state exists but nothing sets itThe tenth wrong password at 08:20 locks ops02 until 08:25 even with the right one.
A20When the universal OTP setting is on, the fixed code in settings passes for every user in every environment, including PROD (accepted risk, as v1).Enforced — auth/auth.service.ts:39-47, common/utils/otp.ts:85-87 (off by default)While no SMS provider exists, support resets a driver's password with the universal code.
A21When an application is disabled, nobody can log in through it.Code differs — enabled is never checked (auth/auth.service.ts:53-65)The old driver app disabled on 10 Nov must refuse logins.
A22When an application secret is stored or checked, it is stored encrypted and never written to a log.Code differs — stored as plain text, and a wrong secret is written to the log (auth/auth.service.ts:67-71)A mistyped secret key must not appear in the server log.
A23When a password is stored, it is hashed with bcrypt.Code differs — sha256 of password plus creation time (user/user.service.ts:57-58)A leaked user table must not allow fast password guessing.
A24When a password is set, it must meet the minimum length in settings.DesignedA 4-character password is refused with "at least 8 characters".
A25When a user holds several roles, his permissions and the apps he can log into are the union of all his roles.Code differs — a user has one role field and the app check is one role (user/user.schema.ts:24-25, auth/auth.service.ts:93)ops02 with roles "Dispatcher" and "Report viewer" can do both.
A26When staff read or change records, they see only their stores' records (list, search, export, and get, update or delete by id); a client filter cannot widen it; a selected store outside their stores is refused (403); a user with no store sees nothing; jobs and Kafka consumers see all stores.Code differs — there is no store filter; any admin token reads every store (delivery_order/delivery_order.controller.ts:27-33, common/base/base.service.ts:104-126)A store 1021 user asking for a store 1033 DO by id gets 404.
A27When a system_admin calls any endpoint, the guard lets him through without reading keys and he sees every store.Code differs — the guard checks the role list only, and the seeded system user has role admin, not system_admin (common/guards/auth.guard.ts:34, user/user.service.ts:75)The system admin can open every admin screen.
A28When a route is not marked public, it needs a valid token.Enforced — common/guards/auth.guard.ts:77-87Calling /v1/plan with no token is refused.
A29When Redis is down during a request, the token is not accepted unchecked.Enforced — common/guards/auth.guard.ts:29 (the error rejects the request)During a Redis outage at 03:00 admin calls fail instead of passing.
A30When a user is created, the username is unique, and login matches it in lower case.Enforced — user/user.schema.ts:42, user/user.service.ts:149-154, auth/auth.service.ts:89"Ops02" and "ops02" are the same login.
A31When a user is created or edits his profile, the system_admin role cannot be given through the API and a user cannot change his own role.Enforced — user/user.dto.ts:25-26, :40; common/enums/user.enum.ts (assignable roles)ops02 editing his profile cannot set role admin.
A32When a list is read through the base service, the page size is 50 by default and at most 500, and every read names its fields.Code differs — default 10 and no maximum (common/base/base.service.ts:104)Asking for 1,000 DOs returns at most 500.
A33When business records (DO, DO item, task, stop, plan, booking, handover) change, it is only through their commands; there is no generic create, update or delete route for them.Code differs — DOs have POST and PUT, DO items POST, PUT and DELETE (delivery_order/delivery_order.controller.ts:42-55, delivery_order_item/delivery_order_item.controller.ts:50-70)An admin cannot delete a DO item of a delivered DO with a raw DELETE call.
A34When a file is uploaded, the bytes go straight to S3 through an upload link, never through the API pod.Enforced — attachment/attachment.service.ts:64-100A 4 MB POD photo goes from the phone to S3 directly.