toga-ai 1.0.1012 → 1.0.1014

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-16
9
+ updated: 2026-10-13
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/update_salesorder_status_from_odp.php
@@ -342,6 +342,7 @@ the tracking number as emailed (it retries next run).
342
342
  future change to the pendingFulfillment or canceled branches of
343
343
  `_Model_Compass_SalesOrder::_status()` must be mirrored into all three crons by hand.** See
344
344
  [Compass Fulfilled vs Partially Fulfilled](../../../clients/compass-usa/features/order-fulfillment-status-per-line.md).
345
+ - **⚠ The USA delivered cron has NO date window.** `send_delivered_email.php` picks every ASN-unit-linked tracking row with `status <> 'DELIVERED'`, however old. Any bulk ASN load (replay, backfill) emails customers about old orders unless both crons are off and the loaded tracking ids are stamped first. Procedure: [ODP ASN replay backfill](../../../clients/compass-usa/workflows/odp-asn-replay-backfill.md).
345
346
  - **⚠ Conflict with the new 2.0 centralized tracking-status refresh.** The delivered-email cron
346
347
  (`worker/crons/toga2/compass/send_delivered_email.php`) polls UPS itself, sends the delivered
347
348
  template (uuid `fe961c7c-277b-4a89-be1d-3d318d8c7718`) via api2 `GET /email-templates/sendEmail`,
@@ -354,6 +355,9 @@ the tracking number as emailed (it retries next run).
354
355
  tracking-numbers today; the plan (not built) is to move this email logic into a Compass `postPut`
355
356
  interceptor and retire this 1.0 cron.
356
357
 
358
+ ## Change history
359
+ - 2026-10-13 — Recorded that the USA delivered cron has no date window, so bulk ASN loads must suppress first. (bala)
360
+
357
361
  ## Related
358
362
 
359
363
  - `2.0/apps/_underscore/features/email-template-sending.md` — the `sendEmail` scripted API +
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-13
10
10
  owners: [bala, ajean]
11
11
  files:
12
12
  - _underscore/Model/Client/Logs/Record.php
@@ -50,7 +50,7 @@ This is the **model-write** audit log — a different thing from the **request**
50
50
 
51
51
  Scope by **model** and **primary key**:
52
52
 
53
- - `Record.recordId` is the **`Core.Records` id of the model**, not the row. Known ids: **`SalesOrders` = 14**, **`ApprovalDecisions` = 179**. Resolve others from `Core.Records` (`route` / `model` columns), don't guess.
53
+ - `Record.recordId` is the **`Core.Records` id of the model**, not the row. Known ids: **`SalesOrders` = 14**, **`PurchaseOrders` = 17**, **`PurchaseOrderItems` = 18**, **`ApprovalDecisions` = 179**. Known Core `RecordFields`: PurchaseOrderItems quantity = 94, cost = 95. Resolve others from `Core.Records` (`route` / `model` columns), don't guess.
54
54
  - `Record.primaryKeyId` is the id of the row **in that model's table** (FK to many models, meaningless without `recordId`).
55
55
  - Join `RecordField ON RecordField.logRecordId = Record.id` for field-level detail, then translate `recordFieldId` through `Core.RecordFields` to get the field name.
56
56
 
@@ -70,9 +70,13 @@ Order by `Record.timestamp` for a fixed `(recordId, primaryKeyId)` for the full
70
70
  - Caveat still holds: this is the **client-facing** activity log (`Logs_Compass.Record` is ~3.6M rows), so keep staff-actor rows sparse and scoped to real writes. First user: the [Tools Compass admin pages](../../../1.0/apps/tools/features/compass-user-persona-admin.md).
71
71
  - **Tables in the `Logs_<Tenant>` schema are SINGULAR — the documented exception to the plural table convention.** `Logs_Compass` holds `Api`, `CustomRecordField`, `Record`, `RecordField`, `Email`, `EmailAttachment`. Name any new log table there singular.
72
72
  - **⚠ `Logs_*` and `Core` are on different database clusters — you cannot join them in one query.** `Record.recordId` → `Core.Records` and `RecordField.recordFieldId` → `Core.RecordFields` are logical FKs across a cluster boundary. Resolve the `Core` ids in a **separate** query and substitute literals, or the query cannot run.
