toga-ai 1.0.564 → 1.0.565

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: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-28
9
+ updated: 2026-08-12
10
10
  owners: [bala]
11
11
  files:
12
12
  - library/app/netsuite.php
@@ -18,6 +18,7 @@ related:
18
18
  - ../../worker/features/netsuite-togasupply-per-client-sync.md
19
19
  - ../../worker/workflows/isfulfillable-multi-client-backfill.md
20
20
  - ../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md
21
+ - ../../../clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md
21
22
  ---
22
23
 
23
24
  ## Summary
@@ -85,9 +86,27 @@ physical goods from services — `itemtype` would, but the sync has never used `
85
86
  `isSerialized`), so this feature intentionally reflects NetSuite's flag **as-is**. **Net effect:
86
87
  nearly everything resolves to `fulfillable = 1`.**
87
88
 
88
- - **Open product decision (caveat):** because services come back fulfillable, the storefront makes
89
- everything clickable. Whether that is intended vs. needing `itemtype` to hide services is
90
- **pending product confirmation**.
89
+ - **Open product decision (RESOLVED for Compass, 2026-08-12):** because services come back
90
+ fulfillable, the storefront makes everything clickable. A prod evidence review found that
91
+ `itemtype` **cannot confirm** fulfillability (physical types like DESKTOPS show 0% tracked) but a
92
+ **tracking ride-along test** cleanly disqualifies fee/software/miscellaneous lines. Compass adopted
93
+ a **type-derived override** of NetSuite's value — see
94
+ [Compass isFulfillable data quality & type rule](../../../clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md).
95
+ Read the write-time-guard gotcha below before implementing it anywhere.
96
+
97
+ ## ⚠ CRITICAL — the existing-item refresh REVERTS any local override
98
+ `getCreateItem`'s refresh (added 2026-07-28) rewrites `isFulfillable` on **existing** items whenever
99
+ NetSuite's value differs from the stored value. NetSuite returns **`T` for service (`NonInvtPart`)
100
+ items**, so **any locally-derived `0` is flipped back to `1` on the next sync run**. A data migration
101
+ alone cannot hold an override — it silently reverts within a day.
102
+
103
+ Proven from the prod audit log: Compass items **2382 / 2384 / 2385** (`US-IOS-SSC-SHR`,
104
+ `US-IOS-VENUENEXT`, `US-IOS-VIRTUALMGR` — itemType `SERVICES`, assetType `CONSULTING`) were stamped
105
+ **NULL → 1 on 2026-08-04 13:56:00-01** by `apiId 1`, `userId NULL`, inside a 1–2 items/second cron
106
+ sweep that started 13:45 and set every item to `1`.
107
+
108
+ **Rule: any policy that overrides NetSuite's flag must be enforced at WRITE TIME** — a
109
+ `prePut`/`prePost` guard on Items in `_Model_Client_Item` — **in addition to** the backfill.
91
110
 
92
111
  ## Verification performed
93
112
  - **Prod read-only dry-run:** 20 reachable Compass items; chain walk correct
@@ -106,6 +125,13 @@ nearly everything resolves to `fulfillable = 1`.**
106
125
  in `Client_Compass.Apis` (name `Agilant`) — never reproduce the secret value.
107
126
 
108
127
  ## Change history
128
+ - 2026-08-12 — Prod investigation (no code change): recorded that the **existing-item refresh
129
+ reverts any local override** (NetSuite returns `T` for services; audit-log proof on Compass items
130
+ 2382/2384/2385, stamped NULL→1 on 2026-08-04), so an override must be enforced by a write-time
131
+ `prePut`/`prePost` guard, not a migration. Closed the long-standing **open product decision** for
132
+ Compass — item type cannot *confirm* fulfillability, but the tracking ride-along test disqualifies
133
+ fee/software/miscellaneous lines; Compass adopted a type-derived override (see the client doc).
134
+ (bala)
109
135
  - 2026-07-28 — Code-review hardening: (1) `getCreateItem` now **refreshes `isFulfillable` on
110
136
  existing items** when NetSuite's value differs (not create-only), mirroring the serialized flag;
111
137
  diff-only PUT keeps re-syncs no-op. (2) The two other `/items` creation paths
@@ -131,3 +157,5 @@ nearly everything resolves to `fulfillable = 1`.**
131
157
  - [Phase 2 — isFulfillable propagation up the SO↔PO chain (2.0)](../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md).
132
158
  - [isFulfillable multi-client backfill (worker)](../../worker/workflows/isfulfillable-multi-client-backfill.md)
133
159
  — the cross-client catch-up cron and its client-DB / catalog-matching gotchas.
160
+ - [Compass isFulfillable — Data Quality & the Type-Derived Rule](../../../clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md)
161
+ — measured prod state, the structural reach limit, and the Compass override + its write-time guard.
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-07-28
9
+ updated: 2026-08-12
10
10
  owners: [bala]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php
@@ -16,6 +16,7 @@ related:
16
16
  - ../../library/features/netsuite-item-isfulfillable-sync.md
17
17
  - ../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md
18
18
  - ../../../clients/compass-usa/profile.md
19
+ - ../../../clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md
19
20
  ---
20
21
 
21
22
  ## Summary
@@ -46,6 +47,20 @@ recursive up-chain propagation** — it never writes the DB directly and never w
46
47
  beta/sandbox endpoint.
47
48
 
48
49
  ## Gotchas / known issues
50
+ - **⚠⚠ Re-running this backfill CANNOT close the remaining NULLs — they are structurally
51
+ unreachable (verified prod, 2026-08-12).** The cron only re-PUTs **Agilant source** items and
52
+ relies on the api2 interceptor to walk up the chain, which requires the client PO to have an
53
+ Agilant-tier SO behind it (a `PurchaseOrderItems_SalesOrderItems` row). **Vendor-direct POs have
54
+ none.** In prod `Client_Compass`, **zero** items with `isFulfillable IS NULL` reach the Agilant
55
+ tier (vs **52,990** line-level reaches for `isFulfillable = 1` items). Per vendor — items ordered /
56
+ reaching the Agilant tier: OFFICE DEPOT **497/124**; STRATEGIC SYSTEMS **66/0**; FREEDOM GROUPS LLC
57
+ **61/0**; PRESIDIO **45/0**; COMPASS GROUP **35/0**; ENCOMPASS SUPPLY CHAIN **2/0**. Only ODP
58
+ partially routes through Agilant NetSuite. **Do not schedule another run expecting these to fill
59
+ in** — closing the gap needs a different data source (see the
60
+ [Compass data-quality doc](../../../clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md)).
61
+ - **⚠ Compass Canada was never covered by this run.** `Client_CompassCanada` is **220 items, 100%
62
+ NULL** as of 2026-08-12 — the storefront Qty Fulfilled cell is dead across the entire Canada
63
+ tenant. Canada is not in the 17-client list above and has never been stamped at all.
49
64
  - **Agilant catalog is matched by NAME, not id.** `Catalogs.name = 'Agilant'` is stable across every
50
65
  client, but the `catalogId` **differs per client** — **Quad = 2, all other clients = 1** (verified
51
66
  across all 17 client DBs). Hardcoding `catalogId = 2` would silently hit only Quad and miss the
@@ -73,6 +88,11 @@ recursive up-chain propagation** — it never writes the DB directly and never w
73
88
  client backfilled from all its Agilant items.
74
89
 
75
90
  ## Change history
91
+ - 2026-08-12 — Prod audit of the post-run state: recorded that the remaining NULLs are
92
+ **structurally unreachable by this cron** (vendor-direct POs have no Agilant-tier SO for the
93
+ interceptor to walk; 0 NULL Compass items reach the Agilant tier, with per-vendor reach counts),
94
+ and that **Compass Canada was never covered** (220 items, 100% NULL). Post-run Compass state:
95
+ 770 NULL / 25 zero / 697 one, with 12,318 order lines fulfilled but flagged NULL. (bala)
76
96
  - 2026-07-28 — Built and ran the multi-client `backfill_isfulfillable_all_clients.php`: direct
77
97
  client-DB scoping of Agilant-catalog items (matched by `Catalogs.name`, not id) keyed by
78
98
  `c_netsuiteInternalItemId`, NetSuite reads by internal id (cached across clients), re-PUT to api2 so
@@ -23,7 +23,7 @@
23
23
  | [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
24
24
  | [FIELD_STORAGE fields — per-row lazy hydration and the platform-wide missing-column 500](features/field-storage-row-hydration.md) | `FIELD_STORAGE` is the 2.0 field type for blob-backed columns (S3 or local folder). | _underscore/Model.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/Invoice.php, dbchanges2/Core/HISTORIC/2024/2024-11b - item-fulfillments.sql |
25
25
  | [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/probe_je_shape.php, test/@dave/fixer.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
26
- | [isFulfillable Propagation Up the SO↔PO Chain](features/fulfillable-item-propagation.md) | `Items.isFulfillable` is a boolean that gates whether a storefront line's **Qty Fulfilled** cell is actionable. | _underscore/Model/Client/Item.php, _underscore/Model/Compass/Item.php, dbchanges2/Core/2026-07-17 - Items isFulfillable RecordField.sql, dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql, dbchanges2/Client/2026-07-17 - ItemsisFulfillable.sql |
26
+ | [isFulfillable Propagation Up the SO↔PO Chain](features/fulfillable-item-propagation.md) | `Items.isFulfillable` is a boolean that gates whether a storefront line's **Qty Fulfilled** cell is actionable. | _underscore/Model/Client/Item.php, _underscore/Model/Compass/Item.php, toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx, dbchanges2/Core/2026-07-17 - Items isFulfillable RecordField.sql, dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql, dbchanges2/Client/2026-07-17 - ItemsisFulfillable.sql |
27
27
  | [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Every ItemFulfillment (IF) now carries an explicit **stage** — picked → packed → shipped — resolved through `ItemFulfillmentStages → ItemFulfillmentStatuses` (m | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderStatus.php, _underscore/Model/Client/SalesOrderItem.php, _underscore/Model/Client/Item.php, _underscore/Model/Client/PurchaseOrderItem.php, library/app/api/toga2.php, dbchanges2/Client/2026-06-30a - BackfillNullStageItemFulfillmentsToShipped.sql, dbchanges2/Client/2026-06-30b - SalesOrderStatusesPickedPacked.sql, dbchanges2/Client/2026-06-30c - ItemFulfillmentStageIdNotNull.sql, dbchanges2/Client_CompassCanada/2026-06-30a - ItemFulfillmentLifecycleAndShippedBackfill.sql |
28
28
  | [DB-free unit testing for _underscore model interceptors](features/model-interceptor-unit-testing.md) | `_underscore` shipped with **no** PHPUnit setup (no `composer.json`/`phpunit`; only vendored PhpOffice tests existed). | _underscore/Test/bootstrap.php, _underscore/Test/Prudential/ServiceRequestTest.php, test/@Bala/tests/netsuite_salesorder_payload_tests.php |
29
29
  | [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php, _underscore/Model.php, _underscore/Model/Rate/Subscription.php |
@@ -6,11 +6,12 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-28
9
+ updated: 2026-08-12
10
10
  owners: [bala]
11
11
  files:
12
12
  - _underscore/Model/Client/Item.php
13
13
  - _underscore/Model/Compass/Item.php
14
+ - toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx
14
15
  - dbchanges2/Core/2026-07-17 - Items isFulfillable RecordField.sql
15
16
  - dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql
16
17
  - dbchanges2/Client/2026-07-17 - ItemsisFulfillable.sql
@@ -18,6 +19,7 @@ related:
18
19
  - recursive-item-fulfillments.md
19
20
  - ../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md
20
21
  - ../../../clients/compass-usa/profile.md
22
+ - ../../../clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md
21
23
  ---
22
24
 
23
25
  ## Summary
@@ -78,6 +80,35 @@ same bridge topology, walked for a different payload (a scalar flag rather than
78
80
  too** — the same integrity class as the Compass off-by-one bridge bugs (see
79
81
  [MITS PO → SO Item Linking](../../../clients/compass-usa/features/mits-po-to-so-item-linking.md)).
80
82
 
83
+ ## Reach limit — vendor-fulfilled items can NEVER be reached (structural, verified 2026-08-12)
84
+ The walk only reaches a client item whose PO has an **Agilant-tier SO behind it** (a
85
+ `PurchaseOrderItems_SalesOrderItems` row). **Vendor-direct POs have none**, so an item that is only
86
+ ever fulfilled by an outside vendor **can never receive `isFulfillable` from this pipeline** — no
87
+ amount of re-running the backfill changes that. It is a **structural gap, not a catch-up gap.**
88
+
89
+ Prod `Client_Compass` (2026-08-12): **zero** items with `isFulfillable IS NULL` reach the Agilant
90
+ tier, versus **52,990** line-level reaches for `isFulfillable = 1` items. Per vendor (Compass items
91
+ ordered / reaching the Agilant tier): OFFICE DEPOT **497/124**; STRATEGIC SYSTEMS **66/0**; FREEDOM
92
+ GROUPS LLC **61/0**; PRESIDIO **45/0**; COMPASS GROUP **35/0**; ENCOMPASS SUPPLY CHAIN **2/0**. Only
93
+ ODP partially routes through Agilant NetSuite. Full analysis + the remediation options:
94
+ [Compass isFulfillable data quality & type rule](../../../clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md).
95
+
96
+ ## Where the flag is consumed (frontend gate)
97
+ `toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx` —
98
+ `isQtyFulfilledClickable` (~lines 21–30) gates the **Qty Fulfilled** cell on:
99
+
100
+ ```
101
+ String(item?.Items?.isFulfillable) === "1" && Number(item?._qtyFulfilled) !== 0
102
+ ```
103
+
104
+ - **`NULL` and `0` behave identically here** — both make the cell dead.
105
+ - The gate also carries a **hardcoded Compass exception**
106
+ (`hostName === COMPASS && AssetTypes.name === 'FEE' → false`) that duplicates the proposed
107
+ type-derived data rule; it should be **deleted once that rule lands in the DB** so the logic lives
108
+ in one place.
109
+ - Because the gate *also* requires `_qtyFulfilled !== 0`, an item that has never been fulfilled is
110
+ unclickable regardless of the flag's value.
111
+
81
112
  ## Data model / schema (deploy)
82
113
  Three `dbchanges2` migrations register the field + hooks; **all must be present in the target
83
114
  env or the flag write is rejected:**
@@ -90,9 +121,16 @@ env or the flag write is rejected:**
90
121
 
91
122
  ## Conflict rule
92
123
  Multi-source kits (an item reachable from more than one source) use **LAST-WRITER-WINS**. A
93
- deterministic `MIN`/`MAX` aggregate was **deliberately deferred** — harmless while values are
124
+ deterministic `MIN`/`MAX` aggregate was **deliberately deferred** on the assumption that values are
94
125
  uniformly `1` (see the Phase-1 gotcha: NetSuite flags nearly everything fulfillable).
95
126
 
127
+ **⚠ That assumption is no longer safe (2026-08-12).** Prod `Client_Compass` holds **25 items with
128
+ `isFulfillable = 0`** — all bundle headers, **14 with real fulfillments** (worst case `B4NY9UC`:
129
+ 11,062 order lines / 2,864 fulfillment rows, its Qty Fulfilled cell disabled). NetSuite marks
130
+ group/kit **headers** `F`, but in TOGa **the header is the line that ships**. Mixed 0/1 values are
131
+ therefore real, and last-writer-wins can land on the wrong one. See the
132
+ [Compass data-quality doc](../../../clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md).
133
+
96
134
  ## Client variations
97
135
  - **Compass** (USA/Canada, DB `Client_Compass`) is the built/verified client. Its `_Model_Compass_Item`
98
136
  overrides `postPost`/`postPut` for a price override, so it **must** `parent::` up to the base or
@@ -123,12 +161,35 @@ uniformly `1` (see the Phase-1 gotcha: NetSuite flags nearly everything fulfilla
123
161
  **resolved/substituted at deploy** — it cannot join across clusters at runtime.
124
162
  - **⚠ Compass subclass shadowing.** If a future client subclass overrides Items `postPost`/`postPut`
125
163
  without calling `parent::`, propagation silently stops for that client.
126
- - **Product decision unresolved (caveat, not a bug).** Because NetSuite's `isfulfillable` is `T`
127
- for services as well as physical goods (see Phase-1 doc), propagation makes **everything**
128
- clickable on the storefront. Whether "reflect NetSuite's flag as-is" is the intended behavior —
129
- vs. using `itemtype` to hide services — is **pending product confirmation**.
164
+ - **⚠ Chain-propagation writes are INVISIBLE to the audit log (debugging gotcha).**
165
+ `propagateFulfillableAcrossChain` issues a **raw `UPDATE Items … WHERE id IN (…)` through `_Query`**
166
+ (`Item.php` ~lines 562–571), bypassing the model layer and therefore the `Logs_<client>` audit
167
+ trail. A **sync PUT is logged; a chain propagation is silent** — so "no log row" does **not** mean
168
+ "never written". Use that as the discriminator between the two write paths: Compass item **2383**
169
+ (Agilant source 2720, NetSuite id 1536664) has **no** log row while 2382/2384/2385 do, which alone
170
+ identifies which path set each value.
171
+ **Query anchor:** prod `Core.RecordFields` **id 2434 = `Items.isFulfillable`** (`recordId 21`); join
172
+ `Logs_<client>.Record` (`recordId = 21`, `primaryKeyId = <itemId>`) to `Logs_<client>.RecordField`
173
+ `ON logRecordId` with `recordFieldId = 2434`. Same technique applies to any
174
+ interceptor-vs-propagation debugging.
175
+ - **Product decision — analyzed 2026-08-12, decision taken for Compass (was "unresolved").** Because
176
+ NetSuite's `isfulfillable` is `T` for services as well as physical goods (see Phase-1 doc),
177
+ propagation makes **everything** clickable. A prod evidence review showed item **type cannot
178
+ confirm** fulfillability (0%-tracked types like DESKTOPS are physical) but the **tracking
179
+ ride-along test can disqualify** fee/software/miscellaneous lines. Compass has since adopted a
180
+ **type-derived override** of NetSuite's flag, which **must be enforced at write time** because the
181
+ Phase-1 daily refresh reverts it. Rule, projected impact, caveats:
182
+ [Compass isFulfillable data quality & type rule](../../../clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md).
130
183
 
131
184
  ## Change history
185
+ - 2026-08-12 — Prod investigation (read-only, no code change): recorded the engine's **structural
186
+ reach limit** (vendor-direct POs have no Agilant-tier SO, so those items can never receive the
187
+ flag — 0 of the NULL Compass items reach the Agilant tier vs 52,990 reaches for `=1` items),
188
+ documented the **toga2-supply frontend gate** that consumes the flag (plus its hardcoded Compass
189
+ `FEE` exception to retire), added the **chain-propagation writes are invisible to the audit log**
190
+ gotcha with the `Core.RecordFields 2434` / `Logs_<client>` query anchor, and retired the
191
+ "values are uniformly 1" assumption behind the deferred conflict rule (25 Compass zeros, 14 with
192
+ real fulfillments). (bala)
132
193
  - 2026-07-28 — Fixed a **read-replica routing** bug: `propagateFulfillableAcrossChain`'s source-item
133
194
  read and the recursive chain-walk CTE now `setisReadHostEnabled(false)` so they run on the writer
134
195
  and see the request's own open write transaction (read-your-writes) — a replica read could return
@@ -151,3 +212,5 @@ uniformly `1` (see the Phase-1 gotcha: NetSuite flags nearly everything fulfilla
151
212
  bridge topology.
152
213
  - [Compass MITS PO → SO Item Linking](../../../clients/compass-usa/features/mits-po-to-so-item-linking.md)
153
214
  — how the SOI↔POI bridges get built (and the integrity bugs that break this walk).
215
+ - [Compass isFulfillable — Data Quality & the Type-Derived Rule](../../../clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md)
216
+ — measured prod state, why the NULLs are unreachable, and the Compass override.
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
18
18
 
19
19
  ## 2.0 framework
20
20
 
21
- - **_underscore** (_Underscore) _(framework core)_ — 54 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
+ - **_underscore** (_Underscore) _(framework core)_ — 55 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 48 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 22 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
24
  - **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -15,12 +15,13 @@ project: _Underscore
15
15
  client: compass-canada
16
16
  type: profile
17
17
  status: active
18
- updated: 2026-08-06
18
+ updated: 2026-08-12
19
19
  owners: [jcardinal, bala, tcox, apeterson]
20
20
  files: []
21
21
  related:
22
22
  - ../compass-usa/profile.md
23
23
  - ../compass-usa/features/approval-decision-flow.md
24
+ - ../compass-usa/features/isfulfillable-data-quality-and-type-rule.md
24
25
  - features/french-order-email-localization.md
25
26
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
26
27
  - ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
@@ -104,6 +105,17 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
104
105
  impact: 34 personas, 7,017 users with personas, 1,461 with more than one. Fix `5313a9b1` exists on
105
106
  `TRUE-80672`/`_beta` but is **not in `_production`**. See
106
107
  [Client Fields → Login-settings branches](../../2.0/apps/toga2-commerce/features/client-fields.md).
108
+ - **⚠ `Items.isFulfillable` is 100% NULL in Canada — the Qty Fulfilled cell is dead storefront-wide.**
109
+ All **220** `Client_CompassCanada` items are NULL (prod, 2026-08-12): Canada was **never** included
110
+ in the multi-client backfill and has never been stamped at all. Note also that Canada uses a
111
+ **different type vocabulary** than Compass USA — title-case `assetTypes` (`Equipment`, `Hardware`,
112
+ `Services`, `Fee`, `Consulting`) and its own `itemTypes` (`LAPTOP`, `MACBOOK`, `MONITORS`,
113
+ `CABLES`, `SERVICE`, `Services`, `CONSULTING`, `SOFTWARE LICENSING`), with **no `FEE` or
114
+ `SERVICEFEES` itemType at all**. The US-derived rule list still matches, because the columns are
115
+ `utf8mb4_0900_ai_ci` (case-insensitive) — **do not split it into two hand-maintained lists**, but
116
+ do re-confirm the Canada vocabulary separately. Projected impact of the proposed rule here: 13 → 0,
117
+ 207 → 1. See
118
+ [isFulfillable — Data Quality & the Type-Derived Rule](../compass-usa/features/isfulfillable-data-quality-and-type-rule.md).
107
119
  - The 2026-06-08 ASN → ItemFulfillment work was for **Compass USA**, not Compass Canada.
108
120
  - **Order status is shipped-only**, same as Compass USA (shared `_Model_Compass_SalesOrder`).
109
121
  Compass Canada's own IF lifecycle stages (picked/packed/shipped) + shipped backfill are seeded by
@@ -5,6 +5,7 @@
5
5
  | [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**, VIP auto-approve, and EN/FR localization — lives **entirely i | _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Usa/ApprovalDecision.php, _underscore/Model/Compass/Canada/ApprovalDecision.php |
6
6
  | [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
7
7
  | [Cost Centers — Unit Locations, numeric-only policy](features/cost-centers.md) | 2.0 | A Compass "cost center" — the value a user picks in commerce and that lands on an order — is **not** a `CostCenters` row. | toga2-commerce/src/pages/Cart/api/CartApi.ts, worker1.5/crons/toga2/compass/import_locations.php, _underscore/Model/Compass/SalesOrder.php, api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-06 - RemoveNonNumericCostCenters.sql |
8
+ | [Compass isFulfillable — Data Quality & the Type-Derived Rule](features/isfulfillable-data-quality-and-type-rule.md) | 2.0 | A **prod-data investigation (2026-08-12, read-only)** into why so many Compass storefront lines still have a dead **Qty Fulfilled** cell. | _underscore/Model/Client/Item.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php, toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx |
8
9
  | [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql, dbchanges2/Client/2026-07-15a - ExcludeFeeItemsFromItemFulfillmentsForSalesOrdersView.sql |
9
10
  | [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
10
11
  | [Compass MITS PO Transmission to Vendors](features/mits-po-transmission-to-vendors.md) | 2.0 | The 1.0 worker cron `2_transmit_mits_purchase_orders_to_vendors.php` transmits Compass PurchaseOrders to their vendors (Office Depot, Strategic Systems, Compass | worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compasscanada/workflow/2_transmit_mits_purchase_orders_to_vendors.php, library/app/client/compass.php |
@@ -0,0 +1,187 @@
1
+ ---
2
+ title: Compass isFulfillable — Data Quality & the Type-Derived Rule
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: compass-usa
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-08-12
10
+ owners: ["bala"]
11
+ files:
12
+ - _underscore/Model/Client/Item.php
13
+ - library/app/api/toga2.php
14
+ - worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php
15
+ - toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx
16
+ related:
17
+ - ../profile.md
18
+ - ../../compass-canada/profile.md
19
+ - ../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md
20
+ - ../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md
21
+ - ../../../1.0/apps/worker/workflows/isfulfillable-multi-client-backfill.md
22
+ ---
23
+
24
+ ## Summary
25
+ A **prod-data investigation (2026-08-12, read-only)** into why so many Compass storefront lines still
26
+ have a dead **Qty Fulfilled** cell. Conclusion in one line: the remaining `Items.isFulfillable`
27
+ NULLs are **not a backfill backlog — they are structurally unreachable**, and the values that *are*
28
+ set are partly wrong. This doc records the measured state of `Client_Compass` /
29
+ `Client_CompassCanada`, the evidence for (and against) deriving the flag from item type, the
30
+ **decision** to override NetSuite with a type-derived rule for Compass, and the **write-time guard**
31
+ that decision requires in order to survive the daily NetSuite sync.
32
+
33
+ No source files were changed and **no migration has been written yet**. All figures below were read
34
+ from **production** on **2026-08-12**.
35
+
36
+ ## Measured state (prod, 2026-08-12)
37
+ - **`Client_Compass`, catalog 1:** **770 NULL / 25 zero / 697 one.**
38
+ - **12,318 Compass order lines have real `ItemFulfillmentItems` but a NULL flag** — the Qty
39
+ Fulfilled cell is dead on all of them. A further **4,873** lines sit behind `isFulfillable = 0`.
40
+ - **`Client_CompassCanada`: 220 items, 100% NULL.** Canada has **never** been stamped at all, so the
41
+ Qty Fulfilled cell is dead across the **entire** Canada storefront.
42
+
43
+ ## Why the NULLs cannot be backfilled (structural, not a catch-up gap)
44
+ The flag is born on an **Agilant-catalog source item** during the 1.0 NetSuite item sync
45
+ ([Phase 1](../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md)) and then
46
+ propagates **up** the `SalesOrderItems`↔`PurchaseOrderItems` chain
47
+ ([Phase 2](../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md)). That walk needs
48
+ the Compass PO to have an **Agilant-tier SO behind it** (a `PurchaseOrderItems_SalesOrderItems`
49
+ row). **Vendor-direct POs have none**, so a vendor-fulfilled item can never receive the flag from
50
+ the current pipeline.
51
+
52
+ Verified in prod `Client_Compass`: **zero** items with `isFulfillable IS NULL` reach the Agilant
53
+ tier, against **52,990** line-level reaches for `isFulfillable = 1` items.
54
+
55
+ Compass items ordered / reaching the Agilant tier, per vendor:
56
+
57
+ | Vendor | Ordered | Reaching Agilant tier |
58
+ |---|---|---|
59
+ | OFFICE DEPOT | 497 | 124 |
60
+ | STRATEGIC SYSTEMS | 66 | 0 |
61
+ | FREEDOM GROUPS LLC | 61 | 0 |
62
+ | PRESIDIO | 45 | 0 |
63
+ | COMPASS GROUP | 35 | 0 |
64
+ | ENCOMPASS SUPPLY CHAIN | 2 | 0 |
65
+
66
+ **Consequence: re-running the multi-client backfill cannot move a single one of these.** Only ODP
67
+ partially routes through Agilant NetSuite.
68
+
69
+ ## The 25 `isFulfillable = 0` Compass items are WRONG — they should be 1
70
+ All 25 are **bundle items**; **14 of them have real fulfillments**. Worst case **`B4NY9UC`**:
71
+ **11,062 order lines and 2,864 fulfillment rows** — the most-shipped laptop in the catalog, with its
72
+ Qty Fulfilled cell **disabled**. NetSuite marks group/kit **headers** `F`, but in TOGa **the header
73
+ IS the line that ships**. This supersedes the earlier assumption (in the Phase 1/Phase 2 docs) that
74
+ the zeros were harmless because values were "uniformly 1".
75
+
76
+ ## Can item type reproduce NetSuite's flag? (evidence)
77
+ **The naive test fails.** "Does this type ever fulfil?" does not separate anything:
78
+ `SERVICES` 119/120 known-fulfillable, `SOFTWARE` 7/7, `FEE` 3/3, while `HARDWARE` fulfils on only
79
+ **26.9%** of lines. `inventoryType = HYBRID` is the **column default** = noise.
80
+ `EntitlementFulfillmentTypes` / `EntitlementFulfillmentMethods` are **EMPTY** in prod.
81
+
82
+ **The decisive test is the tracking "ride-along":** does a fulfillment row carry **its own** tracking
83
+ number, or does it share one with another line on the same shipment?
84
+
85
+ | Type | Fulfillment rows | With tracking | Ride-along (shares tracking) |
86
+ |---|---|---|---|
87
+ | itemType `FEE` | 9,639 | 1 (0.0%) | — (0 units) |
88
+ | assetType `FEE` | 10,071 | 22 | **22/22 = 100%** |
89
+ | itemType `SOFTWARE` | — | 129 | **129/129 = 100%** |
90
+ | assetType `MISCELLANEOUS` | — | 385 | **385/385 = 100%** |
91
+ | itemType `SERVICES` | — | 687 | 274 = **39.9%** (413 rows carry their OWN tracking) |
92
+
93
+ A 100% ride-along rate means the type **never has its own shipment** — a fee/licence line billed
94
+ alongside hardware. `SERVICES` fails that test: 413 service rows genuinely ship on their own.
95
+
96
+ **⚠ Absence of tracking does NOT prove non-fulfillable.** `DESKTOPS`, `CANTEEN TECHNOLOGY` and
97
+ `VENDING AND OCS ORDERING` all show **0% tracked** yet are physical goods. **Type can only
98
+ DISQUALIFY an item, never confirm one.**
99
+
100
+ ## Decision — the type-derived rule for Compass (2026-08-12)
101
+ Developer's call; it **overrides NetSuite's value**:
102
+
103
+ ```
104
+ isFulfillable = 0 WHERE itemType OR assetType IN
105
+ ('FEE','SERVICEFEES','SERVICES','SERVICE','CONSULTING')
106
+ isFulfillable = 1 otherwise
107
+ ```
108
+
109
+ Projected impact:
110
+ - **`Client_Compass`:** **186 → 0** (54 currently NULL + 131 currently **1** + 1 currently 0) and
111
+ **606 → 1**.
112
+ - **`Client_CompassCanada`:** **13 → 0**, **207 → 1**.
113
+
114
+ **⚠ Recorded caveat — the SERVICES/CONSULTING half is NOT supported by the evidence.** 413
115
+ `SERVICES` fulfillment rows carry their own tracking, and the rule flips **131 currently-clickable
116
+ items off**. That half **needs product sign-off**. `SOFTWARE` (100% ride-along) is the *stronger*
117
+ candidate on the evidence but was deliberately **left out of scope**.
118
+
119
+ ## ⚠ CRITICAL — a backfill alone will be undone by the daily NetSuite sync
120
+ Phase 1's `getCreateItem` **refreshes `isFulfillable` on EXISTING items** whenever NetSuite's value
121
+ differs from the stored value. NetSuite returns **`T` for service (`NonInvtPart`) items**, so any
122
+ type-derived **0** is **flipped back to 1 on the next sync run**.
123
+
124
+ Proven from the audit log: Compass items **2382 / 2384 / 2385**
125
+ (`US-IOS-SSC-SHR`, `US-IOS-VENUENEXT`, `US-IOS-VIRTUALMGR` — itemType `SERVICES`, assetType
126
+ `CONSULTING`) were stamped **NULL → 1 on 2026-08-04 13:56:00-01** by `apiId 1`, `userId NULL`,
127
+ inside a **1–2 items/second cron sweep** that began 13:45 and set every item to 1.
128
+
129
+ **Therefore the rule MUST be enforced at write time** — a `prePut`/`prePost` guard on Items in
130
+ `_Model_Client_Item` — **in addition to** the backfill migration. A migration on its own silently
131
+ reverts within a day.
132
+
133
+ ## Evidence-based alternative signals (if type is not wanted)
134
+ - **`ItemFulfillmentItems`** (via `SalesOrderItems.salesOrderItemId`) = **proven shipped**.
135
+ - **`VendorItems`** (`isActive = 1`) = **procurable from a vendor**.
136
+
137
+ Tiering the 770 NULL Compass items: **184** have fulfillment history · **286** have an active
138
+ `VendorItems` row · **300** have neither. The frontend gate already requires
139
+ `_qtyFulfilled !== 0`, so **tier-3 items cannot be clicked regardless of the flag**, and can be left
140
+ NULL.
141
+
142
+ ## Where the storefront consumes the flag (+ a hardcode to retire)
143
+ `toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx`,
144
+ `isQtyFulfilledClickable` (~lines 21–30):
145
+
146
+ ```
147
+ String(item?.Items?.isFulfillable) === "1" && Number(item?._qtyFulfilled) !== 0
148
+ ```
149
+
150
+ plus an extra **hardcoded** `hostName === COMPASS && AssetTypes.name === 'FEE' → false`.
151
+
152
+ - **NULL and 0 behave identically** at this gate.
153
+ - That **FEE hardcode duplicates the proposed data rule** — once the rule lands in the DB it should
154
+ be **deleted** so the logic lives in one place.
155
+
156
+ ## Compass Canada uses a DIFFERENT type vocabulary
157
+ Canada's `assetTypes` are **title case** (`Equipment`, `Hardware`, `Services`, `Fee`, `Consulting`)
158
+ and its `itemTypes` are a **separate vocabulary** (`LAPTOP`, `MACBOOK`, `MONITORS`, `CABLES`,
159
+ `SERVICE`, `Services`, `CONSULTING`, `SOFTWARE LICENSING`) — it has **no `FEE` or `SERVICEFEES`
160
+ itemType at all**.
161
+
162
+ Because these tables are `utf8mb4_0900_ai_ci` (**case-insensitive**), the single US-derived
163
+ `IN ('FEE','SERVICEFEES',…)` list **matches Canada's title-case values** — verified against live
164
+ data. **Do not hand-maintain two string lists**, but **do** re-confirm the Canada vocabulary
165
+ separately, since the names genuinely differ and a US list *looks* wrong for Canada.
166
+
167
+ ## Change history
168
+ - 2026-08-12 — Prod read-only investigation of `Items.isFulfillable` across `Client_Compass` /
169
+ `Client_CompassCanada`. Established that remaining NULLs are **structurally unreachable**
170
+ (vendor-direct POs have no Agilant-tier SO, so the chain walk has nothing to walk — 0 NULL items
171
+ reach the Agilant tier vs 52,990 reaches for `=1` items), that the **25 zeros are wrong** (all
172
+ bundle headers, 14 with real fulfillments, incl. `B4NY9UC` at 11,062 lines / 2,864 fulfillments),
173
+ and that **Canada is 100% NULL**. Proved item type cannot *confirm* fulfillability but the
174
+ **tracking ride-along test** cleanly disqualifies fee/software/miscellaneous lines (100%
175
+ ride-along) while `SERVICES` fails it (39.9%). **Decided** the type-derived rule
176
+ (`FEE/SERVICEFEES/SERVICES/SERVICE/CONSULTING → 0`, else 1) with the SERVICES/CONSULTING half
177
+ flagged for product sign-off, and established that it **must** be enforced by a `prePut`/`prePost`
178
+ guard because the daily sync's existing-item refresh reverts it (audit-log proof: items
179
+ 2382/2384/2385 stamped NULL→1 on 2026-08-04). (bala)
180
+
181
+ ## Related docs
182
+ - [Phase 1 — isFulfillable from NetSuite during item sync (1.0)](../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md)
183
+ — where the value is born and the refresh that reverts overrides.
184
+ - [Phase 2 — isFulfillable propagation up the SO↔PO chain (2.0)](../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md)
185
+ — the chain walk whose reach is the structural limit described here.
186
+ - [isFulfillable multi-client backfill (worker)](../../../1.0/apps/worker/workflows/isfulfillable-multi-client-backfill.md)
187
+ — the cron that **cannot** close this gap.
@@ -17,7 +17,7 @@ project: _Underscore
17
17
  client: compass-usa
18
18
  type: profile
19
19
  status: active
20
- updated: 2026-08-11
20
+ updated: 2026-08-12
21
21
  owners: [jcardinal, bala, tcox, apeterson, dfranks]
22
22
  files: []
23
23
  related:
@@ -27,6 +27,7 @@ related:
27
27
  - features/mits-sales-order-transmission-alerting.md
28
28
  - features/asn-to-item-fulfillment.md
29
29
  - features/cost-centers.md
30
+ - features/isfulfillable-data-quality-and-type-rule.md
30
31
  - features/approval-decision-flow.md
31
32
  - workflows/cross-kit-bundle-corruption.md
32
33
  - workflows/odp-duplicate-po-line-cleanup.md
@@ -134,4 +135,12 @@ separate, related client (see its own profile).
134
135
  up the SO↔PO chain to the client-facing Compass item by an api2 interceptor
135
136
  ([Phase 2](../../2.0/apps/_underscore/features/fulfillable-item-propagation.md)). Verified on prod
136
137
  Compass chains; a one-time July-5+ backfill re-PUTs source items through the same path.
138
+ **⚠ Prod audit 2026-08-12 — the flag is largely broken for Compass and NO backfill can fix it.**
139
+ 770 NULL / 25 zero / 697 one; **12,318** order lines are really fulfilled but flagged NULL, so
140
+ their Qty Fulfilled cell is dead. The NULLs are **structurally unreachable** (vendor-direct POs
141
+ have no Agilant-tier SO to walk up from), and the 25 zeros are **wrong** (bundle headers that do
142
+ ship — incl. `B4NY9UC`, 11,062 lines). A **type-derived override** was decided and **must** be
143
+ enforced by a write-time guard, because the daily NetSuite sync reverts it. Evidence, rule,
144
+ projected impact and caveats:
145
+ [isFulfillable — Data Quality & the Type-Derived Rule](features/isfulfillable-data-quality-and-type-rule.md).
137
146
  - This profile is a starting point; expand as more Compass-specific behavior is captured.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.564",
3
+ "version": "1.0.565",
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",