toga-ai 1.0.818 → 1.0.819

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-10
9
+ updated: 2026-09-16
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/update_salesorder_status_from_odp.php
@@ -18,8 +18,10 @@ files:
18
18
  - library/app/client/compasscanada.php
19
19
  - worker/schedules/cron.worker.sync.json
20
20
  - worker2/Worker/Monitor/Compass.php
21
+ - worker/crons/toga2/compass/workflow/3b_import_strategic_systems_advance_shipping_notices.php
21
22
  - dbchanges2/Client_Compass/2026-09-03 - compass_intransit_email_backlog_backfill.sql
22
23
  related:
24
+ - ../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md
23
25
  - ../../../clients/compass-usa/features/contact-email-resolution.md
24
26
  - ../../../2.0/apps/worker2/features/tracking-status-refresh.md
25
27
  - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
@@ -56,9 +58,11 @@ and earlier packages). The dynamic HTML is injected into a stored `EmailTemplate
56
58
  ## How it works
57
59
 
58
60
  1. **Driving query** finds shipped sales orders and their tracking numbers (`computedStatusSlug
59
- = 'shipped'`, `c_dtInTransitEmailSent IS NULL`, rolling date window) via the ASN chain
61
+ = 'shipped'`, `c_dtInTransitEmailSent IS NULL`, rolling date window on
62
+ **`AdvanceShippingNotices.dtCreated`**) via the ASN chain
60
63
  (`AdvanceShippingNoticeItemUnits_TrackingNumbers`), one row per `TrackingNumbers.number`,
61
- **newest first and capped** — see *Draining the candidate set* below.
64
+ **newest first and capped** — see *Draining the candidate set* and *Which clock the window
65
+ runs on* below.
62
66
  2. Per tracking number: skip if the shipment is already finished — checked first against the
63
67
  stored `TrackingNumbers.status` (free), and only then with a live carrier call.
64
68
  3. **`getOrderFulfillmentData`** returns the order's packages (grouped by tracking number,