73
+ - **This log outlives the request log — use it to prove what an api created.** `Logs_<Client>.Api` keeps about 3 months of writes (oldest `Logs_Compass.Api` row was 2026-07-09 on 2026-10-13), `Archive_Compass` is empty, and the shared `Logs.Api` held only ClickUp calls. A model write still has its `Record` row with `apiId` (→ `Client_<X>.Apis`) and the `RecordField` initial values. Example: proving MITS created a short PO, see [MITS PO linking](../../../../clients/compass-usa/features/mits-po-to-so-item-linking.md#gotchas).
73
74
  - **`note` is free text, not an enum.** Milestone strings are written by interceptor code and can be reworded by any change to it. Match with `LIKE` on a stable prefix, never equality with a full sentence; treat absence as weak evidence (the note may predate the code that writes it).
74
75
  - **Do not paste logged values into tickets or the KB.** `oldValue`/`newValue` and note text routinely contain user emails and other personal data. Reference rows by location; quote the mechanism, not the people.
75
76
 
77
+ ## Change history
78
+ - 2026-10-13 — Added PurchaseOrders / PurchaseOrderItems ids and this log as the surviving source once the Api log ages out. (bala)
79
+
76
80
  ## Related
77
81
 
78
82
  - [V2 Request Logging & Where Requests Land](../../api2/features/request-logging.md) — the HTTP request log, the other half of "what happened to this record".
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
21
21
 
22
22
  ## 2.0 framework
23
23
 
24
- - **_underscore** (_Underscore) _(framework core)_ — 94 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
24
+ - **_underscore** (_Underscore) _(framework core)_ — 95 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
25
25
  - **worker2** (Worker) — 86 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
26
26
  - **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
27
27
  - **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -41,6 +41,7 @@
41
41
  | [Giving a Compass API consumer its own scoped key (one role per key)](workflows/granting-api-access.md) | 2.0 | How to build a Compass API key with one dedicated read-only role (the Refresh-team template); open for any "give team X scoped API access" request. |
42
42
  | [Granting a Compass user the same toga25-supply navigation as another user (role vs. SurfaceOverride)](workflows/granting-navigation-access.md) | 2.0 | How to give a Compass user another user's toga25-supply menu items via role-scoped SurfaceOverrides (not ACL); open for "same nav as user Y" and "user sees a bl |
43
43
  | [Granting a Compass user access to a bundle (persona grant) and the SuperUser role](workflows/granting-persona-bundle-access.md) | 2.0 | The five-table procedure to give a Compass user sight of a kit (persona + bundle + items + assortments + user) plus the SuperUser role; open for any "give user |
44
+ | [Backfilling Compass ODP tracking and Fulfilled status by replaying ASNs (historical orders)](workflows/odp-asn-replay-backfill.md) | 2.0 | Runbook to move old Compass ODP orders from Pending Fulfillment to Fulfilled with tracking, by POSTing one ASN per ODP PO without emailing customers; open befor |
44
45
  | [Compass Office Depot Duplicate PO-Line Cleanup (dual-catalog SKU)](workflows/odp-duplicate-po-line-cleanup.md) | 1.0 | Why the NetSuite fulfillment-SO sync duplicated Office Depot PO lines on a dual-catalog SKU, the forward fix, and the one-time cleanup migration; open when inve |
45
46
  | [Recovering Lost Compass ODP EDI 850 Imports (re-drop from Logs.FileLog, move stuck AS2 files, manual SQL import)](workflows/odp-edi-import-recovery.md) | 1.0 | Runbook to recover Compass ODP EDI 850 imports: re-drop an x12 that cron 3a deleted after a failure, move AS2 files stuck in `processed/` when nothing reaches 3 |
46
47
  | [Compass ODP Order Pipeline to NetSuite (numbered worker crons)](workflows/odp-order-pipeline-to-netsuite.md) | 1.0 | The end-to-end Compass ODP order → NetSuite pipeline through the numbered 1.0 worker crons, plus the "this order never reached NetSuite" diagnostic method; open |
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: compass-usa
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-09-16
8
+ updated: 2026-10-13
9
9
  owners: [jcardinal, bala]
10
10
  files:
11
11
  - _underscore/Model/Compass/AdvanceShippingNotice.php
@@ -31,7 +31,7 @@ related:
31
31
  Compass USA ASN posting auto-creates the ItemFulfillment on the upstream SalesOrder and propagates tracking at every granularity; open when working ASN→IF creation, tracking propagation, or ODP duplicate/deadlock ASN issues.
32
32
 
33
33
  ## Summary
34
- For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. `_Model_Compass_AdvanceShippingNotice::postPost` walks PO → SO, then creates the IF (named after the SO number), one IFI per still-unfulfilled ASN item, IFIUs for serialized units, and IF packages for tracking. ASNs arrive two ways: Office Depot **cXML** (direct V2 API) and the worker email cron `3b_import_strategic_systems_advance_shipping_notices.php`.
34
+ For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. `_Model_Compass_AdvanceShippingNotice::postPost` walks PO → SO, then creates the IF (named after the SO number), one IFI per still-unfulfilled ASN item, IFIUs for serialized units, and IF packages for tracking. ASNs arrive two ways: Office Depot **cXML** (direct V2 API) and the worker email cron `3b_import_strategic_systems_advance_shipping_notices.php`. Presidio (vendorId 4) has never sent an ASN for a Compass PO. To backfill old orders by replaying ASNs, follow [ODP ASN replay backfill](../workflows/odp-asn-replay-backfill.md).
35
35
 
36
36
  Tracking numbers propagate across every ASN and IF granularity, so a single ShipNotice tracking number lands at header, item, and unit level on both sides — not just on one unit.
37
37
 
@@ -107,6 +107,8 @@ Something was fixed around May/June 2026. Not fully fixed.
107
107
  **Ruled out: "ODP numbers lines per shipment."** ODP uses real PO line numbers: 45,995 ASN items land on line 1, 12,774 on line 2, 5,772 on line 3, continuing past line 12. On two-line POs it referenced line 2 correctly in 3,447 of 4,797 cases.
108
108
 
109
109
  ## Gotchas
110
+ - **⚠ Supply's order tracking column never reads ASN header or item tracking.** `_Model_Compass_SalesOrder::_trackingNumbers` (~line 319) is a UNION of two paths only: (1) ASN **unit** tracking, `AdvanceShippingNoticeItemUnits_TrackingNumbers` → `PurchaseOrderItems` → `SalesOrderItems_PurchaseOrderItems` → this SO's `SalesOrderItems`; (2) header tracking on the Agilant SO's IF, through the full Compass → ODP → Agilant header chain. An ASN with header + item tracking but no `advanceShippingNoticeItemUnits` (the 3b CSV cron's ODP shape) shows no tracking when the Agilant leg is missing. The cXML ShipNotice shape (units with `advanceShippingNoticeItemUnitTrackingNumbers`) does show. The in-transit and delivered email crons use the same unit join.
111
+ - **The same tracking number twice in one ASN payload fails the whole POST with MySQL 1062** (unique keys on `AdvanceShippingNotices_TrackingNumbers` / `AdvanceShippingNoticeItems_TrackingNumbers`); api2 rolls back the ASN. Dedupe tracking numbers per ASN before posting.
110
112
  - **⚠ OPEN BUG — an already-tracked, fully-fulfilled line makes `postPost` return 500 `EO-1` (2026-08-20).** When an ASN item resolves to a SalesOrderItem whose only `ItemFulfillmentItem` already carries item-level tracking, `remaining = 0` routes to reconcile, `itemFulfillmentItemHasTracking()` (added 2026-06-18) skips that IFI, and the handler then **fails** instead of completing cleanly — caller gets an operation error, not the intended no-op, and the ASN is lost. Deterministic: reproduced three times across both cXML and JSON transports (repro: PO `50305097-1` line 4; Sentry ids `AV-30`, `69-68`). Found on Compass Canada but lives in the shared Compass parent, so Office Depot re-shipments on already-tracked lines hit it too. Recurs whenever a vendor sends a second package for a line that already has tracking. Fix direction: the skip should complete the request successfully (nothing to attach), not fall through to failure.
