Skip to content

v1 vs v2 ​

What changes from TMS v1 to v2, why, what gets better and what we pay for it. It is written for the team and PM: to explain v2 to people who know v1, and to weigh scope when writing a phase spec. PROD counts were read in Oct–Nov 2026.

In short. The business rules stay. What changes is how a business action is saved, which records exist and who may write them. Plain bugs in v1 should be fixed in v1 and are not a reason for v2. The reason is a set of structural faults that come from v1's data model and its frozen framework, and those can only be patched in v1, never removed.

How to read: "Why rewrite" and "What stays the same" first. The table "Changes by area" is the reference; each row has the trade-off we accepted. The migration section covers running v1 and v2 side by side.

Why rewrite ​

The discovery work listed every known v1 weakness per module: 141 in total. They were sorted two ways.

can v1 fix it?count
yes, cheaply65fix in v1 now; not a reason for v2
partly68v1 can soften the symptom; v2 removes the cause
no8only v2
causecount
plain bug52
structural (data model, no transactions)48
limit of cc-core-lib or cc-admin-lib that only the library owner could fix20
operations13
bad data8

The 48 structural ones are the reason for v2. They come down to four faults, which are what the Foundations rules answer:

v1 faultPROD evidencev2 answer
one business action is many writes with no transaction3,720 DOs IN_LOAD with no live task; 92 prebuilds stuck GENERATINGone all-or-nothing step
the same fact is kept in several copies that drift2,968 delivered stops on in-transit tasks; 613 orders with cash counted twiceeach fact in one place
nothing recognises a repeat~218k events a week with no event id; 555 of 2,414 store-dates ran prebuild more than once in 30 daysa repeat changes data once
nothing closes1,348 DOs TO_ATTEMPT a week later; 66,087 shifts ACTIVE past their dateevery record reaches an end

What stays the same ​

  • Business rules. Who may plan which order, vehicle capacity and time windows, proof of delivery, the cashier PIN and the short-handover record, booking matching, and every report column all carry over. Where the design changes a rule on purpose, the module design says so and gives the reason.
  • Messages to OMS and the staff app. While stores move over, v2 sends messages in v1's format and v1's status names, on every status move v1 sends today, including the backward ones. Each v2 move is translated to the v1 status it maps to (04 States and flows has the name mapping). The full outbound mapping is not finished: it will be built by measuring on PROD which moves v1 actually sends. We trim the messages only after v1 is switched off.
  • What ops already rely on. Editable schedules, templates, export columns and the history tab all stay; how they are stored changes.
  • No messages to customers. Neither v1 nor v2 sends SMS or email to the end customer.

Where the design comes from ​

Three sources, each deciding a different part:

sourcedecides
BRD v2 (the planner solution spec)structure and names: statuses, plan and task, each task committed on its own rather than the whole plan at once, redelivery
v1 on PRODCOD, DO line items and Kafka intake, which the BRD does not model
Locus, the platform another Thai retailer runs for the same kind of store deliveryevidence when a BRD question was open, such as how many attempts to allow; it informed the design but nothing is copied from it as a rule

Changes by area ​

Each row reads: what v1 does, what v2 does, what gets better (with the PROD number where there is one), and what we give up.

Platform ​

areav1v2bettertrade-off
frameworkcc-core-lib and cc-admin-lib: generic CRUD, layouts, workflows, import/export and reports defined as dataplain NestJS (core-v2) and Next.js (store-v2) with a small base of our ownwe own all the code; 20 weaknesses only the library owner could fix go awayscreens and tools the library gave for free (list and detail pages, layout editor, import, report builder) have to be rebuilt; new screens are slower at first
status rulesworkflow rows in the database that admins can edit; the DO has 26 moves, 9 of them backward; a free status buttonstatus tables in code; backward moves become correction commands with a reason and an audit rowone edit can no longer silently break a rule (deleting one field stopped cancel dates for ~2 months)a new move needs a release; ops lose the free status button
outbound Kafkasent inline; a failure is logged and dropped; a new connection per message; random keyan outbox row in the same transaction, retried, DEAD visible in admin, in order per DO, event id on every messageno silent loss; about 190k database writes a day instead of ~520k for the same workat-least-once delivery: OMS and the staff app must drop duplicates by event id; one DEAD message holds back that DO's later updates
schedules and jobs15 schedule rows editable in the database, no catch-up after downtime, zero retries, stuck rows reset by handdeclared in code with an admin on/off switch and timing override; leased workers; retries; catch-up chosen per schedulea dead pod no longer leaves work stucka new schedule needs a release
history and logsa history row for every save, including log tables; 48.7M rows indexed only by id; kafka_logs 7.41M rows kept foreverone history row per business action, two indexes, 13 months; every log has a retention settingthe history tab and searches answer againa job that succeeded is not viewable in admin, only in the logs
login1-year tokens; revoke accepts any token; weak password hash; roles are a fixed list; a driver token can act on any bookingshort access token and rotating refresh token, lockout, stronger hash, roles as data with permission keyscloses the open security gapsthe admin and the driver app must handle token refresh; Designed, the next build
filesv1's own S3 module; old POD photos stored as base64 inside stop copiesthe client uploads straight to S3 with a presigned link; unconfirmed uploads are sweptno file bytes through the serverthe app must upload and then confirm