@@ -73,6 +77,13 @@ and earlier packages). The dynamic HTML is injected into a stored `EmailTemplate
73
77
  `App_Api_Toga2::send(..., 'POST', '/email-templates/sendEmail', $payload, [], true)`.
74
78
  6. On a successful (non-throwing) send, set `c_dtInTransitEmailSent = NOW()`.
75
79
 
80
+ **The two loops behave differently, on purpose.** Loop 1 stamps `c_dtInTransitEmailSent` on any
81
+ row whose `TrackingNumbers.status` is in `TRACKING_FINAL_STATUSES` (`DELIVERED`,
82
+ `RETURNED_TO_SENDER`) and **sends nothing**. Loop 2 emails the rest and stamps after a successful
83
+ send. So a parcel that was already delivered by the time the cron reaches it is **silently
84
+ retired, never announced** — expected behaviour, and the reason a genuine gap can show up as
85
+ "22 rows missed, 1 email actually owed".
86
+
76
87
  ### Draining the candidate set: two-pass, newest-first, bounded
77
88
 
78
89
  **This is the load-bearing part of the cron. Getting it wrong took Compass USA in-transit emails to
@@ -95,9 +106,10 @@ mark the row done"* cron:
95
106
  2. **Newest first, hard cap.** `ORDER BY MAX(TrackingNumbers.dtCreated) DESC` +
96
107
  `LIMIT IN_TRANSIT_EMAIL_BATCH_SIZE` (**400**). Today's shipments are served before any backlog,
97
108
  so a backlog can delay old mail but can never starve new mail.
98
- 3. **Rolling window, not a fixed date.** `IN_TRANSIT_EMAIL_LOOKBACK_DAYS = 14` →
99
- `dtCreated >= DATE_SUB(NOW(), INTERVAL 14 DAY)`. A hardcoded cutoff date guarantees an
100
- ever-growing scan; a rolling window is self-limiting.
109
+ 3. **Rolling window, not a fixed date.** `IN_TRANSIT_EMAIL_LOOKBACK_DAYS` →
110
+ `dtCreated >= DATE_SUB(NOW(), INTERVAL N DAY)`. A hardcoded cutoff date guarantees an
111
+ ever-growing scan; a rolling window is self-limiting. **Which `dtCreated` matters enormously —
112
+ see the next section.** (USA is at 7 days as of 2026-09-15; Canada is still at 14.)
101
113
  4. **Trust the stored status before paying for a carrier call.** `isTrackingStatusFinal(?string)`
102
114
  short-circuits when `TrackingNumbers.status` is already `DELIVERED` or `RETURNED_TO_SENDER`
103
115
  (`TRACKING_FINAL_STATUSES`). worker2's
@@ -105,6 +117,37 @@ mark the row done"* cron:
105
117
  keeps that column current for every client, so this 1.0 cron should not re-ask the carrier about
106
118
  a finished shipment.
107
119
 
120
+ ### Which clock the window runs on: `AdvanceShippingNotices.dtCreated`, never `TrackingNumbers.dtCreated`
121
+
122
+ **This is the single easiest way to break Compass shipping emails, and it has now broken them
123
+ twice.** For Compass a `TrackingNumbers` row is created by the **NetSuite invoice sync**, not by
124
+ the ASN. The ASN shows up days or weeks later and is the only thing that links the tracking number
125
+ to an order and therefore to a recipient. So `TrackingNumbers.dtCreated` is the **invoice** date,
126
+ and a lookback anchored on it measures the wrong thing entirely. Full derivation:
127
+ [where a Compass TrackingNumbers row comes from](../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md).
128
+
129
+ - **Window (this cron):** filter on `AdvanceShippingNotices.dtCreated` — "how long has this been
130
+ *sendable*". Fixed 2026-09-16 at `update_salesorder_status_from_odp.php` line ~432;
131
+ `IN_TRANSIT_EMAIL_LOOKBACK_DAYS` stays at **7**, and the comment above the constant now says why.
132
+ - **Age / alarm (the monitor):** use `GREATEST(TrackingNumbers.dtCreated,
133
+ AdvanceShippingNotices.dtCreated)`.
134
+
135
+ **What went wrong.** On 2026-09-15 11:35 commit `81824325` ("Changing in transit email limit to 7")
136
+ cut the lookback from 14 to 7 days. Because the window was anchored on the invoice date,
137
+ **22 shipments** invoiced 2026-09-03 11:17–11:38 whose ASNs only landed 2026-09-15 16:00–17:00 fell
138
+ outside the 7-day window and were never emailed: SA136625, SA136627, SA136643, SA136673, SA136680,
139
+ SA136694, MR243688, MR244839, MR244893, MR244904, MR245219, MR245303, MR245337, MR245404, MR245409,
140
+ MR245420, MR245424, MR245429, MR245441, MR245454, MR245462, MR245463. Since 2026-08-01, **53**
141
+ tracking rows had their ASN land more than 7 days after the tracking row (max gap **13 days**), so
142
+ the 14-day window had been masking the same design flaw all along.
143
+
144
+ **Verified on prod before shipping:** the ASN-anchored window returns the same 22 rows, drops **0**
145
+ rows the old window covered, and yields exactly **1** real catch-up email — MR245462 (tracking
146
+ `1Z8696XA0392020614`, still `IN_TRANSIT`). The other 21 already read `DELIVERED`, so loop 1 stamps
147
+ them and sends nothing.
148
+
149
+ **Status: edited in the working tree, not yet committed or deployed (2026-09-16).**
150
+
108
151
  ### Resolving the recipient: a `Users` row is NOT required (USA)
109
152
 
110
153
  The To address originally came only from `Users.email`, via `INNER JOIN Users ON Users.contactId =
@@ -227,14 +270,30 @@ the tracking number as emailed (it retries next run).
227
270
  (e.g. `update_salesorder_status_from_gt.php`) with `schedules/cron.worker.sync.json` repointed.**
228
271
  Never put `&` (or any shell metacharacter) in a cron filename. See
229
272
  [tracing a 1.0 worker cron run](../workflows/tracing-a-worker-cron-run-in-production.md).