111
113
  - **The cXML API needs `MATCH_CREATE` overrides on all three ASN tracking bridges.** Compass USA's cXML API (`apiId 4`) has `Client_Compass.Apis_RecordFields` rows overriding RecordFields **1437/1441/1445** to `MATCH_CREATE`. Only 1437 defaults to `MATCH_CREATE` in Core, so without the 1441/1445 overrides the nested `trackingNumber` is looked up instead of created and every ship notice fails EV-12 — exactly what happened to Compass Canada for 67 days. Do not delete or "normalize" these rows. See [cXML ShipNotice Gateway](../../../2.0/apps/api2/features/cxml-shipnotice-gateway.md).
112
114
  - **`findOrCreateShippingCarrier()` was writing other tenants' carriers into `Client_Compass` (fixed 2026-08-20, not yet committed).** The cXML helper authenticated its carrier-create call with hardcoded Compass USA credentials, so Compass **Canada** traffic minted carriers here: 15 `PRECI`, 5 `GANDT` and 3 `XXXX` junk `ShippingCarriers` rows in `Client_Compass` are NOT Compass USA data and should be cleaned up.
@@ -128,5 +130,9 @@ Something was fixed around May/June 2026. Not fully fixed.
128
130
  - **Duplicate tracking from vendor ASN re-transmission (root cause, fixed forward 2026-06-18) — DISTINCT from cross-line contamination above.** Compass vendors (Office Depot, Strategic Systems) re-send the same shipment ASN file for several days; one PO can receive many ASNs (e.g. SA132898 / PO `50305272-1` got 6). The first ASN takes the create path and attaches its tracking; a later ASN re-listing the same, now fully-fulfilled line takes the reconcile path and — pre-fix — attached its *different* tracking number onto the existing IFI. On a qty-1 line this yields two item-level tracking rows (plus a propagated package row). Unlike the cross-line bug, both tracking numbers here map cleanly to the correct line via their own valid ASN items, so the `NOT EXISTS (ASN item with matching VendorItems.itemId)` cleanup guard does not catch it — diagnose instead by an IFI carrying more item-level tracking numbers than its fulfilled quantity. Fix: the reconcile path now skips any IFI that already has item-level tracking (`itemFulfillmentItemHasTracking()`), so only untracked out-of-band IFIs receive ASN tracking. The legitimate out-of-band case (IFI has no tracking, one ASN arrives) is preserved. Forward-only; SA132898 repaired by `2026-06-18a` (delete the later tracking + orphaned package row, keep the original; no backfill needed since the correct number is already present).
