toga-ai 1.0.1012 → 1.0.1013

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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1012",
3
+ "version": "1.0.1013",
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",