230
- - **⚠ Invoice tracking numbers are NOT in scope and must never be counted as in-transit
231
- backlog.** `POST /v2/invoice-tracking-numbers` creates `TrackingNumbers` rows bridged through the
232
- **`InvoiceTrackingNumbers`** table — never the ASN chain. On 2026-09-03 alone, 98 were created
233
- (11:15–11:40) and **zero** of them touch `AdvanceShippingNoticeItemUnits_TrackingNumbers`.
234
- Anything that walks the ASN unit bridge (this cron, and the OneUptime queue monitor) correctly
235
- ignores them. Their shape is distinctive: **`shippingCarrierId` set, `shippingMethodId` NULL**.
236
- A naive `TrackingNumbers WHERE c_dtInTransitEmailSent IS NULL` count will read these as a backlog
237
- that never drains.
273
+ - **⚠ CORRECTED 2026-09-16 — invoice tracking numbers ARE in scope; they are the normal origin.**
274
+ This doc previously said `POST /v2/invoice-tracking-numbers` rows are bridged only through
275
+ `InvoiceTrackingNumbers` and "never the ASN chain". **That was wrong.** For Compass the invoice
276
+ sync is what *creates* nearly every `TrackingNumbers` row (98 POSTs / 100 rows in the 2026-09-03
277
+ 11:15–11:40 window, against 4 ASNs created all day); the ASN attaches to those same rows days or
278
+ weeks later. What survives is the narrow version: a tracking number that **never** receives an
279
+ ASN can never be emailed, so a naive `TrackingNumbers WHERE c_dtInTransitEmailSent IS NULL` count
280
+ with no ASN join reads as a backlog that never drains. And because the row is born at invoice
281
+ time, `TrackingNumbers.dtCreated` must never be used as the window clock — see
282
+ [tracking-number origin](../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md).
283
+
284
+ - **⚠ Do NOT make `3b_import_strategic_systems_advance_shipping_notices.php` email for
285
+ pre-existing tracking numbers — tried 2026-09-16 and REVERTED.** 3b gates its in-transit email on
286
+ `$isTrackingNumberInserted` (line ~722 in `processOfficeDepotFile`, line ~1431 in
287
+ `processStrategicSystems`): it only emails when that run INSERTs the tracking row. When the
288
+ invoice sync already created the number, 3b still builds the ASN link but sends nothing, which
289
+ looks like the bug. Changing the gate to "has this customer been told"
290
+ (`c_dtInTransitEmailSent IS NULL`) was implemented and then backed out, because **3b only ever
291
+ sends the plain in-transit template** `e936aa4b-d5b6-41dd-be1a-938b2b27af9e`, while this catch-up
292
+ cron chooses between the **partial** template `d9b3f2a7-1c84-4e6d-9f50-3a7c1e8b2d46` (split order
293
+ / items still being prepared) and the plain one. Firing 3b for pre-existing tracking numbers would
294
+ **silently downgrade every split order from the partial email to the plain email**. 3b was also
295
+ never the real break: the catch-up cron already covers that case inside its window, so fixing the
296
+ window was the correct and far smaller change.
238
297
  - **Clear an in-transit backlog by STAMPING, not by emailing — and refresh statuses first.**
239
298
  Decided 2026-09-03 while draining the outage backlog
240
299
  (`dbchanges2/Client_Compass/2026-09-03 - compass_intransit_email_backlog_backfill.sql`):
@@ -292,6 +351,20 @@ the tracking number as emailed (it retries next run).
292
351
  interceptor and retire this 1.0 cron.
293
352
 
294
353
  ## Change history