129
131
  - **⚠ Contract weakness to record — ASN identifies the PO item positionally.** The ASN payload identifies the PO item **positionally**, by `purchaseOrderItem.lineNumber`, NOT by uuid. So any line-numbering drift — on either side — silently credits the wrong item, with no error. Same positional-identity class as the PO-local `lineNumber` collision fixed in 2026-06, and it is why [Compass USA's per-line fulfillment rule was rejected](order-fulfillment-status-per-line.md): the feed cannot currently support a line-level completeness test.
130
132
 
133
+ ## Change history
134
+ - 2026-10-13 — Recorded that supply's tracking column reads only ASN unit tracking (or the Agilant IF), the 1062 on a repeated tracking number in one payload, and that Presidio sends no ASNs. (bala)
135
+
131
136
  ## Related
132
137
  - [Recursive Item Fulfillments](../../../2.0/apps/_underscore/features/recursive-item-fulfillments.md) (upstream mirroring) — the engine these IFs/packages feed.
138
+ - [ODP ASN replay backfill](../workflows/odp-asn-replay-backfill.md)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-13
10
10
  owners: ["jcardinal", "rgirish", "dfranks", "bala"]
11
11
  files:
12
12
  - _underscore/Model/Compass/PurchaseOrder.php
@@ -110,5 +110,11 @@ USA and Canada share the parent handler unchanged.
110
110
  - **Scope still open:** ~116 PO items across other Compass orders still carry the consecutive off-by-one bridge fingerprint (a `SalesOrderItems_PurchaseOrderItems` PO item linked to two adjacent SO items). Only SA132763 was remediated; a broader reviewed backfill is pending.
111
111
  - **Empty MA orders (fixed 2026-07-13 — forward only).** The 3a cron's new-SO branch created MA Compass sales orders with **no** `salesOrderItems`, because item population lived only on the ODP-facing SO and the back-linking block excludes MA (`if (… == 'SA' || … == 'MR')`). Empty MA (and MR) shells then stick at "Pending Initial Approval" because Compass `_status` gates on approvals first — see [MR/MA Order Auto-Approval & Status Gate](mr-ma-order-approval-and-status.md). **Fix:** build a `salesOrderItems` array from `$purchaseOrder['items']` in the new-SO branch (partNumber + `UUID_ITEM_CATALOG` + qtyOrdered + unitPrice), mirroring the ODP SO item payload. Only the new-SO branch changed; the SO-already-exists branch was intentionally left alone. `php -l` passes. This stops NEW empty MA orders; it does **not** repair the 192 historical empties (which need item backfill from the sibling ODP SO first).
112
112
 
113
+ - **Historical ODP PO lines with a missing or placeholder bridge (found 2026-10-13).** 36 orders / 52 lines from Feb to early Mar 2026 have no `SalesOrderItems_PurchaseOrderItems` row at all, and 10 orders had their PO line linked to **SOI id 1** (order SA100000, a placeholder; it carries 26 links). Any link to SOI id 1 is wrong. Both break ASN→IF creation and supply's tracking column. Repair: link or repoint by `VendorItems.itemId = SalesOrderItems.itemId` AND equal quantity, scoped per order.
114
+ - **MITS can create the ODP PO with a smaller quantity than the SO line.** SA130307 (38 ordered) and SA130906 (10 ordered) each got an ODP PO line with quantity 1, created by MITS (apiId 2), so the order can never read Fulfilled from that PO. Proved from `Logs_Compass.Record` (recordId 18 = PurchaseOrderItems) and its `RecordField` rows (Core RecordFields 94 = quantity, 95 = cost), because the raw `Logs_Compass.Api` payload had aged out. Method: [Record change audit log](../../../2.0/apps/_underscore/features/record-change-audit-log.md).
115
+
116
+ ## Change history
117
+ - 2026-10-13 — Recorded the historical missing / SOI-id-1 bridge gaps and that MITS can create a short ODP PO. (bala)
118
+
113
119
  ## Related
114
120
  - [Compass ASN → ItemFulfillment Auto-Creation](asn-to-item-fulfillment.md)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-10-09
9
+ updated: 2026-10-13
10
10
  owners: ["rgirish", "bala"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/workflow/3b_import_strategic_systems_advance_shipping_notices.php
@@ -85,11 +85,14 @@ Both files parse tracking numbers in two branches each — ODP and Strategic Sys
85
85
 
86
86
  - **⚠ OPEN — wider tracking-number data quality problem (168 bad rows).** Of **73,909** rows in `Client_Compass.TrackingNumbers`, **168** fail `^[A-Za-z0-9]{8,40}$`: 44 scientific notation, 79 with leading spaces, 45 shorter than 8 characters, plus keyboard typos such as `!Z8696XA0140259245` (`!` instead of `1` — a shift-key slip on a UPS number). Max observed length is 114. A CHECK constraint on `TrackingNumbers.number` is the only fix that covers **every** write path including the unknown 06:22 one, but the existing rows must be cleaned first and the upper bound validated against carriers that use longer formats.
87
87
 
88
+ - **The ODP branch's ASN quantity is the PO line quantity, not the CSV quantity.** It loads `PurchaseOrderItems.quantity` (~line 560) and posts that per line, so an ODP ASN quantity is not proof of what ODP shipped. The same query INNER JOINs `SalesOrderItems_PurchaseOrderItems`, so a PO line with no bridge row is left off the ASN entirely.
89
+ - **The ODP branch posts no `advanceShippingNoticeItemUnits`** (tracking on header + last item only; only the Strategic Systems branch builds units). So its tracking never reaches supply's tracking column or the in-transit / delivered email crons, which all join through ASN units. See [ASN → ItemFulfillment](./asn-to-item-fulfillment.md#gotchas).
88
90
  - **Only two files in the whole `crons/toga2/compass/` tree create TrackingNumbers rows**, and they are copy-paste siblings with no shared helper. When you fix one guard, grep the other file for the same block.
89
91
 
90
92
  - The Compass **Canada** G&T importer already rejects scientific-notation tracking as a row-level guard; its read-only inbox inspector reports it as a dry-run category. That is the pattern Compass USA now matches.
91
93
 
92
94
  ## Change history
95
+ - 2026-10-13 — Recorded that the ODP branch posts PO-line quantity and no ASN units, so its tracking is invisible to supply and the email crons. (bala)
93
96
  - 2026-10-09 — Recorded that the feed, not the tracking number, is the carrier source here; the 1.0
94
97
  pattern rewrite does not change what this cron stores. (bala)
95
98
  - 2026-09-22 — Removed the lossy `number_format()` "recovery" from all 4 guard sites in both files; now split-then-guard, appending to `$validationErrors` instead. Fixes the fabricated-tracking-number bug and the multi-value PHP fatal. Written and linted (`php -l` passes), **not deployed**. (rgirish)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-13
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - _underscore/Model/Compass/SalesOrder.php
@@ -113,6 +113,10 @@ Post-fulfillment tail (USA, explicit stage): **473** Pending Billing, **39,869**
113
113
  - **⚠ `self::` defeats the region seam.** `_Model_Compass_SalesOrder` calls **`self::_status()`** (not `static::`) in its ApprovalDecision notification query, so that query always evaluates the parent rule even for Canada orders.
114
114
  - **`salesOrderStageId = 2` is a dormant, USA-impossible branch.** `_status` hardcodes stage 2 as a recompute trigger, but USA's `SalesOrderStages` table contains **only ids 5 to 9** (Pending Billing, Pending Billing/Partially Fulfilled, Billed, Canceled, Closed) — stage 2 does not exist there, so setting it would be an orphan. Canada has all ten. No order in either client currently uses stage 2.
115
115
  - **The same `partNumber` can exist as several `Items` rows, some purchased and some not.** Any such test must be **per line**, never per part number.
116
+ - **No stage means Fulfilled, so you cannot set it.** USA stages 5 to 9 are all post-fulfillment or terminal. To move an order to Fulfilled, give it shipped quantity (an ASN). Bulk procedure: [ODP ASN replay backfill](../workflows/odp-asn-replay-backfill.md).
117
+
118
+ ## Change history
119
+ - 2026-10-13 — Noted that Fulfilled has no stage and is reached only through shipped quantity. (bala)
116
120
 
117
121
  ## Related
118
122
  - [Compass Canada — per-line, ASN-only rule](../../compass-canada/features/order-fulfillment-status-per-line.md) — the region that did change, and why its rule is safe there.
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Backfilling Compass ODP tracking and Fulfilled status by replaying ASNs (historical orders)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: compass-usa
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-10-13
10
+ owners: ["bala"]
11
+ files:
12
+ - _underscore/Model/Compass/SalesOrder.php
13
+ - _underscore/Model/Compass/AdvanceShippingNotice.php
14
+ - worker/crons/toga2/compass/workflow/3b_import_strategic_systems_advance_shipping_notices.php
15
+ - worker/crons/toga2/compass/update_salesorder_status_from_odp.php
16
+ - worker/crons/toga2/compass/send_delivered_email.php
17
+ - worker/schedules/cron.worker.sync.json
18
+ related:
19
+ - ../features/asn-to-item-fulfillment.md
20
+ - ../features/order-fulfillment-status-per-line.md
21
+ - ../features/mits-po-to-so-item-linking.md
22
+ - ../features/odp-strategic-systems-asn-csv-import.md
23
+ - ../features/tracking-number-origin-and-asn-link.md
24
+ - ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
25
+ - order-lifecycle-and-data-integrity.md
26
+ ---
27
+ Runbook to move old Compass ODP orders from Pending Fulfillment to Fulfilled with tracking, by POSTing one ASN per ODP PO without emailing customers; open before any bulk ASN replay or historical tracking backfill.
28
+
29
+ ## Summary
30
+ Compass USA has **no SalesOrderStage that means Fulfilled** (USA stages are only 5 Pending Billing, 6, 7 Billed, 8 Canceled, 9 Closed). Fulfilled is computed by `_Model_Compass_SalesOrder::_fulfillmentStatus()` from shipped quantity: ASN item qty on POs linked through `SalesOrders_PurchaseOrders`, plus shipped TOGa Tech IF qty, must reach the ordered qty (SOIs with `price > 0`, `parentSalesOrderItemId IS NULL`, unlinked or linked to a non-Compass PO, `vendorId <> 26`). Rule detail: [Fulfilled vs Partially Fulfilled](../features/order-fulfillment-status-per-line.md).
31
+
32
+ So the only correct way to mark an ODP order fulfilled is to **create an ASN**. Posting it through api2 also runs `_Model_Compass_AdvanceShippingNotice::postPost`, which creates the ItemFulfillment. First run 2026-10-13: **279 orders** moved Pending Fulfillment to Fulfilled.
33
+
34
+ ## Steps
35
+ 1. **Repair item bridges first.** `postPost` creates an IF, and supply shows tracking, only when each ASN item maps to a SOI through `SalesOrderItems_PurchaseOrderItems`. Sweep the target POs for (a) PO lines with no bridge row and (b) PO lines linked to placeholder SOI id 1. Link or repoint by `VendorItems.itemId = SalesOrderItems.itemId` AND equal quantity. See the [bridge-gap gotcha](../features/mits-po-to-so-item-linking.md#gotchas).
36
+ 2. **Turn off customer emails.** In `worker/schedules/cron.worker.sync.json` set `"active": 0` on "Update Compass SalesOrder Status From ODP CXML" (`update_salesorder_status_from_odp.php`) and "Delievered Emails Compass" (`send_delivered_email.php`). Deploy worker. A replayed ASN looks new to the in-transit cron (window is `AdvanceShippingNotices.dtCreated`, 7 days), and the delivered cron has no date window at all.
37
+ 3. **Build one ASN per ODP PO.** Every PO line at full PO-line quantity, tracking on the header and the last item (same shape as the 3b cron), **plus `advanceShippingNoticeItemUnits` carrying `advanceShippingNoticeItemUnitTrackingNumbers`** (the ODP cXML shape). Without units, supply's tracking column shows nothing. **Dedupe tracking numbers inside each ASN** (a repeat fails the whole POST with 1062).
38
+ 4. **POST `/v2/advance-shipping-notices`** authenticated as `Client_Compass.Apis` id 4 "Office Depot (Cxml)". The secret lives in that table row; read it at run time, never copy it into a script or doc. 2026-10-13 tooling was one-off scripts outside any repo (`build_odp_tracking_asns.py`, `replay_compass_asns.sh`).
39
+ 5. **Check every PO in the DB, not the curl output.** A curl `FAIL 000` (no response) saved nothing. Confirm the ASN row and its IF exist before resending, or you post a duplicate ASN.
40
+ 6. **If ASNs went in without units, add them by SQL.** Insert tracking-only `AdvanceShippingNoticeItemUnits` (`unitId` NULL) and `AdvanceShippingNoticeItemUnits_TrackingNumbers` rows with literal uuid4 values (2026-10-13: 416 units, 647 bridge rows).
41
+ 7. **Retire the loaded tracking numbers.** `UPDATE TrackingNumbers SET c_dtInTransitEmailSent = NOW(), status = 'DELIVERED'` scoped to `TrackingNumbers.id IN (<tracking ids on the loaded ASNs>)` only.
42
+ 8. **Turn both crons back on** (`"active": 1`) and deploy worker.
43
+
44
+ ## Gotchas
45
+ - **⚠ Never scope step 7 by "tracking created today".** That also catches live ODP ASNs that arrived the same day and silences real customer emails. The suppression SQL printed by `replay_compass_asns.sh` is this broad version; do not run it as is.
46
+ - **An ASN quantity is not proof of shipment.** The replay and the 3b cron both post the PO line quantity, not what ODP shipped. A backfilled order is "marked fulfilled", not "verified delivered".
47
+ - **An order can stay Partially Fulfilled after a full replay because MITS created the PO short** (SA130307: 38 ordered, PO qty 1; SA130906: 10 ordered, PO qty 1). Not a replay bug. See [MITS PO linking](../features/mits-po-to-so-item-linking.md#gotchas).
48
+ - **Presidio (vendorId 4) lines cannot be backfilled from vendor data.** Presidio has never sent an ASN for a Compass PO, yet those lines still count as ordered.
49
+ - Step 7 sets `status = 'DELIVERED'` without asking the carrier. Only safe for old shipments; never use it on a recent batch.
50
+
51
+ ## Change history
52
+ - 2026-10-13 — Created from the first ODP historical tracking backfill (279 orders to Fulfilled). (bala)
53
+
54
+ ## Related
55
+ - [ASN → ItemFulfillment](../features/asn-to-item-fulfillment.md)
56
+ - [Fulfilled vs Partially Fulfilled](../features/order-fulfillment-status-per-line.md)
57
+ - [Compass in-transit / delivered emails](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md)
58
+ - [Order lifecycle & data integrity](order-lifecycle-and-data-integrity.md)
@@ -0,0 +1,82 @@
1
+ ---
2
+ type: session
3
+ slug: compass-odp-tracking-backfill
4
+ title: Compass ODP pending-fulfillment tracking backfill
5
+ author: bala
6
+ repos: [worker, _underscore, api2]
7
+ framework: "both"
8
+ client: compass-usa
9
+ status: active
10
+ created: 2026-10-13
11
+ updated: 2026-10-13
12
+ ---
13
+
14
+ # Session: compass-odp-tracking-backfill
15
+ **Date:** 2026-10-13
16
+ **Project/Repo:** worker (1.0 email crons) + _underscore / api2 (2.0 ASN and status logic), data in prod Client_Compass
17
+ **Task:** Load the tracking numbers Office Depot (ODP) returned for the Compass USA pending-fulfillment list (418 SA orders) so the orders show Fulfilled with tracking in supply, without emailing customers, then list what ODP, Presidio and Compass MITS still owe.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+ - Refreshed status for all 418 orders with the exact `_status` SQL from `_underscore/Model/Compass/SalesOrder.php` (7 Oct: 283 Pending, 92 Fulfilled, 25 Partial, 18 Canceled; no change since 1 Sep). File sent to ODP: `Downloads/compass_odp_pending_fulfillment_SA_2026-06-09_updated_2026-10-07.xlsx` with a "Needs Tracking From ODP" column and "Send to ODP" sheet (307 orders).
23
+ - ODP returned the file filled in (`Downloads/...updated_2026-10-07 (1).xlsx`). `c:/WWW/build_odp_tracking_asns.py` built 279 ASN payloads (one per ODP PO, full PO line qty, tracking on header + last item, carrier detected from number format). Review sheet: `c:/WWW/compass_odp_asn_review_2026-10-13.xlsx`.
24
+ - Developer turned off the two email crons in `worker/schedules/cron.worker.sync.json`, then ran `c:/WWW/replay_compass_asns.sh` with `TOGA_PAYLOAD_FILE=compass_odp_asn_payloads_2026-10-13.json TOGA_STATE_FILE=replay_compass_asns_2026-10-13.done`, posting as Client_Compass.Apis id 4 "Office Depot (Cxml)". Evidence: 279 ASNs created 13 Oct 13:47 to 14:08, exactly 1 per order, all 279 Fulfilled.
25
+ - Email suppression scoped to the loaded ASNs only: `c:/WWW/compass_odp_asn_suppress_emails_2026-10-13.sql` (UPDATE 1: 378 rows `c_dtInTransitEmailSent = NOW()`; UPDATE 2: 53 rows `status = 'DELIVERED'`). Verified: 0 of 378 tracking rows would be picked by either cron.
26
+ - Tracking display fix: `c:/WWW/build_odp_asn_units_sql.py` produced `c:/WWW/compass_odp_asn_units_tracking_2026-10-13.sql` (52 PO-to-SO line links for 36 old orders, 416 ASN item units, 647 unit tracking rows). Verified counts match after the developer ran it.
27
+ - Repointed 10 `SalesOrderItems_PurchaseOrderItems` rows that pointed at SalesOrderItems id 1 (SA100000) to each order's own line (SA128095, SA128137, SA128142, SA128152, SA128159, SA128162, SA128164, SA128165, SA128167, SA128479). Verified 0 still on id 1.
28
+ - Final verification on all 418: 371 Fulfilled (92 + 279), 25 Partially Fulfilled, 4 Pending (SA130592, SA128463, SA128176, SA127958), 18 Canceled. All 279 loaded orders return tracking from `_trackingNumbers`.
29
+ - Vendor follow-up file: `Downloads/compass_vendor_followup_2026-10-13.xlsx` (ODP not shipped 46 lines / no tracking 6 / wrong tracking 3; Presidio 12 lines on 4 orders; Compass MITS quantity check 4 lines).
30
+ - Proved MITS created short POs: Logs_Compass.Record recordId 18, Core.RecordFields 94 = Quantity, 95 = Cost. PO 50300524-1 (SA130307, 38 ordered) and PO 50301610-1 (SA130906, 10 ordered) were created by api id 2 "MITS Service Hub" with Quantity 1.
31
+ - /capture pushed 7 KB docs incl. runbook `knowledge/clients/compass-usa/workflows/odp-asn-replay-backfill.md`.
32
+
33
+ ## What did NOT work — DO NOT RETRY THESE
34
+ - First live run: `ERROR: auth did not return a token ... DZ Invalid API key or Secret`. Cause: exported the literal placeholder `<Office Depot api secret>`. Real secret is in Client_Compass.Apis id 4.
35
+ - SA128218 payload: `FAIL 400 ... Error #: 1062 Duplicate entry '110967-82830' for key 'AdvanceShippingNotices_TrackingNumbers...UNIQUE_1'`. Cause: ODP listed `RPSI 461999940435, 461999940435`; same number twice in one payload. api2 rolled back fully. Fixed by deduping (builder now dedupes).
36
+ - SA128507: `FAIL 000 ... no db error code` (curl got no response). Nothing was saved; resent fine. Always check the DB before resending after a 000.
37
+ - The 3b-cron payload shape (no `advanceShippingNoticeItemUnits`) created ASNs with 0 units, so supply showed no tracking for all 279 orders. Do not reuse that shape for a backfill without adding units.
38
+ - The replay script's printed suppression SQL (`dtCreated >= today`) would also silence 2 real live ODP ASNs created the same afternoon (POs 50315263-1, 50315270-1). Do not run it as printed; scope by the loaded ASNs.
39
+ - Writing a loose `.sql` file under c:/WWW via the Write tool is blocked by hook `.claude/hooks/toga/sql-file-location.js`. One-off prod data fixes were given inline instead (not in dbchanges2, because prod-only ids would run in every env).
40
+ - Raw MITS April payloads are gone: Logs_Compass.Api oldest row is 2026-07-09; Logs.Api holds only ClickUp calls; Logs.Webhook had nothing; Archive_Compass is empty.
41
+ - Wide LIKE/SUBSTRING scans on Logs_Compass.Api over the full range time out (300s); query it in one-month dtStamp chunks.
42
+
43
+ ## Not tried yet (candidates for next session)
44
+ - Load corrected tracking once ODP answers for: SA130592 (no tracking), SA128463 ("NOT DELIVERED"), SA128176 ("ARFXF2"), SA130307 and SA130906 (no tracking), SA127958 (In Process).
45
+ - Correct the 3 wrong-length FedEx numbers already loaded: SA128154 51496961434, SA128274 51497033046, SA128297 5123026780169 (would need an UPDATE on TrackingNumbers.number).
46
+ - Load ASNs for the 25 Partially Fulfilled orders once ODP / Presidio send tracking for the remaining lines (list in the vendor follow-up file).
47
+ - Decide SA130307 / SA130906 with Compass: either reduce the SO lines to 1 (then existing ASN makes them Fulfilled) or Compass sends ODP a new PO for 37 and 9.
48
+ - Ask Jeff whether a prod-logs RDS snapshot from before July exists if the raw MITS JSON is required.
49
+ - Optionally create ItemFulfillments for the 46 loaded orders that have none (36 newly linked + 10 repointed); not needed for status or tracking display.
50
+
51
+ ## Current file state
52
+ | File | Status | Notes |
53
+ |------|--------|-------|
54
+ | c:/WWW/build_odp_tracking_asns.py | created | ASN payload builder; dedupes tracking numbers |
55
+ | c:/WWW/compass_odp_asn_payloads_2026-10-13.json | created | 279 payloads, all loaded |
56
+ | c:/WWW/replay_compass_asns_2026-10-13.done | created | state file, 279 accepted |
57
+ | c:/WWW/compass_odp_asn_review_2026-10-13.xlsx | created | carrier/tracking review per order |
58
+ | c:/WWW/compass_odp_asn_suppress_emails_2026-10-13.sql | created, run in prod | scoped email suppression |
59
+ | c:/WWW/build_odp_asn_units_sql.py | created | units + line-link SQL generator; do not re-run after the SQL was applied |
60
+ | c:/WWW/compass_odp_asn_units_tracking_2026-10-13.sql | created, run in prod | 52 links, 416 units, 647 unit tracking rows |
61
+ | worker/schedules/cron.worker.sync.json | back to original | both Compass email crons show "active": 1 again, no local diff; confirm the re-enable is deployed |
62
+ | Downloads/compass_odp_pending_fulfillment_SA_2026-06-09_updated_2026-10-07.xlsx | created | list sent to ODP |
63
+ | Downloads/compass_vendor_followup_2026-10-13.xlsx | created | open items for ODP, Presidio, Compass |
64
+
65
+ ## Decisions made
66
+ - Mark orders Fulfilled only by creating ASNs. Rationale: `_fulfillmentStatus` has no fulfilled stage (stages 5,6,7,8,9 only); setting a stage would show Billed/Closed, not Fulfilled, and would hide the real shipped data. Rejected: manual stage edit.
67
+ - Use full PO line quantity on each ASN (same as the live 3b cron). Rejected: per-line quantities from ODP's sheet, which only had order-level counts.
68
+ - Leave the 25 Partially Fulfilled orders alone. Rationale: ODP sent no new shipped quantity. Rejected: forcing them to Fulfilled.
69
+ - Do not mark SA130307 / SA130906 Fulfilled. Rationale: would claim 37 and 9 units shipped that were never ordered from ODP.
70
+ - Suppress emails by tracking ids on the loaded ASNs only. Rejected: date-based suppression (catches live shipments).
71
+ - Fix line links by matching VendorItems.itemId = SalesOrderItems.itemId and equal quantity, guarded inserts. Rejected: leaving 36 + 10 orders without visible tracking.
72
+
73
+ ## Blockers
74
+ - Waiting on ODP for corrected/missing tracking (6 no-tracking orders, 3 wrong numbers, 46 unshipped lines).
75
+ - Waiting on Presidio: no ASN ever received for POs 50299660-2, 50299729-2, 50302062-2, 50303441-2.
76
+ - Waiting on Compass (MITS) to confirm quantities for SA130307 (38 vs PO 1), SA130906 (10 vs PO 1), SA131879 Presidio lines CW9172I-RTG and MX85-HW---IE (2 vs PO 1).
77
+
78
+ ## Exact next step
79
+ > Open `Downloads/compass_vendor_followup_2026-10-13.xlsx`, check which rows ODP / Presidio / Compass have answered, then build ASN payloads for the answered rows with `c:/WWW/build_odp_tracking_asns.py` (include advanceShippingNoticeItemUnits with tracking this time) after turning the two email crons off.
80
+
81
+ ---
82
+ _Saved by /session-save on 2026-10-13_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1012",
3
+ "version": "1.0.1014",
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",