Orders and planning ​

areav1v2bettertrade-off
order status11 names that differ by module (PENDING, PLANNED, IN_LOAD, IN_TASK, TO_ATTEMPT, RE_SCHEDULED and more)8 names from BRD v2, one table for every moduleone vocabulary in code, docs and reportsreports and integrations keyed on v1 names need a mapping during migration
"hold"one word for an ops pause, a failed attempt and a booking flagthree names: ops hold, HOLD status, HELD duty statev1 has 199 DOs still flagged held at a final statusthree terms to learn
redeliverya field failure, an OMS redeliver and an admin partial all spawn copies in different waysa field failure stays on the same DO and counts an attempt; an OMS redeliver or an admin partial opens a new DO linked to the old one3,011 field-failed DOs in 14 days stay one record eachan OMS cancel is refused once the order is in a task or has a failed attempt, and goes to the ops list instead (BigC to confirm)
unknown storethe order fails and is lost; 8 store codes missing from master lost 1,537 orders since 5 Octthe order is parked with its payload and replayed once the store existsno lost ordersops have a parked list to watch
planningprebuild runs inside the web request; route strategies, shift strategies and postal-code zones; load-based generationone plan per store and date with variants, from one template per store; optimise as a background job; generate is one all-or-nothing step; orders claimed only at generateno stuck GENERATING runs, no duplicate runs; one place to read why a plan looks the way it doespostal-code zones are retired and the zoning design that replaces them is not written yet; variants are a new idea for ops
loada load record wrapping each task (7,089 loads for 7,083 tasks), plus load itemsdropped; load scan results live on the stop DO (one order at one stop)one less layer to keep in stepops used to load screens see the task instead

Execution, people and cash ​

areav1v2bettertrade-off
stopseach stop holds a full copy of its DOsa stop refers to its DOs by id2,968 delivered stops on in-transit tasks cannot happenshowing a stop with order details reads the DOs too
driver's daybooking plus shift rows that drift; planning keeps its own copyone duty record per driver per date, closed by day close66,087 stale ACTIVE shifts and 162 open bookings cannot build upworkforce is the only writer; planning has to call it
CODcash spread over cod, cod_item, delivery_payment and a stored daily sheetCOD fields on the DO; one handover per task; the cashier PIN must belong to the task's store; the daily sheet is a query613 double-counted orders and 2,030 stray payment rows cannot happenthere is no stored daily sheet, so a past sheet is rebuilt from the DOs
reportsreport tables copied by pipelines (~1.28M rows), with duplicates when a unique key is missingreports read the live tables directlyno stale or duplicated report rowsone report covers at most 31 days; live queries need the right indexes
entities50 entitieseach v1 entity is kept (14), renamed (9), merged into another (8), turned into a field or a function (6) or dropped (13); v2 ends with 30 recordsfewer joins and fewer places to keep in stepdata migration has to map every dropped and merged entity

The entity-by-entity list is in the discovery design; 03 Schema has what v2 stores.

Moving stores from v1 to v2 ​

Stores move one at a time, so v1 and v2 run side by side for a while.

  • Each DO belongs to one system, and only that system sends its Kafka messages.
  • v2's messages match v1's exactly for the whole migration, so OMS and the staff app need no change beyond dropping duplicates by the new event id.
  • A daily check compares v1 plus v2 order counts against OMS, because an order could reach the wrong system.

Before a store moves, ops clear its v1 leftovers: 3,720 DOs stuck IN_LOAD, 958 orphan stops and 1,348 undecided TO_ATTEMPT orders across all stores today.

Cost. Two systems to run and watch during the migration, and a reconciliation to keep up every day.

Still open ​

  • 17 business rules wait on BigC, for example how long a HOLD can stay undecided, whether an expired task cancels itself, and how long history is kept. Each one must be answered before the spec of the phase that uses it. The list is in 05 Business rules.
  • Zoning. Postal-code zones are retired, but the v2 zoning design is not written.
  • Some v1 numbers are still missing. About 79 weaknesses have no PROD count yet because the PROD token was refused. The counts would show how big each problem is; none of these weaknesses adds a new reason for v2.