354
+ - 2026-09-16 — **Re-anchored the in-transit window on `AdvanceShippingNotices.dtCreated`** (was
355
+ `TrackingNumbers.dtCreated`, `update_salesorder_status_from_odp.php` line ~432);
356
+ `IN_TRANSIT_EMAIL_LOOKBACK_DAYS` stays 7 with a comment explaining why. Cause: commit `81824325`
357
+ (2026-09-15 11:35) cut the lookback 14 → 7 while the window still measured the **invoice** date,
358
+ so 22 shipments invoiced 2026-09-03 whose ASNs landed 2026-09-15 fell out of the window (only
359
+ MR245462 still owed a real email; the other 21 read DELIVERED). Verified on prod: 22 rows in,
360
+ 0 rows lost, 1 catch-up email. **Corrected the old "invoice tracking numbers never touch the ASN
361
+ chain" gotcha** — the invoice sync is in fact the normal origin of Compass `TrackingNumbers` rows;
362
+ new doc
363
+ [tracking-number origin and the ASN link](../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md).
364
+ Recorded the **rejected** alternative of making 3b email for pre-existing tracking numbers (it
365
+ would downgrade split orders from the partial template to the plain one). Not yet committed or
366
+ deployed. (bala)
367
+
295
368
  - 2026-09-10 — Recorded that a zero-line-item ASN is invisible to this cron (candidates come from
296
369
  `AdvanceShippingNoticeItemUnits(_TrackingNumbers)`), the failure mode Compass Canada's G&T
297
370
  importer produced until 2026-09-08. (bala)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-08
9
+ updated: 2026-09-16
10
10
  owners: ["jcardinal", "mhammontree", "bala"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Compass.php
@@ -23,6 +23,7 @@ files:
23
23
  - worker2/Config/sandbox-dev.ini
24
24
  related:
25
25
  - ./oneuptime-incident-clickup-sync.md
26
+ - ../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md
26
27
  - ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
27
28
  - ./monitoring-framework.md
28
29
  - ./all-client-email-queue-monitor.md
@@ -110,7 +111,9 @@ Each measures one Compass/Office Depot integration metric and pushes a decided a
110
111
  `s3://agilant-as2/OfficeDepot/OUTBOX/` (skips subfolders + directory placeholders).
111
112
  - **InTransitEmailQueue** (DB, added 2026-09-04) — watches the Compass USA **in-transit
112
113
  shipping-notification queue**: `TrackingNumbers` rows on the ASN unit bridge with
113
- `c_dtInTransitEmailSent IS NULL` inside a 14-day window (the window the 1.0 sending cron uses).
114
+ `c_dtInTransitEmailSent IS NULL` inside a **7-day window on `AdvanceShippingNotices.dtCreated`**
115
+ (exactly the window the 1.0 sending cron uses — see *Age the backlog on the clock the work
116
+ actually starts on* below).
114
117
  Status-page name *"Shipping Notification Emails (USA, In Transit)"*. See
115
118
  [why it exists](#watch-the-step-before-the-log-row-exists) below.
116
119
  - **MaRefreshOrderExceptions** (DB, added 2026-09-08) — Compass **`MA%` employee-refresh**
@@ -244,6 +247,36 @@ deliberately does not drive the alarm** — otherwise the monitor would sit red
244
247
  cron structurally cannot send. That is the *"backlog monitors must exclude known-permanently-stranded
245
248
  rows"* rule.
246
249
 
250
+ ### Age the backlog on the clock the work actually starts on
251
+
252
+ `InTransitEmailQueue` originally did both of these off `TrackingNumbers.dtCreated`, and both were
253
+ wrong for Compass, because a Compass `TrackingNumbers` row is created by the **NetSuite invoice
254
+ sync** and only becomes sendable when the ASN lands days or weeks later
255
+ ([derivation](../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md)).
256
+ Three corrections (a. committed as `82d614c` and deployed; b. and c. in the working tree, not yet
257
+ deployed as of 2026-09-16):
258
+
259
+ a. **`oldestQueuedHours` / `lateRows` now age from
260
+ `GREATEST(TrackingNumbers.dtCreated, AdvanceShippingNotices.dtCreated)`** — "how long has this
261
+ been *sendable*", not "how long since we invoiced it".
262
+ b. **`$LOOKBACK_DAYS` 14 → 7**, to match the cron's `IN_TRANSIT_EMAIL_LOOKBACK_DAYS`.
263
+ c. **The WHERE lookback filters on `AdvanceShippingNotices.dtCreated`**, so the monitor counts
264
+ exactly the set the cron will work on.
265
+
266
+ **Why it matters — the monitor cried wolf, then muddied a real incident.** Two HIGH alarms fired on
267
+ 2026-09-15 (15:00:46 and 15:35:46) reporting "oldest 90h / 91h" for rows that had been sendable for
268
+ *minutes*. Replaying both moments with the `GREATEST(...)` expression gives **1h** and **0h** with
269
+ **0** late rows, i.e. OK. A third alarm on 2026-09-16 08:55 was a **real** gap, and the 14-vs-7 day
270
+ mismatch made it confusing: the monitor was counting 22 rows the cron would never look at.
271
+
272
+ **Rule: a backlog monitor must age and window on the same clock, and the same column, as the tier
273
+ that does the work.** Any drift between the two turns the monitor into noise in both directions —
274
+ false alarms on rows that are fine, and silence on rows the cron cannot see.
275
+
276
+ Rows with no `Users` row and no `ContactEmailAddresses` fallback (typically EDI-created MR orders)
277
+ can never be emailed at all; the monitor reports those as `unroutableRows` and deliberately never
278
+ alarms on them.
279
+
247
280
  > **⚠ Known divergence (2026-09-04):** the monitor decides routable/unroutable from the
248
281
  > `Users` join **only**, while the sending cron now also falls back to
249
282
  > `Contacts` / `ContactEmailAddresses`. So some rows counted as `unroutableRows` are in fact
@@ -543,6 +576,15 @@ check for another client.
543
576
  monitors must pass the region (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
544
577
 
545
578
  ## Change history
579
+ - 2026-09-16 — **`InTransitEmailQueue` re-anchored onto the sendable clock.** `oldestQueuedHours` /
580
+ `lateRows` now age from `GREATEST(TrackingNumbers.dtCreated, AdvanceShippingNotices.dtCreated)`
581
+ (committed `82d614c`, deployed); `$LOOKBACK_DAYS` 14 → 7 and the WHERE lookback moved onto
582
+ `AdvanceShippingNotices.dtCreated` to match the 1.0 cron exactly (working tree, not deployed).
583
+ Cause: a Compass `TrackingNumbers` row is born from the NetSuite invoice sync, so the old age
584
+ measured invoice age — two false HIGH alarms on 2026-09-15 ("oldest 90h / 91h") replay as 1h / 0h
585
+ with 0 late rows. Added the general rule that a backlog monitor must age and window on the same
586
+ clock and column as the tier that does the work. (bala)
587
+
546
588
  - 2026-09-08 — Added an **11th monitor, `MaRefreshOrderExceptions`** (Compass `MA%`
547
589
  employee-refresh orders with no NetSuite order; `Core.CronJobs` id 74; DEGRADED >10 waiting /
548
590
  >32h, HIGH >20 / >72h) — full derivation in
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 26 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 35 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 36 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
10
10
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
11
11
  - **togadesk** (TOGa Desk) — 13 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -23,6 +23,7 @@
23
23
  | [Persona Model & Levy-Sector Gating (worker2 PEOPLE cron)](features/persona-model-and-levy-gating.md) | 2.0 | Compass USA catalogue visibility is driven by **personas** in `Client_Compass`. |
24
24
  | [Compass Sales-Order Line-Number Renumbering (and why it hangs off the SO hooks)](features/sales-order-line-renumbering.md) | 2.0 | Compass sales-order lines must stay numbered **1..N with no gaps** after any add, edit, or delete — downstream MITS/PO linking reads `lineNumber` as an identity |
25
25
  | [Stranded Approval Reassignment (repointing approvals off dead duplicate Compass Users rows)](features/stranded-approval-reassignment.md) | 2.0 | A Compass employee who leaves and comes back after more than `CONTACT_UNLINK_GRACE_DAYS` (5) is **inserted as a brand-new `Users` row** by the PEOPLE importer i |
26
+ | [Compass: where a `TrackingNumbers` row comes from (invoice sync, NOT the ASN) and how it reaches a customer](features/tracking-number-origin-and-asn-link.md) | 1.0 | For Compass USA a `TrackingNumbers` row is **born from the NetSuite invoice sync**, not from the ASN. |
26
27
  | [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. |
27
28
  | [Compass USA — reversing the 199-item catalog fold (MITS outage recovery)](workflows/catalog-fold-reversal.md) | 2.0 | The **root cause** of the Compass MITS transmission outage (the `assetType`-NULL and duplicate-vendor symptoms) was an ad-hoc cross-client **duplicate-items mer |
28
29
  | [Changing what is IN a Compass kit (adding / swapping a BundleItems line)](workflows/changing-a-kit-line-item.md) | 2.0 | "Add this fee/component to every kit that has X" is a recurring Compass request, and it looks like one `INSERT` into `Client_Compass.BundleItems`. |
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Compass: where a `TrackingNumbers` row comes from (invoice sync, NOT the ASN) and how it reaches a customer
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: compass-usa
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-09-16
10
+ owners: ["bala"]
11
+ files:
12
+ - library/app/api/toga2.php
13
+ - worker/crons/toga2/netsuite/common_sync_togasupply.php
14
+ - worker/crons/toga2/netsuite/sync_togasupply_compass_usa.php
15
+ - worker/crons/toga2/compass/update_salesorder_status_from_odp.php
16
+ - worker2/Worker/Monitor/Compass.php
17
+ related:
18
+ - ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
19
+ - ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
20
+ - ./asn-to-item-fulfillment.md
21
+ - ./contact-email-resolution.md
22
+ - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
23
+ ---
24
+
25
+ ## Summary
26
+
27
+ For Compass USA a `TrackingNumbers` row is **born from the NetSuite invoice sync**, not from the
28
+ ASN. The ASN turns up days or weeks later and is the **only** thing that ties that tracking number
29
+ to a sales order, and therefore to a customer's email address.
30
+
31
+ The consequence, and the reason this doc exists: **`TrackingNumbers.dtCreated` is the invoice
32
+ date.** It is the wrong clock to time any shipping notification, backlog age, or SLA from. Anything
33
+ that asks *"when did we get this tracking number?"* and means *"when could we first have told the
34
+ customer?"* is wrong for Compass by up to two weeks.
35
+
36
+ ## Key files / entry points
37
+
38
+ - `worker/crons/toga2/netsuite/sync_togasupply_compass_usa.php` → `common_sync_togasupply.php` →
39
+ `App_Api_Toga2::syncInvoiceFromNetsuite()` (`library/app/api/toga2.php`, function opens at
40
+ line 3203; the tracking-number POST is at line 3662).
41
+ - The POST is `POST /v2/invoice-tracking-numbers`, payload shaped as a nested object:
42
+ `{"invoice":{"uuid":"…"},"trackingNumber":{"number":"…","shippingCarrier":{…}}}`. api2 creates
43
+ the `TrackingNumbers` row **from that nested object** — so the row exists long before any ASN.
44
+ - `worker/crons/toga2/compass/update_salesorder_status_from_odp.php` — the consumer that must use
45
+ the ASN clock, not this one.
46
+ - `worker2/Worker/Monitor/Compass.php` → `InTransitEmailQueue` — the monitor over the same queue.
47
+
48
+ ## How it works
49
+
50
+ 1. The invoice sync imports a NetSuite invoice and POSTs each of its tracking numbers. That
51
+ **creates** `TrackingNumbers`, stamping `dtCreated` = the moment of the invoice import.
52
+ 2. The invoice does **not** carry the order link. `Invoices.salesOrderId` is NULL on all of the
53
+ affected invoices, and across Compass since 2026-08-01 it is NULL on **1,868 of 2,022**
54
+ invoices. So at this point the tracking number belongs to nobody.
55
+ 3. Later (hours to weeks) the ASN arrives, and **that** is what links the tracking number to a
56
+ customer. The only path is:
57
+
58
+ ```
59
+ TrackingNumbers
60
+ → AdvanceShippingNoticeItemUnits_TrackingNumbers
61
+ → AdvanceShippingNoticeItemUnits
62
+ → AdvanceShippingNoticeItems
63
+ → AdvanceShippingNotices.purchaseOrderId
64
+ → SalesOrders_PurchaseOrders
65
+ → SalesOrders
66
+ → Contacts / Users (the email address)
67
+ ```
68
+
69
+ 4. Only once that chain exists can the in-transit email be built or sent.
70
+
71
+ ### How big the gap is (measured on prod)
72
+
73
+ - **2026-09-03, 11:15:56–11:40:20** — `Logs_Compass.Api` shows **98** `POST /v2/invoice-tracking-numbers`
74
+ from the worker instances. **100** `TrackingNumbers` rows were created in that window, while only
75
+ **4** ASNs were created in the whole of 09-03.
76
+ - Since **2026-08-01**, **53** tracking rows had their ASN land **more than 7 days** after the
77
+ tracking row, with a **maximum gap of 13 days**.
78
+ - The concrete failure: 22 shipments invoiced 2026-09-03 11:17–11:38 whose ASNs only landed
79
+ 2026-09-15 16:00–17:00.
80
+
81
+ ## Data model
82
+
83
+ - `TrackingNumbers.dtCreated` — **invoice import time** for Compass. Not the ship time, not the
84
+ time the customer became notifiable.
85
+ - `AdvanceShippingNotices.dtCreated` — the first moment a tracking number is attached to an order.
86
+ **This is the notifiable-from clock.**
87
+ - `Invoices.salesOrderId` — usually NULL for Compass; do not rely on it to reach the order.
88
+ - `InvoiceTrackingNumbers` — the invoice-side bridge. A row can sit on this bridge *and* later on
89
+ the ASN unit bridge; the two are not mutually exclusive.
90
+
91
+ ## Gotchas / known issues
92
+
93
+ - **⚠ Never window, age, or alarm a Compass shipping feature on `TrackingNumbers.dtCreated`.** Use
94
+ `AdvanceShippingNotices.dtCreated` for a window, and `GREATEST(TrackingNumbers.dtCreated,
95
+ AdvanceShippingNotices.dtCreated)` when you need "how long has this been sendable". Getting this
96
+ wrong produced both a real missed-email gap and two false HIGH alarms — see
97
+ [Compass in-transit emails](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
98
+ - **⚠ Correction to an earlier belief.** It was previously recorded that invoice-created tracking
99
+ numbers "never touch the ASN chain" and are therefore out of scope for in-transit email. That is
100
+ wrong. They are the *normal* origin for Compass tracking numbers; the ASN chain simply attaches
101
+ to them later. What is true is the narrower statement: a tracking number that **never** gets an
102
+ ASN can never be emailed, and a naive `TrackingNumbers WHERE c_dtInTransitEmailSent IS NULL`
103
+ count (with no ASN join) reads those as a backlog that never drains.
104
+ - **A tracking number with no ASN has no customer.** There is no fallback route from invoice to
105
+ contact, because `Invoices.salesOrderId` is NULL. Waiting for the ASN is the design, not a bug.
106
+ - Credentials for the sync ride on `App_Api_Toga2` class constants (e.g.
107
+ `App_Api_Toga2::CLIENT_UUID_COMPASS`) — reference the constant, never the value.
108
+
109
+ ## Change history
110
+ - 2026-09-16 — First write-up. Established that Compass `TrackingNumbers` rows are created by the
111
+ NetSuite invoice sync (`syncInvoiceFromNetsuite` → `POST /v2/invoice-tracking-numbers`), that
112
+ `Invoices.salesOrderId` is NULL on 1,868 of 2,022 Compass invoices since 2026-08-01, and that the
113
+ ASN chain is the only route from a tracking number to a customer. Measured the invoice→ASN gap
114
+ (53 rows over 7 days since 2026-08-01, max 13 days) and corrected the earlier "invoice tracking
115
+ numbers never touch the ASN chain" claim. (bala)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.818",
3
+ "version": "1.0.819",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",