toga-ai 1.0.669 → 1.0.670
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.
- package/knowledge/2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md +26 -9
- package/knowledge/clients/compass-canada/features/order-fulfillment-status-per-line.md +153 -55
- package/knowledge/clients/compass-canada/profile.md +15 -9
- package/knowledge/clients/compass-usa/INDEX.md +2 -2
- package/knowledge/clients/compass-usa/features/asn-to-item-fulfillment.md +82 -1
- package/knowledge/clients/compass-usa/features/isfulfillable-data-quality-and-type-rule.md +102 -3
- package/knowledge/clients/compass-usa/features/mr-ma-order-approval-and-status.md +95 -5
- package/knowledge/clients/compass-usa/features/order-fulfillment-status-per-line.md +176 -104
- package/knowledge/clients/compass-usa/profile.md +15 -8
- package/package.json +1 -1
package/knowledge/2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
CHANGED
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: [bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/SalesOrder.php
|
|
@@ -47,8 +47,11 @@ This makes the *picked/packed* portion of the fulfillment lifecycle visible (pre
|
|
|
47
47
|
- **Compass USA + Compass Canada:** all IF stages are *imported*, but order status counts
|
|
48
48
|
**shipped only** — picked/packed never advance a Compass order's status. Compass walks
|
|
49
49
|
Pending → Partially Fulfilled → Fulfilled by shipped quantity alone. **The shipped-only *stage*
|
|
50
|
-
policy is shared, but
|
|
51
|
-
the fulfillment branch lives in `_fulfillmentStatus()
|
|
50
|
+
policy is shared, but the fulfilled-vs-partiallyFulfilled *rule* is now per region.** **Only Canada
|
|
51
|
+
actually changed:** the fulfillment branch lives in `_fulfillmentStatus()`, Canada overrides it with
|
|
52
|
+
a pure per-line rule, and **Compass USA still runs the original order-total logic on purpose**
|
|
53
|
+
(its ASN feed is not trustworthy at line level). The seam was reverted in git on 2026-08-26 and
|
|
54
|
+
rebuilt uncommitted. See
|
|
52
55
|
[Compass USA](../../../clients/compass-usa/features/order-fulfillment-status-per-line.md) and
|
|
53
56
|
[Compass Canada](../../../clients/compass-canada/features/order-fulfillment-status-per-line.md).
|
|
54
57
|
|
|
@@ -67,7 +70,8 @@ shipped-only for ALL clients** — picked/packed quantities never count as inven
|
|
|
67
70
|
`COUNT` and the shipped-qty `SUM` subqueries `INNER JOIN ItemFulfillmentStages →
|
|
68
71
|
ItemFulfillmentStatuses` filtered to the shipped status slug. Shared by Compass USA + Canada.
|
|
69
72
|
Its **fulfillment half is a separate `protected static _fulfillmentStatus()`** called via
|
|
70
|
-
`static::` — that is the per-region seam (see the Compass docs above)
|
|
73
|
+
`static::` — that is the per-region seam (see the Compass docs above), and its **body is the original
|
|
74
|
+
order-total logic that USA still runs**. The approval gate stays in
|
|
71
75
|
`_status()` and is region-independent.
|
|
72
76
|
- **`Model/Compass/Canada/SalesOrder.php`** — Canada's `_fulfillmentStatus()` override (per-line,
|
|
73
77
|
ASN-bridge only).
|
|
@@ -177,11 +181,15 @@ Fan-out (`Client/`, every client) + a Compass-Canada-specific set. Execution is
|
|
|
177
181
|
the Compass order-lifecycle workflow.
|
|
178
182
|
- **⚠ An order-level "ordered total vs shipped total" comparison is not a fulfillment test.** A
|
|
179
183
|
grand total lets one line's surplus cancel another line's gap, so an order with one line short and
|
|
180
|
-
another over-shipped reads **Fulfilled**. This is
|
|
181
|
-
|
|
182
|
-
`parentSalesOrderItemId IS NULL`, the shipped side
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
another over-shipped reads **Fulfilled**. This is what the Compass `_status` does for **USA
|
|
185
|
+
today** — and its two sides do not even count the same lines (the ordered side filters
|
|
186
|
+
`parentSalesOrderItemId IS NULL`, the shipped side does not). Measured on prod: **90** Canada orders
|
|
187
|
+
were mislabeled and have been corrected by a per-line rule, and **5,223** USA orders would move —
|
|
188
|
+
but the USA change was **deliberately not made**, because duplicate Office Depot ASNs make its
|
|
189
|
+
per-line shipped quantity unreliable. Any client `_status` that judges completeness must compare
|
|
190
|
+
**per line**, and must not do so on a feed that double-credits lines. **⚠ Earlier figures of 5,234
|
|
191
|
+
USA / 115 Canada are void** (the first attempt was reverted in git). See the two Compass docs
|
|
192
|
+
linked above.
|
|
185
193
|
- **A `self::`-qualified call to a calculated field inside the same model always uses the parent's
|
|
186
194
|
logic.** `_Model_Compass_SalesOrder` calls `self::_status()` in its ApprovalDecision notification
|
|
187
195
|
query, so that one call site never sees a region subclass override. Use `static::` unless you
|
|
@@ -191,6 +199,15 @@ Fan-out (`Client/`, every client) + a Compass-Canada-specific set. Execution is
|
|
|
191
199
|
picked/packed constant would let the literal be removed.
|
|
192
200
|
|
|
193
201
|
## Change history
|
|
202
|
+
- 2026-08-27 — **Corrected the 2026-08-26 entry below: that work was reverted in git (`90c964f1`
|
|
203
|
+
reverted by `43049d1d`) and the "5,234 USA / 115 Canada" figures are void.** Rebuilt state: the
|
|
204
|
+
`_fulfillmentStatus()` per-region seam exists again but is **uncommitted**, `_fulfillmentStatus()`'s
|
|
205
|
+
body is the **original order-total logic that Compass USA still runs deliberately**, and **only
|
|
206
|
+
Compass Canada overrides it** — with a pure per-line rule, no order-total condition, correcting **90**
|
|
207
|
+
prod orders (not 115). A USA per-line rule was measured (5,223 orders would move) and rejected
|
|
208
|
+
because duplicate Office Depot ASNs make USA's per-line shipped quantity unreliable. The
|
|
209
|
+
order-total-is-not-a-fulfillment-test gotcha now records that a per-line test also needs a
|
|
210
|
+
trustworthy line-level feed. (bala)
|
|
194
211
|
- 2026-08-26 — Recorded that the Compass **fulfilled-vs-partiallyFulfilled** rule is now **per
|
|
195
212
|
region**: the fulfillment branch of `_Model_Compass_SalesOrder::_status()` was extracted into a
|
|
196
213
|
`protected static _fulfillmentStatus()` invoked via `static::` (approval gate untouched, emitted
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: compass-canada
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/Canada/SalesOrder.php
|
|
@@ -25,43 +25,62 @@ TOGa Supply showed **Fulfilled** on Compass Canada orders that were only partly
|
|
|
25
25
|
`SAC100664` (`SalesOrders.id 665`): line 1 the ZBOOK `C40QYUC#ABA` ordered 1 / shipped 0, line 2 the
|
|
26
26
|
mouse `9VA80AA#ABA` ordered 1 / shipped 1, and the badge read **Fulfilled**.
|
|
27
27
|
|
|
28
|
-
Root cause
|
|
29
|
-
**order-level grand totals** whose two sides did not count the
|
|
30
|
-
`SalesOrderItems.parentSalesOrderItemId IS NULL` (dropping
|
|
31
|
-
summed **every** ASN quantity on the order with no such
|
|
32
|
-
of the ZBOOK (`parentSalesOrderItemId = 1131`), so
|
|
33
|
-
Confirmed by running the exact expression on prod:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
>
|
|
28
|
+
Root cause is in the shared parent `_Model_Compass_SalesOrder`, which decided fulfilled vs.
|
|
29
|
+
partiallyFulfilled by comparing two **order-level grand totals** whose two sides did not count the
|
|
30
|
+
same lines: the ordered side filtered `SalesOrderItems.parentSalesOrderItemId IS NULL` (dropping
|
|
31
|
+
bundle child lines) while the shipped side summed **every** ASN quantity on the order with no such
|
|
32
|
+
filter. On `SAC100664` the mouse is a child of the ZBOOK (`parentSalesOrderItemId = 1131`), so
|
|
33
|
+
ordered = 1 and shipped = 1 and it read fulfilled. Confirmed by running the exact expression on prod:
|
|
34
|
+
orderedSide `1.00`, shippedSide `1`.
|
|
35
|
+
|
|
36
|
+
Canada's fix is a **pure per-line rule** in its own subclass, with **no order-total check at all**.
|
|
37
|
+
Measured on prod after the rule: **489 fulfilled / 184 pendingFulfillment / 109 partiallyFulfilled**,
|
|
38
|
+
which is **90 orders corrected** from Fulfilled to Partially Fulfilled, plus 3 corrected from
|
|
39
|
+
partiallyFulfilled to pendingFulfillment.
|
|
40
|
+
|
|
41
|
+
> **⚠ Read this before trusting any older note.** An earlier version of this work was **reverted in
|
|
42
|
+
> git** and its published numbers were wrong — see *History: the first attempt was reverted* below.
|
|
43
|
+
> The rule described on this page was rebuilt from scratch and is **uncommitted working-tree code**.
|
|
44
|
+
|
|
45
|
+
> **Canada's rule is NOT the USA rule, and the two must not be merged.** Compass **USA is
|
|
46
|
+
> deliberately unchanged** and still runs the order-total logic, because the Office Depot ASN feed is
|
|
47
|
+
> not trustworthy at line level. See
|
|
42
48
|
> [the USA doc](../../compass-usa/features/order-fulfillment-status-per-line.md).
|
|
43
49
|
|
|
44
50
|
## Key files / entry points
|
|
45
51
|
|
|
46
52
|
- **`_underscore/Model/Compass/Canada/SalesOrder.php`** — `_fulfillmentStatus()`, a `protected static`
|
|
47
|
-
override
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
override. **Self-contained:** it builds its own expected-line set and its own per-line shipped
|
|
54
|
+
quantity and borrows nothing conditional from the parent. This is the whole Canada rule.
|
|
55
|
+
- **`_underscore/Model/Compass/SalesOrder.php`** — the parent. The fulfillment branch of `_status()`
|
|
56
|
+
was extracted into `protected static _fulfillmentStatus()` and is called via `static::`, which is
|
|
57
|
+
what makes the Canada override reachable. **The parent body is the ORIGINAL order-total logic,
|
|
58
|
+
moved verbatim** — the seam exists only so Canada can override. Proved behaviour-neutral by
|
|
59
|
+
diffing the emitted `_status` SQL: whitespace-identical, **7,548 normalized characters** both ways.
|
|
60
|
+
- `const ITEM_TYPES__NOT_SHIPPABLE` (Canada) — `SERVICE`, `SERVICES`, `CONSULTING`, `WARRANTY`.
|
|
51
61
|
|
|
52
62
|
## How it works
|
|
53
63
|
|
|
54
64
|
```
|
|
55
|
-
pendingFulfillment WHEN
|
|
56
|
-
partiallyFulfilled WHEN any line
|
|
65
|
+
pendingFulfillment WHEN every expected-to-ship line has received ZERO quantity
|
|
66
|
+
partiallyFulfilled WHEN any expected-to-ship line has received LESS than its ordered quantity
|
|
57
67
|
fulfilled otherwise
|
|
58
68
|
```
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
**Both branches read the same expected-line set and the same per-line quantity**, so the gate and
|
|
71
|
+
the shortfall test can never disagree. That symmetry is the design; keep it if you edit the method.
|
|
72
|
+
|
|
73
|
+
### "Expected to ship" — all four conditions
|
|
74
|
+
|
|
75
|
+
A `SalesOrderItems` row counts only when:
|
|
76
|
+
|
|
77
|
+
1. `price > 0`;
|
|
78
|
+
2. it has a `SalesOrderItems_PurchaseOrderItems` link (it was actually purchased);
|
|
79
|
+
3. its `ItemTypes.name` is **not** in `ITEM_TYPES__NOT_SHIPPABLE`; and
|
|
80
|
+
4. `COALESCE(Items.isFulfillable, 1) = 1`.
|
|
81
|
+
|
|
82
|
+
No parent/child line filter, so bundle children are judged on their own merits — that is the
|
|
83
|
+
original fix.
|
|
65
84
|
|
|
66
85
|
### The per-line shipped source (ASN bridge, and only the ASN bridge)
|
|
67
86
|
|
|
@@ -74,57 +93,136 @@ AdvanceShippingNoticeItems.purchaseOrderItemId
|
|
|
74
93
|
This mapping is **100% complete in `Client_CompassCanada`**: all **947** ASN items have a
|
|
75
94
|
`purchaseOrderItemId` and every one resolves to a sales-order line. Both join columns are indexed.
|
|
76
95
|
Canada ships through Grand & Toy ASNs only (see
|
|
77
|
-
[G&T ASN Import](grand-and-toy-asn-import.md)), so this single source is sufficient
|
|
78
|
-
|
|
96
|
+
[G&T ASN Import](grand-and-toy-asn-import.md)), so this single source is sufficient — Canada needs
|
|
97
|
+
none of USA's TOGa Tech / Office Depot fulfillment chain.
|
|
98
|
+
|
|
99
|
+
## Why each condition is there (measured, prod)
|
|
100
|
+
|
|
101
|
+
- **Condition 3 (item type) is what corrected the over-correction.** Canada has SERVICE and
|
|
102
|
+
CONSULTING lines that **do carry PO links but never ship**: `COMPASS-IPAD-ABM` (Apple Business
|
|
103
|
+
Manager enrolment — 53 lines, only 9 ever on an ASN), `SDU42Z/A` and `SDRU2Z/A` (AppleCare for
|
|
104
|
+
Enterprise), `CN-COMP-DED-IN` / `CN-CONE-SHR-WS1` (Profile SKUs), and `COMPASS-MACBOOK-ABM-ACE`.
|
|
105
|
+
Requiring them to ship held **25 fully-delivered orders** at partiallyFulfilled — e.g. `SAC100041`,
|
|
106
|
+
`SAC100088`, `SAC100523`. Excluding the item types fixed all 25. **This is the 115 to 90 correction.**
|
|
107
|
+
- **Condition 2 (purchased line) is still required.** Canada has **100 priced lines with no PO link
|
|
108
|
+
across 48 orders**; without the link test those lines can never ship and would strand their orders.
|
|
109
|
+
- **Condition 4 (`isFulfillable`) is a no-op in Canada today.** `Items.isFulfillable` is **NULL on
|
|
110
|
+
every Canada item**, so the `COALESCE(...,1)` always passes. It is in the expression so the rule
|
|
111
|
+
starts honouring the flag automatically once Canada is stamped — the **item-type list does all the
|
|
112
|
+
real work right now**. See
|
|
113
|
+
[isFulfillable — Data Quality & the Type-Derived Rule](../../compass-usa/features/isfulfillable-data-quality-and-type-rule.md).
|
|
114
|
+
- **`ItemTypes` is the only usable column in Canada.** Canada's `AssetTypes` are only
|
|
115
|
+
`Equipment` / `Hardware` / `Tablets` — **there is no `FEE` asset type in Canada at all**, so
|
|
116
|
+
`AssetTypes` gives zero signal here. USA is the opposite case; see the USA doc.
|
|
117
|
+
- **Keep the `price > 0` filter.** Only **9** of Canada's **69** zero-price lines ever receive an
|
|
118
|
+
ASN, so requiring them to ship would strand orders in partiallyFulfilled forever.
|
|
119
|
+
|
|
120
|
+
## Why per-line QUANTITY is the right unit (the counter-examples)
|
|
121
|
+
|
|
122
|
+
- **Order totals fail.** `SAC100664`: the mouse's shipment paid for the ZBOOK's quantity.
|
|
123
|
+
- **Line counts fail.** `SAC100108`: both expected lines had an ASN, so a line-count check matched —
|
|
124
|
+
but only 1 of 2 iPads and 1 of 2 accessories actually shipped. Also **96 Canada lines carry 2 to 3
|
|
125
|
+
ASN lines each**, so an ASN-line count can exceed the order-line count while the order is still
|
|
126
|
+
short.
|
|
127
|
+
- **There is no line-coverage gap to catch.** Across **601** Canada orders past approval with
|
|
128
|
+
shipments, **zero** read fulfilled while an expected line had no ASN line — a line with no ASN has
|
|
129
|
+
shipped 0 and is always caught as short. A line-count check would add nothing and only weaken the
|
|
130
|
+
rule.
|
|
131
|
+
- **Going per line also neutralises duplicate ASNs for the verdict.** A surplus stays on its own line
|
|
132
|
+
and can no longer cover another line's gap, so no clamping or de-duplication is needed.
|
|
133
|
+
|
|
134
|
+
## The pendingFulfillment gate reads quantity, not "an ASN exists"
|
|
135
|
+
|
|
136
|
+
The gate is **not** `COUNT(ASNs on the order) = 0`. Three orders (`SAC100131`, `SAC100509`,
|
|
137
|
+
`SAC100522`) had an ASN raised only against an AppleCare / ABM line while **zero physical goods had
|
|
138
|
+
shipped**, and therefore read partiallyFulfilled. Reading the expected-line quantity instead makes
|
|
139
|
+
them correctly read **pendingFulfillment**.
|
|
79
140
|
|
|
80
141
|
## Design decisions worth keeping
|
|
81
142
|
|
|
82
143
|
- **⚠ Do NOT switch Canada to the local `ItemFulfillments` / `ItemFulfillmentItems` tables**, even
|
|
83
144
|
though the line-level `_qtyFulfilled` UI column does read them. They are **incomplete** in Canada:
|
|
84
145
|
**47** orders have ASN shipments with no local `ItemFulfillment` at all and **52** more disagree on
|
|
85
|
-
quantity, so moving
|
|
86
|
-
|
|
87
|
-
- **
|
|
88
|
-
|
|
146
|
+
quantity, so moving this rule onto the base `_Model_Client_SalesOrder` logic would regress roughly
|
|
147
|
+
**99** orders.
|
|
148
|
+
- **Canada deliberately has NO order-total condition.** USA keeps one; Canada does not need it,
|
|
149
|
+
because its ASN bridge is complete and its expected-line set is never empty in the way USA's is.
|
|
89
150
|
- **Canada deliberately omits any Compass-vendor exclusion.** `VENDOR_ID__COMPASS = 26` on the parent
|
|
90
151
|
is the **US** Compass vendor id. In `Client_CompassCanada` all **901** POs are GRAND & TOY
|
|
91
|
-
(`vendorId 1`), and "COMPASS CANADA" is `vendorId 4` with **zero** POs —
|
|
92
|
-
|
|
93
|
-
- **Canada needs no purchased-line filter** (unlike USA) precisely because the ASN bridge is complete
|
|
94
|
-
and the vendor set is a single vendor.
|
|
152
|
+
(`vendorId 1`), and "COMPASS CANADA" is `vendorId 4` with **zero** POs — the inherited constant is
|
|
153
|
+
a latent wrong-id no-op. Do not "restore" it here.
|
|
95
154
|
|
|
96
155
|
## Gotchas / known issues
|
|
97
156
|
|
|
157
|
+
- **⚠ Uncommitted.** Both touched files pass `php -l`, nothing was committed or pushed, and the repo
|
|
158
|
+
is sitting on **`_production`** — a branch is required before committing. Canada has **not** been
|
|
159
|
+
exercised through the Supply UI yet.
|
|
98
160
|
- **The badge is display-only and never stored.** The status is computed live, so there is no
|
|
99
|
-
backfill and reverting the code fully reverts the behaviour.
|
|
161
|
+
backfill and reverting the code fully reverts the behaviour. **No cron, email or report reads the
|
|
162
|
+
fulfilled vs partiallyFulfilled distinction.** Canada's in-transit email cron
|
|
100
163
|
(`worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php`) keeps its **own
|
|
101
164
|
duplicated copy** of the `_status` CASE, but it only ever produces canceled /
|
|
102
165
|
pendingApprovalUnknown / pendingFulfillment / shipped and filters on
|
|
103
|
-
`computedStatusSlug = 'shipped'
|
|
104
|
-
|
|
166
|
+
`computedStatusSlug = 'shipped'`. Note the Canada pendingFulfillment gate is **no longer
|
|
167
|
+
byte-identical** to that copy (it now reads quantity, not ASN existence). The cron's copy is
|
|
168
|
+
unchanged and still only acts on `'shipped'`, so behaviour is unaffected, but the two are now a
|
|
169
|
+
genuine divergence.
|
|
105
170
|
- **⚠ Canada's order status is no longer identical to Compass USA.** Older notes (and the profile)
|
|
106
171
|
said "order status is shipped-only, same as Compass USA". The *stage* policy is still shared
|
|
107
|
-
(shipped-only), but the **fulfilled vs partiallyFulfilled rule now diverges by region
|
|
172
|
+
(shipped-only), but the **fulfilled vs partiallyFulfilled rule now diverges by region** — and it
|
|
173
|
+
diverges because **only Canada changed**.
|
|
108
174
|
- **The ApprovalDecision notification query on the parent calls `self::_status()`**, not `static::`,
|
|
109
|
-
so it always evaluates the **parent** fulfillment rule even for Canada.
|
|
110
|
-
|
|
111
|
-
|
|
175
|
+
so it always evaluates the **parent** fulfillment rule even for Canada. Because Canada's
|
|
176
|
+
pendingFulfillment gate now differs from the parent's, that query sees the **parent's** gate for
|
|
177
|
+
Canada orders. Harmless today (it only branches on pendingApproval / canceled / pendingFulfillment
|
|
178
|
+
and acts on approvals) but it is now a real divergence, not a theoretical one.
|
|
179
|
+
- **Canada's approval gate is healthy**, unlike USA's: 733 approved, 30 denied, only **19** stuck at a
|
|
180
|
+
pending-approval status. Canada also has all ten `SalesOrderStages` rows, where USA has only ids
|
|
181
|
+
5 to 9.
|
|
182
|
+
|
|
183
|
+
## History: the first attempt was reverted
|
|
184
|
+
|
|
185
|
+
- The original fix, commit **`90c964f1`** "Updating the status logic as per the client", was
|
|
186
|
+
**reverted by commit `43049d1d`** ("Revert …", kmaramreddy08, 2026-08-26 16:19). The revert is on
|
|
187
|
+
**`#sprint85`, `TRUE-81284`, `_beta`, `_production` and `_sandbox-client`**; only **`_sandbox-dev`**
|
|
188
|
+
still carries the original fix.
|
|
189
|
+
- Every refinement made after `90c964f1` was **never committed** and is gone from git. The rule on
|
|
190
|
+
this page was therefore **rebuilt from scratch** on `_production`.
|
|
191
|
+
- The ticket branch for this work is **`TRUE-81284`**, not `TRUE-80431`.
|
|
192
|
+
- **Any statement that this shipped is wrong**, including the previously published "115 Canada
|
|
193
|
+
orders corrected" figure.
|
|
112
194
|
|
|
113
195
|
## Change history
|
|
114
|
-
- 2026-08-
|
|
115
|
-
`
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`
|
|
122
|
-
|
|
123
|
-
|
|
196
|
+
- 2026-08-27 — **Corrects the 2026-08-26 entry below, which described work that was reverted in git
|
|
197
|
+
(`90c964f1` reverted by `43049d1d`) and published a wrong count.** Canada's `_fulfillmentStatus()`
|
|
198
|
+
rebuilt from scratch as a self-contained pure per-line rule with **no order-total check**:
|
|
199
|
+
pendingFulfillment when every expected-to-ship line has received zero, partiallyFulfilled when any
|
|
200
|
+
expected line is short, else fulfilled — both branches reading the same expected-line set and the
|
|
201
|
+
same per-line quantity. "Expected to ship" now also requires `ItemTypes.name NOT IN
|
|
202
|
+
ITEM_TYPES__NOT_SHIPPABLE` (SERVICE/SERVICES/CONSULTING/WARRANTY) and
|
|
203
|
+
`COALESCE(Items.isFulfillable,1) = 1`, which released **25 fully-delivered orders** wrongly held at
|
|
204
|
+
partiallyFulfilled by PO-linked-but-never-shipped AppleCare/ABM/Profile SKUs — so the real impact is
|
|
205
|
+
**90 orders corrected, not 115**. The pendingFulfillment gate now reads expected-line quantity
|
|
206
|
+
instead of "any ASN exists", correcting `SAC100131` / `SAC100509` / `SAC100522`. Prod after:
|
|
207
|
+
489 fulfilled / 184 pendingFulfillment / 109 partiallyFulfilled. Recorded that `isFulfillable` is
|
|
208
|
+
NULL on every Canada item (so the item-type list does the work), that Canada has no `FEE` asset
|
|
209
|
+
type, and why per-line **quantity** beats order totals (`SAC100664`) and line counts (`SAC100108`).
|
|
210
|
+
Uncommitted; repo on `_production`; not yet exercised through the Supply UI. (bala)
|
|
211
|
+
- 2026-08-26 — **REVERTED IN GIT — do not rely on this entry.** Original per-line
|
|
212
|
+
`_fulfillmentStatus()` override for Canada (pendingFulfillment when the order had no ASN at all,
|
|
213
|
+
otherwise partiallyFulfilled if any `price > 0` line was short of its own ASN-bridge quantity),
|
|
214
|
+
reported as 115 orders corrected. Commit `90c964f1` was reverted by `43049d1d` on 2026-08-26; the
|
|
215
|
+
rule over-corrected by 25 orders and its gate tested ASN existence rather than quantity. Superseded
|
|
216
|
+
by the 2026-08-27 entry. Still valid from this work: the ASN bridge is the correct source (100%
|
|
217
|
+
complete, 947 items), the local `ItemFulfillments` tables are not (would regress ~99 orders),
|
|
218
|
+
`price > 0` stays, and the inherited `VENDOR_ID__COMPASS = 26` is a US-only id and a no-op here.
|
|
219
|
+
(bala)
|
|
124
220
|
|
|
125
221
|
## Related docs
|
|
126
|
-
- [Compass USA — order aggregate
|
|
127
|
-
— the
|
|
222
|
+
- [Compass USA — order aggregate, deliberately unchanged](../../compass-usa/features/order-fulfillment-status-per-line.md)
|
|
223
|
+
— the parent rule, and why USA was not touched.
|
|
128
224
|
- [Grand & Toy ASN Import](grand-and-toy-asn-import.md) — where Canada's ASN rows come from.
|
|
129
225
|
- [IF Stage Lifecycle & Order Status](../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md)
|
|
130
226
|
— the shipped-only stage policy shared by both Compass regions.
|
|
227
|
+
- [FIELD_SQL calculated fields](../../../2.0/apps/_underscore/features/calculated-sql-fields.md)
|
|
228
|
+
— why a region-subclass override of a calculated field is picked up at all.
|
|
@@ -16,7 +16,7 @@ project: _Underscore
|
|
|
16
16
|
client: compass-canada
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-08-
|
|
19
|
+
updated: 2026-08-27
|
|
20
20
|
owners: [jcardinal, bala, tcox, apeterson]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
@@ -148,14 +148,20 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
148
148
|
lifecycle stages (picked/packed/shipped) + shipped backfill are seeded by
|
|
149
149
|
`dbchanges2/Client_CompassCanada/2026-06-30a - ItemFulfillmentLifecycleAndShippedBackfill.sql`.
|
|
150
150
|
See [IF stage lifecycle & order status](../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
|
|
151
|
-
- **⚠ Order status is no longer identical to Compass USA — the
|
|
152
|
-
|
|
153
|
-
`_Model_Compass_Canada_SalesOrder` with a
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
- **⚠ Order status is no longer identical to Compass USA — Canada is the only region whose
|
|
152
|
+
fulfilled-vs-partiallyFulfilled rule changed (2026-08-27).** Canada overrides
|
|
153
|
+
`_fulfillmentStatus()` in `_Model_Compass_Canada_SalesOrder` with a **pure per-line** rule and **no
|
|
154
|
+
order-total condition**, sourced from the **ASN bridge only**
|
|
155
|
+
(`AdvanceShippingNoticeItems.purchaseOrderItemId` -> `SalesOrderItems_PurchaseOrderItems`, 100%
|
|
156
|
+
complete here). A line counts as expected-to-ship only when `price > 0`, it has a PO-item link, its
|
|
157
|
+
`ItemTypes.name` is not in `ITEM_TYPES__NOT_SHIPPABLE` (SERVICE/SERVICES/CONSULTING/WARRANTY) and
|
|
158
|
+
`COALESCE(Items.isFulfillable,1) = 1` — the item-type list matters because Canada has PO-linked
|
|
159
|
+
AppleCare/ABM SKUs that never ship, and `isFulfillable` is NULL on every Canada item. Do not read
|
|
160
|
+
Canada's local `ItemFulfillments` tables for this (incomplete: ~99 orders would regress), and do
|
|
161
|
+
not port USA's rule or its `VENDOR_ID__COMPASS = 26` (a US vendor id; all 901 Canada POs are Grand
|
|
162
|
+
& Toy `vendorId 1`). **Corrects 90 prod orders, not the 115 previously published** — the first
|
|
163
|
+
attempt was reverted in git and over-corrected by 25 orders. **Uncommitted** (repo on
|
|
164
|
+
`_production`). See
|
|
159
165
|
[Fulfilled vs Partially Fulfilled](features/order-fulfillment-status-per-line.md).
|
|
160
166
|
- **Item titles in customer emails** come from the `ItemTranslations` sidecar (prod: 220 fr-CA rows,
|
|
161
167
|
100% coverage of every item ever ordered). Resolve the English/translated fallback **in PHP**, never
|
|
@@ -5,7 +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, _underscore/Model/Client/ApprovalTemplateStage.php, _underscore/Model/Quad/SalesOrder.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
|
+
| [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, dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql, toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx |
|
|
9
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 |
|
|
10
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 |
|
|
11
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 |
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
| [Compass MR/MA Order Auto-Approval & Status Gate](features/mr-ma-order-approval-and-status.md) | 2.0 | Compass **MR** and **MA** sales orders are system-generated from the MITS / Office Depot EDI pipeline (they do not originate as user-entered SA orders) and must | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
15
15
|
| [Compass ODP EDI 850 Line-Item Resolution (VA part number to IN SKU fallback)](features/odp-edi-850-item-resolution.md) | 1.0 | How each **PO1 line** on an inbound Office Depot (ODP) **EDI 850** is resolved to a real `Client_Compass` catalog item before cron `3a_import_office_depot_purch | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, library/app/Edi.php |
|
|
16
16
|
| [Compass Office Depot EDI 855 Acknowledgement + Over-Quantity PO Guard](features/odp-edi-855-acknowledgement-and-overquantity-guard.md) | 1.0 | How Compass acknowledges Office Depot (ODP) inbound **EDI 850** purchase orders with an **X12 855**, and the **over-quantity guard** that rejects a duplicate PO | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/4_transmit_office_depot_po_acknowledgements.php, library/app/edi.php |
|
|
17
|
-
| [Compass USA — Fulfilled vs Partially Fulfilled (order
|
|
17
|
+
| [Compass USA — Fulfilled vs Partially Fulfilled (order total kept; per-line rule built then rejected)](features/order-fulfillment-status-per-line.md) | 2.0 | TOGa Supply shows **Fulfilled** on Compass USA orders that are only partly shipped. | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php |
|
|
18
18
|
| [Compass PEOPLE-File User Lifecycle (duplicate accounts, reactivation grace window, raw-SQL deactivation)](features/people-file-user-lifecycle.md) | 2.0 | The nightly **PEOPLE** file cron (`_Worker_Client_Compass_PeopleFile`, `worker2/Worker/Client/Compass/PeopleFile.php`) owns the whole `Users` row lifecycle for | worker2/Worker/Client/Compass/PeopleFile.php |
|
|
19
19
|
| [Persona Model & Levy-Sector Gating (worker2 PEOPLE cron)](features/persona-model-and-levy-gating.md) | 2.0 | Compass USA catalogue visibility is driven by **personas** in `Client_Compass`. | worker2/Worker/Client/Compass/PeopleFile.php |
|
|
20
20
|
| [Compass Sales-Order Line-Number Renumbering (and why it hangs off the SO hooks)](features/sales-order-line-renumbering.md) | 2.0 | Compass sales-order lines must stay numbered **1..N with no gaps** after any add, edit, or delete — downstream MITS/PO linking reads `lineNumber` as an identity | _underscore/Model/Compass/SalesOrderItem.php, _underscore/Model/Compass/SalesOrder.php |
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
8
|
+
updated: 2026-08-27
|
|
9
9
|
owners: [jcardinal, bala]
|
|
10
10
|
files:
|
|
11
11
|
- _underscore/Model/Compass/AdvanceShippingNotice.php
|
|
@@ -26,6 +26,7 @@ related:
|
|
|
26
26
|
- ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
27
27
|
- ../../../2.0/apps/_underscore/features/model-save-parent-cascade-stored-field-deadlock.md
|
|
28
28
|
- ../../../2.0/apps/api2/features/v2-deadlock-retry.md
|
|
29
|
+
- order-fulfillment-status-per-line.md
|
|
29
30
|
---
|
|
30
31
|
|
|
31
32
|
## Summary
|
|
@@ -244,8 +245,88 @@ Compass Canada (`Model/Compass/Canada/`) is a separate sub-client. The Compass c
|
|
|
244
245
|
Forward-only; SA132898 repaired by `2026-06-18a` (delete the later tracking + orphaned package
|
|
245
246
|
row, keep the original; no backfill needed since the correct number is already present).
|
|
246
247
|
|
|
248
|
+
## Office Depot sends duplicate ASN messages (measured 2026-08-27)
|
|
249
|
+
|
|
250
|
+
**When an Office Depot PO line has more than one shipping notice, it is a DUPLICATE, not a partial
|
|
251
|
+
shipment.** This removes the "maybe it is a split shipment" caveat that earlier notes on this page
|
|
252
|
+
left open. Line-level shape of **every** ODP PO line on prod:
|
|
253
|
+
|
|
254
|
+
| Shape | ODP PO lines |
|
|
255
|
+
|---|---|
|
|
256
|
+
| never notified | 132,520 |
|
|
257
|
+
| exactly one notice, quantity matches | 35,362 |
|
|
258
|
+
| **multiple notices that EXCEED the PO line quantity** | **10,012** |
|
|
259
|
+
| multiple notices that legitimately add up to the quantity | **8** |
|
|
260
|
+
|
|
261
|
+
Order-level, of **30,431** ODP POs that have an ASN: **5,621** have a line credited over its PO
|
|
262
|
+
quantity, **6,874** have a line that was never notified, and **1,260** have **both** — that last
|
|
263
|
+
shape is the one that can mislead an order status, because a surplus on one line masks a gap on
|
|
264
|
+
another.
|
|
265
|
+
|
|
266
|
+
### It is being fixed, but ~7% remains
|
|
267
|
+
|
|
268
|
+
Share of ASN items landing on an over-credited line, by month:
|
|
269
|
+
|
|
270
|
+
| Month | Over-credited | Share |
|
|
271
|
+
|---|---|---|
|
|
272
|
+
| 2026-02 | 10,812 / 10,870 | 99% |
|
|
273
|
+
| 2026-03 | — | 66% |
|
|
274
|
+
| 2026-04 | — | 69% |
|
|
275
|
+
| 2026-05 | — | 41% |
|
|
276
|
+
| 2026-06 | — | 9% |
|
|
277
|
+
| 2026-07 | — | 8% |
|
|
278
|
+
| 2026-08 | — | 7% |
|
|
279
|
+
|
|
280
|
+
Something was clearly fixed around **May/June 2026**. It is not fully fixed.
|
|
281
|
+
|
|
282
|
+
### Worked example — SA135272 (raw payload evidence)
|
|
283
|
+
|
|
284
|
+
PO `50309979-1` (`PurchaseOrders.id 108979`): line 1 = `9X3V1UT` qty 4, line 2 = `9D9L6UT` qty 4,
|
|
285
|
+
both cleanly linked 1:1 to their sales-order lines. **Both ASNs (99473, 99685) referenced
|
|
286
|
+
`lineNumber 1`; line 2 was never mentioned.** Verified from the raw payloads in
|
|
287
|
+
`Logs_Compass.Api` ids **9111442** and **9152991**.
|
|
288
|
+
|
|
289
|
+
The second payload is a **bad message**, not a second shipment: same line, same qty 4, but **no
|
|
290
|
+
`shipToAddress`, no `trackingUrl`**, and `dateShipped = 2026-08-04T06:01:34` which is the earlier
|
|
291
|
+
batch run's timestamp, where the first carried a real date of `2026-08-11`. The tracking numbers
|
|
292
|
+
differ, so it is also not one shipment logged twice.
|
|
293
|
+
|
|
294
|
+
**Each ASN was ALSO POSTed twice, seconds apart, with identical payloads** (`9111442`/`9111477` and
|
|
295
|
+
`9152991`/`9152994`) — the sender double-fires. That is a second, separate duplication on top of the
|
|
296
|
+
wrong-line problem, and it is the same traffic pattern behind the
|
|
297
|
+
[same-PO deadlocks](../../../2.0/apps/_underscore/features/model-save-parent-cascade-stored-field-deadlock.md).
|
|
298
|
+
|
|
299
|
+
### Ruled out: "ODP numbers lines per shipment"
|
|
300
|
+
|
|
301
|
+
ODP does use **real PO line numbers**, so the drift is not a numbering convention: 45,995 ASN items
|
|
302
|
+
land on line 1, 12,774 on line 2, 5,772 on line 3, continuing past line 12. On two-line POs it
|
|
303
|
+
referenced line 2 **correctly in 3,447 of 4,797 cases**.
|
|
304
|
+
|
|
305
|
+
### ⚠ Contract weakness to record
|
|
306
|
+
|
|
307
|
+
The ASN payload identifies the PO item **positionally**, by `purchaseOrderItem.lineNumber`, **not by
|
|
308
|
+
uuid**. So any line-numbering drift — on either side — silently credits the wrong item, with no
|
|
309
|
+
error. This is the same positional-identity class as the PO-local `lineNumber` collision fixed in
|
|
310
|
+
2026-06 (see the change history below) and it is why
|
|
311
|
+
[Compass USA's per-line fulfillment rule was rejected](order-fulfillment-status-per-line.md): the
|
|
312
|
+
feed cannot currently support a line-level completeness test.
|
|
313
|
+
|
|
247
314
|
## Change history
|
|
248
315
|
Dated one-liners, newest first.
|
|
316
|
+
- 2026-08-27 — Quantified the **Office Depot duplicate-ASN** problem on prod and settled what it is:
|
|
317
|
+
of ODP PO lines with more than one shipping notice, **10,012 exceed the PO line quantity and only 8
|
|
318
|
+
are genuine split shipments**, so a second notice on an ODP line is a **duplicate**, not a partial.
|
|
319
|
+
1,260 ODP POs carry both an over-credited line and a never-notified line — the shape that misleads
|
|
320
|
+
order status. Trend shows a fix around May/June 2026 (99% of Feb ASN items landed on over-credited
|
|
321
|
+
lines, down to ~7% in August). Worked `SA135272` back to the raw `Logs_Compass.Api` payloads
|
|
322
|
+
(9111442, 9152991): both ASNs referenced `lineNumber 1` and line 2 was never mentioned, and the
|
|
323
|
+
second payload is malformed (no `shipToAddress`, no `trackingUrl`, `dateShipped` = the earlier batch
|
|
324
|
+
run timestamp). Each ASN was also POSTed twice seconds apart with identical payloads
|
|
325
|
+
(9111442/9111477, 9152991/9152994) — the sender double-fires. Ruled out the "per-shipment line
|
|
326
|
+
numbering" theory (ODP uses real PO line numbers; line 2 referenced correctly in 3,447 of 4,797
|
|
327
|
+
two-line POs). Recorded the underlying contract weakness: the payload identifies the PO item
|
|
328
|
+
**positionally by `purchaseOrderItem.lineNumber`, not by uuid**. Investigation only, no code change;
|
|
329
|
+
this is the evidence that blocked the Compass USA per-line fulfillment rule. (bala)
|
|
249
330
|
- 2026-08-20 — Recorded an **open, deterministic 500 `EO-1`** in `postPost`: an ASN item whose
|
|
250
331
|
SalesOrderItem has one already-tracked `ItemFulfillmentItem` takes the reconcile path, the IFI is
|
|
251
332
|
skipped by `itemFulfillmentItemHasTracking()`, and the handler then fails rather than completing —
|
|
@@ -6,12 +6,13 @@ project: _Underscore
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/Item.php
|
|
13
13
|
- library/app/api/toga2.php
|
|
14
14
|
- worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php
|
|
15
|
+
- dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql
|
|
15
16
|
- toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx
|
|
16
17
|
related:
|
|
17
18
|
- ../profile.md
|
|
@@ -19,6 +20,7 @@ related:
|
|
|
19
20
|
- ../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md
|
|
20
21
|
- ../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md
|
|
21
22
|
- ../../../1.0/apps/worker/workflows/isfulfillable-multi-client-backfill.md
|
|
23
|
+
- ../../compass-canada/features/order-fulfillment-status-per-line.md
|
|
22
24
|
---
|
|
23
25
|
|
|
24
26
|
## Summary
|
|
@@ -30,8 +32,9 @@ set are partly wrong. This doc records the measured state of `Client_Compass` /
|
|
|
30
32
|
**decision** to override NetSuite with a type-derived rule for Compass, and the **write-time guard**
|
|
31
33
|
that decision requires in order to survive the daily NetSuite sync.
|
|
32
34
|
|
|
33
|
-
No source files were changed and **no migration has been written yet**.
|
|
34
|
-
from **production** on
|
|
35
|
+
No source files were changed and **no migration has been written yet**. Figures in the sections dated
|
|
36
|
+
2026-08-12 were read from **production** on that date; the later sections (retraction, column choice,
|
|
37
|
+
data-fix status) were measured on **2026-08-27** against prod and **client-sandbox** as marked.
|
|
35
38
|
|
|
36
39
|
## Measured state (prod, 2026-08-12)
|
|
37
40
|
- **`Client_Compass`, catalog 1:** **770 NULL / 25 zero / 697 one.**
|
|
@@ -164,7 +167,103 @@ Because these tables are `utf8mb4_0900_ai_ci` (**case-insensitive**), the single
|
|
|
164
167
|
data. **Do not hand-maintain two string lists**, but **do** re-confirm the Canada vocabulary
|
|
165
168
|
separately, since the names genuinely differ and a US list *looks* wrong for Canada.
|
|
166
169
|
|
|
170
|
+
## ⚠ RETRACTED (2026-08-27) — "it appears on an ASN, so it must be fulfillable"
|
|
171
|
+
|
|
172
|
+
An in-session proposal to run `UPDATE Items SET isFulfillable = 1` for **7 Compass items — 691, 237,
|
|
173
|
+
661, 2004, 2005, 703, 657** — on the grounds that they appeared to ship was **wrong and was
|
|
174
|
+
withdrawn.**
|
|
175
|
+
|
|
176
|
+
All 7 have `AssetTypes.name = 'FEE'` and their own descriptions confirm it: *"5-Year Comprehensive
|
|
177
|
+
Coverage"*, *"Printer Warranty 3 Year Plan"*, *"Staging and Kitting Charge"*, *"Tablet Lab fee"*,
|
|
178
|
+
*"Config fee for Samsung Configurations"*. **`isFulfillable = 0` is CORRECT for all of them.**
|
|
179
|
+
|
|
180
|
+
They "ship" only because the vendor lists the warranty / staging charge as a **line on the shipping
|
|
181
|
+
notice next to the hardware**. This is the same ride-along phenomenon measured above, seen from the
|
|
182
|
+
other direction:
|
|
183
|
+
|
|
184
|
+
> **Appearing on an ASN is NOT the same as being a physical good.** Never promote an item to
|
|
185
|
+
> `isFulfillable = 1` on shipment evidence alone.
|
|
186
|
+
|
|
187
|
+
## Never hand-edit `Items.isFulfillable`
|
|
188
|
+
|
|
189
|
+
Two independent reasons, both verified:
|
|
190
|
+
|
|
191
|
+
1. **It is written by the NetSuite sync.** Beyond the Phase 1 existing-item refresh already
|
|
192
|
+
documented, `dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql`
|
|
193
|
+
(`recordId 21`) registers interceptors that **copy the value up the SO / PO chain**. A manual
|
|
194
|
+
value is therefore transient by design.
|
|
195
|
+
2. **It drives UI behaviour, not just data.** `toga2-supply`
|
|
196
|
+
`pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx` gates the
|
|
197
|
+
**Qty Fulfilled** cell's clickability on `String(item?.Items?.isFulfillable) === "1"`, alongside a
|
|
198
|
+
separate Compass-only check on `AssetTypes.name === "FEE"`. Flipping the flag silently changes what
|
|
199
|
+
a user can click.
|
|
200
|
+
|
|
201
|
+
## Which column does code read — `ItemTypes` or `AssetTypes`? (USA)
|
|
202
|
+
|
|
203
|
+
The two columns disagree, and the gap matters:
|
|
204
|
+
|
|
205
|
+
| Signal | Priced Compass USA order lines | Lines ever on an ASN |
|
|
206
|
+
|---|---|---|
|
|
207
|
+
| `AssetTypes.name = 'FEE'` | 90,857 | 732 |
|
|
208
|
+
| `ItemTypes.name = 'FEE'` | 89,921 | 70 |
|
|
209
|
+
|
|
210
|
+
**936 lines are assetType FEE but NOT itemType FEE.**
|
|
211
|
+
|
|
212
|
+
**Decision (developer's call): code reads `ItemTypes`, and lets `Items.isFulfillable` carry the
|
|
213
|
+
assetType meaning**, because the data fix below folds **both** columns into the flag. So the flag is
|
|
214
|
+
the single source of truth for consumers and `ItemTypes` is only a secondary, code-side signal.
|
|
215
|
+
|
|
216
|
+
**Compass Canada has no such choice.** Canada's `AssetTypes` are only `Equipment` / `Hardware` /
|
|
217
|
+
`Tablets` — **there is no `FEE` asset type in Canada at all** — so `ItemTypes` is the only usable
|
|
218
|
+
column there, and Canada's fulfillment rule uses an item-type exclusion list
|
|
219
|
+
(`SERVICE`, `SERVICES`, `CONSULTING`, `WARRANTY`) rather than the flag. See
|
|
220
|
+
[Compass Canada — Fulfilled vs Partially Fulfilled](../../compass-canada/features/order-fulfillment-status-per-line.md).
|
|
221
|
+
|
|
222
|
+
## Status of the type-derived data fix (verified 2026-08-27)
|
|
223
|
+
|
|
224
|
+
The developer's two-statement `UPDATE`, scoped to **`catalogId = 1`**, sets `isFulfillable = 0` where
|
|
225
|
+
`ItemTypes.name` **or** `AssetTypes.name` `IN ('FEE','SERVICEFEES')` and `1` otherwise. It is
|
|
226
|
+
**pending Alex's sign-off before prod**. Verified environment state:
|
|
227
|
+
|
|
228
|
+
| Environment | State |
|
|
229
|
+
|---|---|
|
|
230
|
+
| **client-sandbox** | **applied** — 58 fee items now `0` |
|
|
231
|
+
| **dev-sandbox** | **NOT applied** — fee items still NULL or 1 |
|
|
232
|
+
| prod | not applied |
|
|
233
|
+
|
|
234
|
+
Two gaps to carry forward:
|
|
235
|
+
|
|
236
|
+
- **⚠ The NetSuite sync rewrites some of them back.** On client-sandbox **6 fee items are back at
|
|
237
|
+
`isFulfillable = 1` despite matching the WHERE** — `US-AND-CROTHALL-PO`, `LVY_MobileOps`,
|
|
238
|
+
`US-TASKS-TECHS-IN`, `US-IOS-ESFM-NUVOLO`, `HDL-TASKUP-IN`, `Unidine-Knox` — with `dtUpdated`
|
|
239
|
+
18–25 Aug, i.e. **after** the fix ran. Their `itemType` is `SOFTWARE` / `SERVICES` / `HARDWARE`
|
|
240
|
+
while only their `assetType` says FEE, so **an itemType-based code test will not catch these if the
|
|
241
|
+
flag drifts.** This is the concrete failure mode of the column decision above, and it re-proves the
|
|
242
|
+
write-time-guard requirement recorded further up this page.
|
|
243
|
+
- **⚠ The `catalogId = 1` scope misses 49 items carrying 52,367 priced order lines** —
|
|
244
|
+
`PC-KIT-OPT2` (33,259 lines), `CG-MEDKITTING` (6,317), `CG-MEDKITTING-NWN-APPLE`, `ASI-WAREHAND`,
|
|
245
|
+
`ODP-COMPASS_KIOSKBUILD` — all on **catalog 2 or NULL**, all flagged `isFulfillable = 1`, and
|
|
246
|
+
**none has ever shipped**. They cause no problem *today* only because none of them has a PO link,
|
|
247
|
+
so a purchased-line test excludes them anyway. Widen the scope or accept that the flag is wrong on
|
|
248
|
+
the largest-volume non-shipping SKU in the catalog.
|
|
249
|
+
|
|
167
250
|
## Change history
|
|
251
|
+
- 2026-08-27 — **Retracted a wrong in-session proposal** to set `isFulfillable = 1` on 7 Compass items
|
|
252
|
+
(691, 237, 661, 2004, 2005, 703, 657) because they appeared on shipping notices: all 7 are
|
|
253
|
+
`AssetTypes.name = 'FEE'` warranty/staging/config charges, so `0` is correct — **appearing on an ASN
|
|
254
|
+
is not the same as being a physical good.** Recorded two hard reasons never to hand-edit the column:
|
|
255
|
+
the NetSuite sync owns it (`dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql`,
|
|
256
|
+
`recordId 21`, copies the value up the SO/PO chain) and it gates the toga2-supply Qty Fulfilled
|
|
257
|
+
cell's clickability. Settled the **column question** — code reads `ItemTypes` and lets the flag carry
|
|
258
|
+
the assetType meaning (USA: 90,857 assetType-FEE priced lines vs 89,921 itemType-FEE, **936 lines
|
|
259
|
+
assetType-only**); Canada has no `FEE` assetType at all, so it uses an item-type exclusion list
|
|
260
|
+
instead. Verified the **data fix state**: applied on **client-sandbox** (58 fee items now 0), **not**
|
|
261
|
+
in effect on dev-sandbox, not on prod. Two gaps: 6 client-sandbox fee items are **back at 1** with
|
|
262
|
+
`dtUpdated` 18–25 Aug (`US-AND-CROTHALL-PO`, `LVY_MobileOps`, `US-TASKS-TECHS-IN`,
|
|
263
|
+
`US-IOS-ESFM-NUVOLO`, `HDL-TASKUP-IN`, `Unidine-Knox` — all assetType-only FEE, so an itemType code
|
|
264
|
+
test misses them), and the `catalogId = 1` scope **misses 49 items carrying 52,367 priced lines**
|
|
265
|
+
(`PC-KIT-OPT2` at 33,259, `CG-MEDKITTING` at 6,317, …) on catalog 2 or NULL, all flagged 1 and never
|
|
266
|
+
shipped. Investigation only, no code or migration written. (bala)
|
|
168
267
|
- 2026-08-12 — Prod read-only investigation of `Items.isFulfillable` across `Client_Compass` /
|
|
169
268
|
`Client_CompassCanada`. Established that remaining NULLs are **structurally unreachable**
|
|
170
269
|
(vendor-direct POs have no Agilant-tier SO, so the chain walk has nothing to walk — 0 NULL items
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: ["rgirish", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/SalesOrder.php
|
|
@@ -31,11 +31,19 @@ the active `ApprovalTemplate` has no `ApprovalDecision`, `_status` returns the p
|
|
|
31
31
|
Two independent root causes produced the stuck-order population, and both are addressed at the
|
|
32
32
|
source (auto-approval + item population), **not** in the status SQL.
|
|
33
33
|
|
|
34
|
+
> **⚠ Scope update (2026-08-27).** This page started as an MR/MA story, but the approval gate strands
|
|
35
|
+
> **11,341** Compass USA orders overall — MR is now clean, and Office Depot (9,926) plus a live
|
|
36
|
+
> Agilant Drop Ship spike account for most of it. Read
|
|
37
|
+
> [The stuck population is no longer MR/MA-shaped](#-the-stuck-population-is-no-longer-mrma-shaped-re-measured-2026-08-27)
|
|
38
|
+
> before acting on the older figures on this page.
|
|
39
|
+
|
|
34
40
|
## Key files / entry points
|
|
35
41
|
- **`_underscore/Model/Compass/SalesOrder.php`**
|
|
36
|
-
- `_status` calculated field — approval gate runs before the fulfillment machine
|
|
37
|
-
|
|
38
|
-
`static
|
|
42
|
+
- `_status` calculated field — the approval gate runs **before** the fulfillment machine (gates 1–3
|
|
43
|
+
of 4). The fulfillment half sits in a separate `protected static _fulfillmentStatus()` called via
|
|
44
|
+
`static::`, which **only Compass Canada overrides — Compass USA's rule is unchanged**. That seam
|
|
45
|
+
was reverted in git on 2026-08-26 and rebuilt uncommitted; either way **the approval gate
|
|
46
|
+
described on this page is untouched and stays in `_status()`.** See
|
|
39
47
|
[Fulfilled vs Partially Fulfilled](order-fulfillment-status-per-line.md) and
|
|
40
48
|
[IF stage lifecycle & order status](../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
|
|
41
49
|
- `postPost` — the `if ($isMrOrder)` block loops `foreach ([1,2] as $step)` and issues two
|
|
@@ -56,6 +64,16 @@ row on **any** order whenever an active template exists. Compass `_status` then
|
|
|
56
64
|
`ApprovalDecision` for every active-template stage before it will look at fulfillment. No
|
|
57
65
|
decision on a stage ⇒ `_status` short-circuits to that stage's slug.
|
|
58
66
|
|
|
67
|
+
|
|
68
|
+
`_status` evaluates **four gates in this order**, and only the last one looks at shipping:
|
|
69
|
+
|
|
70
|
+
1. an explicit `salesOrderStageId` (and not 2) wins outright;
|
|
71
|
+
2. then any **denied** `ApprovalDecision` returns `canceled`;
|
|
72
|
+
3. then any active-template stage with **no decision** returns that stage's slug — and **never looks
|
|
73
|
+
at shipping**;
|
|
74
|
+
4. only then fulfillment (`static::_fulfillmentStatus()`).
|
|
75
|
+
|
|
76
|
+
Gate 3 is where the stuck orders live.
|
|
59
77
|
### Auto-approval (the intended MR path)
|
|
60
78
|
`_Model_Compass_SalesOrder::postPost` auto-approves MR orders by POSTing an approval decision for
|
|
61
79
|
each of the two stages (`isApproved=true`). `_status` only checks `isApproved` on the decision —
|
|
@@ -92,7 +110,61 @@ columns is fully valid.
|
|
|
92
110
|
`_String::parseBetween()` — see
|
|
93
111
|
[_String helpers](../../../2.0/apps/_underscore/features/string-html-entity-helpers.md).
|
|
94
112
|
|
|
95
|
-
##
|
|
113
|
+
## ⚠ The stuck population is no longer MR/MA-shaped (re-measured 2026-08-27)
|
|
114
|
+
|
|
115
|
+
**The MR/MA framing on this page now understates and mislocates the problem.** The 2026-07-13 entry
|
|
116
|
+
below recorded **3,119** stuck orders scoped to MR/MA. Re-measured across **all** Compass USA orders
|
|
117
|
+
on the computed path:
|
|
118
|
+
|
|
119
|
+
| `_status` outcome (USA, computed path) | Orders |
|
|
120
|
+
|---|---|
|
|
121
|
+
| fully approved, reaches fulfillment | 37,173 |
|
|
122
|
+
| **stuck at a pending-approval status** | **11,341** |
|
|
123
|
+
| correctly has no `Approval` row at all | 21,362 |
|
|
124
|
+
| denied (returns `canceled`) | 353 |
|
|
125
|
+
|
|
126
|
+
**The true figure is ~3.5x the recorded one**, and most of it is not MR/MA. Stuck orders by customer:
|
|
127
|
+
|
|
128
|
+
| Customer | Stuck |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Office Depot | 9,926 |
|
|
131
|
+
| Compass | 950 |
|
|
132
|
+
| Agilant | 465 |
|
|
133
|
+
|
|
134
|
+
- **MR is now clean — 0 stuck against 4,584 approved.** The 2026-07-13 decisions-only backfill worked
|
|
135
|
+
and the 2026-06-03 auto-approval holds.
|
|
136
|
+
- **MA is 222 and growing.**
|
|
137
|
+
- The problem is **ongoing**, generating **1,200–2,000 new stuck orders a month**, and the
|
|
138
|
+
**Compass-customer share is rising**: 280 in August against 70–148 in earlier months.
|
|
139
|
+
|
|
140
|
+
### ⚠ NEW LIVE SIGNAL — Agilant stuck orders spiked
|
|
141
|
+
|
|
142
|
+
Agilant stuck orders ran at roughly **5 a month**, then **268 appeared on 2026-08-11**, and the rate
|
|
143
|
+
has been **17–51 a day since 2026-08-24**. Every one is **`salesOrderTypeId 2` (Drop Ship)** with
|
|
144
|
+
**`createdByUserId NULL`** — i.e. system-created orders on a path with no auto-approval, the same
|
|
145
|
+
class of defect as the original MR problem but on a different order type. This is live and
|
|
146
|
+
unresolved.
|
|
147
|
+
|
|
148
|
+
### Compass Canada's gate is healthy
|
|
149
|
+
|
|
150
|
+
For contrast: **733 approved, 30 denied, only 19 stuck.** Canada does not share this problem.
|
|
151
|
+
|
|
152
|
+
### Verified clean (negatives worth recording)
|
|
153
|
+
|
|
154
|
+
Audited in both Compass clients on 2026-08-27:
|
|
155
|
+
|
|
156
|
+
- each client has exactly **one** active `ApprovalTemplate` (id 1) with two stages mapping to
|
|
157
|
+
`pendingInitialApproval` / `pendingApproval`, so the approval gate **cannot return NULL**;
|
|
158
|
+
- **no order has more than one `Approval` row**, and no stage has duplicate decisions;
|
|
159
|
+
- every `salesOrderStageId` in use resolves to a real `SalesOrderStages` row — no orphans;
|
|
160
|
+
- therefore **`_status` cannot return NULL in either client today.**
|
|
161
|
+
|
|
162
|
+
So the stuck orders are a **data-population** problem (missing decisions), not a schema or
|
|
163
|
+
template-integrity problem. The 2026-07-13 ruling still stands: **do not fix them in `_status`.** The
|
|
164
|
+
fulfillment rule is gate 4 and never runs for any of these 11,341 orders — see
|
|
165
|
+
[Fulfilled vs Partially Fulfilled](order-fulfillment-status-per-line.md).
|
|
166
|
+
|
|
167
|
+
## Prod status buckets (2026-07-13 — historical, superseded by the section above)
|
|
96
168
|
| Type | NO_APPROVAL (correct — pre-template, gate falls through) | APPROVAL_NO_DECISION (STUCK) | FULLY_APPROVED |
|
|
97
169
|
|---|---|---|---|
|
|
98
170
|
| MR | 5922 (Sep 2025–Jan 2026) | 2927 | 583 |
|
|
@@ -123,6 +195,24 @@ Safe scoping baked in — **decisions only, never Approvals**:
|
|
|
123
195
|
from the sibling ODP SO first).
|
|
124
196
|
|
|
125
197
|
## Change history
|
|
198
|
+
- 2026-08-27 — **Re-measured the stuck-order population and reframed this page: the true figure is
|
|
199
|
+
11,341, ~3.5x the 3,119 recorded on 2026-07-13, and it is no longer MR/MA-shaped.** USA
|
|
200
|
+
computed-path split is 37,173 approved / 11,341 stuck / 21,362 correctly with no `Approval` row /
|
|
201
|
+
353 denied; by customer the stuck are Office Depot 9,926, Compass 950, Agilant 465. **MR is now
|
|
202
|
+
clean (0 stuck vs 4,584 approved), so the backfill worked**, while MA is 222 and growing and the
|
|
203
|
+
whole problem still generates 1,200–2,000 new stuck orders a month with the Compass-customer share
|
|
204
|
+
rising (280 in August vs 70–148 earlier). **New live signal: Agilant went from ~5/month to 268 on
|
|
205
|
+
2026-08-11 and 17–51/day since 2026-08-24, all `salesOrderTypeId 2` (Drop Ship) with
|
|
206
|
+
`createdByUserId NULL`.** Canada's gate is healthy (733 approved, 30 denied, 19 stuck). Recorded
|
|
207
|
+
`_status`'s explicit four-gate order, and the clean-audit negatives that make this a data-population
|
|
208
|
+
problem rather than a template one: one active template per client, no duplicate `Approval` rows or
|
|
209
|
+
decisions, no orphan `salesOrderStageId`, so `_status` cannot return NULL in either client.
|
|
210
|
+
Investigation only, no code change. (bala)
|
|
211
|
+
- 2026-08-27 — Corrected the entry below: the `_fulfillmentStatus()` extraction it describes was
|
|
212
|
+
**reverted in git** (`90c964f1` reverted by `43049d1d`) and rebuilt this session. The seam is real
|
|
213
|
+
again in the working tree but **uncommitted**, and **only Compass Canada overrides it — Compass USA
|
|
214
|
+
is deliberately unchanged**. The approval gate this page documents remains untouched either way.
|
|
215
|
+
(bala)
|
|
126
216
|
- 2026-08-26 — Noted that `_status()`'s fulfillment half moved into `_fulfillmentStatus()` (per-region
|
|
127
217
|
seam); the approval gate this page documents is untouched, and the "do not fix stuck MR/MA orders in
|
|
128
218
|
`_status`" ruling still stands — the stuck orders are empty, so no fulfillment rule helps them.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Compass USA — Fulfilled vs Partially Fulfilled (order
|
|
2
|
+
title: Compass USA — Fulfilled vs Partially Fulfilled (order total kept; per-line rule built then rejected)
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: _underscore
|
|
5
5
|
project: _Underscore
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/SalesOrder.php
|
|
@@ -18,79 +18,127 @@ related:
|
|
|
18
18
|
- ../../../2.0/apps/_underscore/features/sales-order-status-filter-surface.md
|
|
19
19
|
- mr-ma-order-approval-and-status.md
|
|
20
20
|
- asn-to-item-fulfillment.md
|
|
21
|
+
- isfulfillable-data-quality-and-type-rule.md
|
|
21
22
|
- ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
|
|
22
23
|
---
|
|
23
24
|
|
|
24
25
|
## Summary
|
|
25
26
|
|
|
26
|
-
TOGa Supply
|
|
27
|
-
structural
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
TOGa Supply shows **Fulfilled** on Compass USA orders that are only partly shipped. The cause is
|
|
28
|
+
structural: the fulfillment half of `_Model_Compass_SalesOrder` decides fulfilled vs.
|
|
29
|
+
partiallyFulfilled by comparing **two order-level grand totals** — an "ordered" sum against a
|
|
30
|
+
"shipped" sum. A grand total lets one line's surplus cancel another line's gap, so an order with
|
|
31
|
+
line 1 short and line 2 over-shipped reads as complete.
|
|
32
|
+
|
|
33
|
+
**As of 2026-08-27 Compass USA is deliberately NOT fixed.** A per-line rule was written, measured
|
|
34
|
+
against prod, and then **reverted on purpose**. The parent method now holds the **original
|
|
35
|
+
order-total logic moved verbatim**; the only change to USA's code path is a refactor seam so
|
|
36
|
+
Compass **Canada** can override the rule.
|
|
37
|
+
|
|
38
|
+
> **The reason USA was left alone is the Office Depot ASN feed, not the SQL.** ODP sends
|
|
39
|
+
> **duplicate ASN messages** that credit the same PO line twice: of ODP PO lines with more than one
|
|
40
|
+
> shipping notice, **10,012 exceed the PO line quantity and only 8 are legitimate split shipments**.
|
|
41
|
+
> A per-line rule fed by that data would move ~5,223 orders to Partially Fulfilled, many of which
|
|
42
|
+
> were probably delivered. Full evidence lives in
|
|
43
|
+
> [ASN → ItemFulfillment](asn-to-item-fulfillment.md#office-depot-sends-duplicate-asn-messages-measured-2026-08-27).
|
|
44
|
+
> **Revisit this page only after the ODP feed is fixed.**
|
|
45
|
+
|
|
46
|
+
Compass Canada has the same false-Fulfilled bug and **has** been changed, to a pure per-line rule —
|
|
47
|
+
see the [Canada doc](../../compass-canada/features/order-fulfillment-status-per-line.md). USA cannot
|
|
48
|
+
adopt it as-is; that is the subject of the design gotchas below.
|
|
49
|
+
|
|
50
|
+
## Status of the code (read this first)
|
|
51
|
+
|
|
52
|
+
- **USA behaviour is unchanged.** The extraction was proved behaviour-neutral by simulating the PHP
|
|
53
|
+
concatenation and diffing the emitted `_status` SQL: **whitespace-identical, 7,548 normalized
|
|
54
|
+
characters both ways**.
|
|
55
|
+
- An earlier attempt at a real USA change shipped as commit **`90c964f1`** and was **reverted by
|
|
56
|
+
`43049d1d`** (2026-08-26 16:19) on `#sprint85`, `TRUE-81284`, `_beta`, `_production` and
|
|
57
|
+
`_sandbox-client`. Only **`_sandbox-dev`** still carries it. So **any claim that a USA per-line
|
|
58
|
+
rule shipped — including the previously published "Fulfilled fell by exactly 5,234" — is wrong.**
|
|
59
|
+
- The current working tree is **uncommitted**, on branch `_production`, and both touched files pass
|
|
60
|
+
`php -l`. The ticket branch for this work is **`TRUE-81284`**.
|
|
42
61
|
|
|
43
62
|
## Key files / entry points
|
|
44
63
|
|
|
45
64
|
- **`_underscore/Model/Compass/SalesOrder.php`**
|
|
46
|
-
- `_status()` —
|
|
47
|
-
**`static::_fulfillmentStatus($alias_SalesOrders)`**.
|
|
48
|
-
- `_fulfillmentStatus()` (`protected static
|
|
49
|
-
|
|
50
|
-
- `
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- `const VENDOR_ID__COMPASS = 26` — the **US** Compass vendor id, excluded from "purchased" lines.
|
|
65
|
+
- `_status()` — four gates, in order (see *The `_status` gate order* below). Its fulfillment branch
|
|
66
|
+
now calls **`static::_fulfillmentStatus($alias_SalesOrders)`**.
|
|
67
|
+
- `_fulfillmentStatus()` (`protected static`) — **the original order-total logic, verbatim.** This
|
|
68
|
+
is what Compass USA runs. Canada overrides it.
|
|
69
|
+
- `const VENDOR_ID__COMPASS = 26` — the **US** Compass vendor id, excluded from "purchased" lines.
|
|
70
|
+
- **`_underscore/Model/Compass/Canada/SalesOrder.php`** — the only region that actually overrides the
|
|
71
|
+
rule.
|
|
54
72
|
- Front end (labels only): toga2-supply `FILTERFIELDS.ts` (user-selectable status filter) and
|
|
55
73
|
`renderBadge.tsx` (badge text).
|
|
56
74
|
|
|
57
|
-
##
|
|
58
|
-
|
|
59
|
-
### The seam: `_status()` vs `_fulfillmentStatus()`
|
|
75
|
+
## The `_status` gate order (unchanged, and the reason fulfillment often never runs)
|
|
60
76
|
|
|
61
|
-
`_status
|
|
62
|
-
approval gate (denials to canceled, any undecided stage to that stage's slug); only then fulfillment.
|
|
63
|
-
The refactor moved **only** the fulfillment branch into `_fulfillmentStatus()` and calls it with
|
|
64
|
-
`static::`, so a region subclass can replace the rule without touching the approval logic. The
|
|
65
|
-
extraction was proved behaviour-preserving by simulating the PHP concatenation and diffing the
|
|
66
|
-
emitted `_status` SQL: **whitespace-identical, same 7,548 normalized characters.**
|
|
77
|
+
`_status` evaluates four gates in this order:
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
1. an explicit `salesOrderStageId` (and not 2) wins outright;
|
|
80
|
+
2. then any **denied** `ApprovalDecision` returns `canceled`;
|
|
81
|
+
3. then any active-template stage with **no decision** returns that stage's slug and **never looks at
|
|
82
|
+
shipping**;
|
|
83
|
+
4. only then fulfillment.
|
|
69
84
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
**Gate 3 is the bigger live problem on USA.** Of USA orders on the computed path, **37,173** are
|
|
86
|
+
fully approved and reach fulfillment while **11,341 are stuck at a pending-approval status** — so
|
|
87
|
+
roughly a quarter of computed-path orders never reach any fulfillment rule at all. That population,
|
|
88
|
+
its per-customer breakdown, and the live Agilant spike are documented in
|
|
89
|
+
[MR/MA Order Approval & Status Gate](mr-ma-order-approval-and-status.md). Fixing the fulfillment rule
|
|
90
|
+
would not touch a single one of them.
|
|
73
91
|
|
|
74
|
-
|
|
92
|
+
## What USA runs today
|
|
75
93
|
|
|
76
94
|
```
|
|
77
|
-
|
|
95
|
+
pendingFulfillment WHEN COUNT(shipped TOGa Tech ItemFulfillments) + COUNT(ASNs on the order) = 0
|
|
96
|
+
partiallyFulfilled WHEN NOT ( orderedSum <= shippedSum ) # order-level grand totals
|
|
78
97
|
fulfilled otherwise
|
|
79
98
|
```
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
Both the ordered and shipped sides are the legacy expressions, with all their known defects intact:
|
|
101
|
+
|
|
102
|
+
- the **ordered side filters `parentSalesOrderItemId IS NULL`** while the shipped side does not, so
|
|
103
|
+
the two sides never count the same lines. USA has **62,865** child lines, so this is widespread.
|
|
104
|
+
Canada's `SAC100664` is the minimal reproduction.
|
|
105
|
+
- the **ordered side explicitly INCLUDES lines with no PO link**, i.e. it expects tax, freight and
|
|
106
|
+
service lines to ship — which is what makes the comparison unreachable for many orders.
|
|
107
|
+
- the **order-level TOGa Tech shipped sum is inflated by bridge fan-out**, because it correlates
|
|
108
|
+
nothing at item level: on `SA136116` the order-level chain returns **48** where a per-item chain
|
|
109
|
+
returns **32**.
|
|
110
|
+
|
|
111
|
+
These are recorded as known-wrong, not fixed. They are the case *for* a per-line rule; the ODP feed
|
|
112
|
+
is the case against it.
|
|
84
113
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
114
|
+
## The per-line rule that was built and rejected
|
|
115
|
+
|
|
116
|
+
Shape tested (kept the order total as an AND, exactly so the change could only ever downgrade):
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
partiallyFulfilled WHEN NOT ( legacyOrderTotalCondition ) OR ( any expected-to-ship line is short )
|
|
120
|
+
```
|
|
89
121
|
|
|
90
|
-
|
|
122
|
+
Measured on prod:
|
|
91
123
|
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
- **5,223 orders** would move fulfilled → partiallyFulfilled.
|
|
125
|
+
- **3,728 orders** were correctly held back **by keeping the order-total check as an AND** — without
|
|
126
|
+
it they would have been *promoted* to Fulfilled.
|
|
127
|
+
- **1,550 USA orders have ZERO expected-to-ship lines** (priced hardware with no PO-item link at
|
|
128
|
+
all). A pure per-line rule passes **vacuously** on those and returns fulfilled on nothing. Worked
|
|
129
|
+
example: `SA127967` has five priced hardware lines (`AW5M5UT-EW`, `S24D402GAN`, `HDMM6---ODP`,
|
|
130
|
+
`9VA80AA`, `9SR37UT`), none PO-linked.
|
|
131
|
+
|
|
132
|
+
**Decision (2026-08-27): reverted, not shipped.** The 5,223 cannot be trusted as corrections while
|
|
133
|
+
ODP duplicate ASNs make a line's shipped quantity unreliable. Two independent conclusions follow, and
|
|
134
|
+
both must survive any future attempt:
|
|
135
|
+
|
|
136
|
+
- **⚠ Any future USA per-line rule MUST keep an order-total guard** (or an equivalent non-vacuous
|
|
137
|
+
check), because of the 1,550 zero-expected-line orders.
|
|
138
|
+
- **⚠ Do NOT port Canada's clean per-line rule to USA.** Canada has no order-total condition at all;
|
|
139
|
+
on USA that omission alone promotes thousands of orders to a *new* false Fulfilled.
|
|
140
|
+
|
|
141
|
+
## Telling a shippable line from a service line (USA column choice)
|
|
94
142
|
|
|
95
143
|
**Neither `price > 0` nor `Items.inventoryType` separates shippable from non-shippable** — HYBRID
|
|
96
144
|
dominates both groups. Verified on prod: `PC-KIT-OPT2`, `Tech Support`, `LEASE REFRESH`,
|
|
@@ -98,43 +146,59 @@ dominates both groups. Verified on prod: `PC-KIT-OPT2`, `Tech Support`, `LEASE R
|
|
|
98
146
|
`CG-MEDKITTING-NWN-APPLE` and `ODP-COMPASS_MERAKI` have **zero** PO links, while real hardware does
|
|
99
147
|
(`C40QYUC`: 1,208 of 1,362 lines; `MD4A4LL/A-S`: 130 of 137).
|
|
100
148
|
|
|
101
|
-
|
|
102
|
-
|
|
149
|
+
USA fees are identifiable through **either** type column, and the two do not agree:
|
|
150
|
+
|
|
151
|
+
| Signal | Priced order lines | Lines ever on an ASN |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `AssetTypes.name = 'FEE'` | 90,857 | 732 |
|
|
154
|
+
| `ItemTypes.name = 'FEE'` | 89,921 | 70 |
|
|
155
|
+
|
|
156
|
+
**936 lines are assetType FEE but not itemType FEE.** The developer's decision is that **code reads
|
|
157
|
+
`ItemTypes` and lets `Items.isFulfillable` carry the assetType meaning**, because the planned data
|
|
158
|
+
fix folds both columns into the flag — see
|
|
159
|
+
[isFulfillable — Data Quality & the Type-Derived Rule](isfulfillable-data-quality-and-type-rule.md).
|
|
160
|
+
Note the risk that leaves: if the flag drifts back to 1, an itemType-only code test will not catch an
|
|
161
|
+
assetType-only FEE item.
|
|
162
|
+
|
|
163
|
+
> **The same `partNumber` can exist as several `Items` rows, some purchased and some not.** Any such
|
|
164
|
+
> test must be **per line**, never per part number.
|
|
103
165
|
|
|
104
166
|
## Gotchas / known issues
|
|
105
167
|
|
|
106
|
-
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
168
|
+
- **Duplicate ASNs over-credit a line — this is the blocker, not a footnote.** Confirmed on
|
|
169
|
+
`SA135272`: PO line 1 was credited twice (ordered 4 / shipped 8) while line 2 was never mentioned
|
|
170
|
+
(ordered 4 / shipped 0), and the order-total rule said fulfilled. Both ASN payloads referenced
|
|
171
|
+
`lineNumber 1`. Full analysis, the trend by month, and the payload evidence are in
|
|
172
|
+
[ASN → ItemFulfillment](asn-to-item-fulfillment.md#office-depot-sends-duplicate-asn-messages-measured-2026-08-27).
|
|
173
|
+
- **4.3% of USA ASN shipped quantity cannot be attributed to a sales-order line at all**: **5,536**
|
|
174
|
+
`AdvanceShippingNoticeItems` have a NULL `purchaseOrderItemId` and **12,163** more point at a
|
|
175
|
+
`purchaseOrderItemId` with no `SalesOrderItems_PurchaseOrderItems` link (**26,770 of 617,166
|
|
176
|
+
units**). Treat per-line shipped quantity as a floor, not a truth.
|
|
177
|
+
- **⚠ `self::` defeats the region seam.** `_Model_Compass_SalesOrder` calls **`self::_status()`** (not
|
|
178
|
+
`static::`) in its ApprovalDecision notification query, so that query always evaluates the parent
|
|
179
|
+
rule even for Canada orders.
|
|
180
|
+
- **`salesOrderStageId = 2` is a dormant, USA-impossible branch.** `_status` hardcodes stage 2 as a
|
|
181
|
+
recompute trigger, but USA's `SalesOrderStages` table contains **only ids 5 to 9** (Pending
|
|
182
|
+
Billing, Pending Billing/Partially Fulfilled, Billed, Canceled, Closed) — stage 2 does not exist
|
|
183
|
+
there, so setting it would be an orphan. Canada has all ten. No order in either client currently
|
|
184
|
+
uses stage 2.
|
|
185
|
+
|
|
186
|
+
## Verified clean (worth recording as negatives)
|
|
187
|
+
|
|
188
|
+
Audited across both Compass clients on 2026-08-27:
|
|
189
|
+
|
|
190
|
+
- every `salesOrderStageId` in use resolves to a real `SalesOrderStages` row — **no orphans**;
|
|
191
|
+
- the approval half is structurally sound too (one active template, no duplicate `Approval` rows or
|
|
192
|
+
decisions), so **`_status` cannot return NULL in either client today** — details in
|
|
193
|
+
[MR/MA Order Approval & Status Gate](mr-ma-order-approval-and-status.md).
|
|
194
|
+
|
|
195
|
+
Post-fulfillment tail (USA, explicit stage): **473** Pending Billing, **39,869** Billed, **1,592**
|
|
196
|
+
Canceled, **809** Closed — the lifecycle continues Fulfilled → Pending Billing → Billed via the
|
|
197
|
+
stored stage, not the computed status.
|
|
134
198
|
|
|
135
199
|
## Blast radius — fulfilled vs partiallyFulfilled is display-only
|
|
136
200
|
|
|
137
|
-
|
|
201
|
+
Re-confirmed 2026-08-27: **no cron, email or report reads the distinction.**
|
|
138
202
|
|
|
139
203
|
- **The status is computed live and never stored.** No backfill; reverting the code fully reverts the
|
|
140
204
|
behaviour.
|
|
@@ -143,39 +207,47 @@ Every consumer was audited before shipping, which is why this carries no downstr
|
|
|
143
207
|
`worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php` and
|
|
144
208
|
`compass/workflow/test_partial_in_transit_email.php` each keep their **own duplicated copy** of the
|
|
145
209
|
`_status` CASE, but those copies only ever produce canceled / pendingApprovalUnknown /
|
|
146
|
-
pendingFulfillment / shipped and they filter on `computedStatusSlug = 'shipped'`.
|
|
147
|
-
|
|
148
|
-
byte-identical. **That duplication is itself a divergence risk** — see the
|
|
210
|
+
pendingFulfillment / shipped and they filter on `computedStatusSlug = 'shipped'`. **That
|
|
211
|
+
duplication is itself a divergence risk** — see the
|
|
149
212
|
[in-transit email doc](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
|
|
150
213
|
- **The MA sales-order exception report**
|
|
151
214
|
(`compass/workflow/7_generate_ma_sales_order_exception_report.php`) filters the **stored**
|
|
152
215
|
`salesOrderStageId` column, not the computed status.
|
|
153
216
|
- **toga2-supply** sends only the approvals filter values (`pendingInitialApproval` /
|
|
154
217
|
`pendingApproval`) to the server; `fulfilled` and `partiallyFulfilled` appear only in
|
|
155
|
-
`FILTERFIELDS.ts`
|
|
156
|
-
- **⚠ One call site always uses the parent rule.** `_Model_Compass_SalesOrder` calls
|
|
157
|
-
**`self::_status()`** (not `static::`) in its ApprovalDecision notification query, so that query
|
|
158
|
-
never picks up a region override. Harmless today — it only branches on pendingApproval / canceled /
|
|
159
|
-
pendingFulfillment, and the pendingFulfillment gate is byte-identical across both regions — but it
|
|
160
|
-
is a live trap if a region ever changes those three.
|
|
218
|
+
`FILTERFIELDS.ts` and `renderBadge.tsx`.
|
|
161
219
|
|
|
162
220
|
## Change history
|
|
163
|
-
- 2026-08-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
221
|
+
- 2026-08-27 — **Corrects the 2026-08-26 entry below: no USA rule change ever shipped, and the
|
|
222
|
+
"Fulfilled fell by exactly 5,234" figure is void.** Commit `90c964f1` was reverted by `43049d1d`
|
|
223
|
+
(2026-08-26), and this session **deliberately kept USA unchanged**: `_fulfillmentStatus()` holds the
|
|
224
|
+
original order-total logic verbatim and the seam exists only for Canada's override (emitted `_status`
|
|
225
|
+
SQL whitespace-identical, 7,548 chars). A per-line USA rule was built and measured (**5,223** orders
|
|
226
|
+
would move fulfilled → partiallyFulfilled; **3,728** correctly held back by keeping the order total
|
|
227
|
+
as an AND) then reverted, because the **Office Depot ASN feed is not trustworthy at line level** —
|
|
228
|
+
10,012 ODP PO lines are credited over quantity by duplicate notices against only 8 genuine split
|
|
229
|
+
shipments. Recorded the two constraints on any future attempt: **1,550 USA orders have zero
|
|
230
|
+
expected-to-ship lines** (so a pure per-line rule passes vacuously) and Canada's rule must not be
|
|
231
|
+
ported. Also recorded the full `_status` gate order and that gate 3 (approvals) strands **11,341**
|
|
232
|
+
USA orders before fulfillment is ever evaluated, the USA FEE column choice (`ItemTypes` in code,
|
|
233
|
+
936 lines are assetType-FEE only), the dormant `salesOrderStageId = 2` branch that cannot exist in
|
|
234
|
+
USA, and the clean-audit negatives (`_status` cannot return NULL in either client).
|
|
235
|
+
Uncommitted; on `_production`; not exercised through the Supply UI. (bala)
|
|
236
|
+
- 2026-08-26 — **REVERTED IN GIT — do not rely on this entry.** Reported USA as fixed by requiring the
|
|
237
|
+
legacy order-level aggregate **AND** no short purchased line, with helpers `_isPurchasedLine()`,
|
|
238
|
+
`_qtyShippedByAsn()`, `_qtyShippedByTogaTech()`, and claimed prod impact "Fulfilled 26,856 → 21,622
|
|
239
|
+
(down 5,234)". Commit `90c964f1` was reverted by `43049d1d`; the refinements after it were never
|
|
240
|
+
committed. Superseded by the 2026-08-27 entry. Still valid from this work: the false-Fulfilled root
|
|
241
|
+
cause, the shippable-line test (PO link, not price or `inventoryType`), the ASN attribution limits,
|
|
242
|
+
and that a clean per-line port of Canada's rule is wrong here. (bala)
|
|
173
243
|
|
|
174
244
|
## Related docs
|
|
175
245
|
- [Compass Canada — per-line, ASN-only rule](../../compass-canada/features/order-fulfillment-status-per-line.md)
|
|
246
|
+
— the region that did change, and why its rule is safe there.
|
|
247
|
+
- [Compass ASN → ItemFulfillment](asn-to-item-fulfillment.md) — the ODP duplicate-ASN evidence that
|
|
248
|
+
blocks the USA change.
|
|
249
|
+
- [Compass MR/MA Order Approval & Status Gate](mr-ma-order-approval-and-status.md) — the approval
|
|
250
|
+
gate that strands more USA orders than the fulfillment rule mislabels.
|
|
176
251
|
- [IF Stage Lifecycle & Order Status](../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md)
|
|
177
|
-
— the shipped-only stage policy that feeds both sides of this comparison.
|
|
178
252
|
- [FIELD_SQL calculated fields](../../../2.0/apps/_underscore/features/calculated-sql-fields.md)
|
|
179
253
|
— why a region-subclass override of a calculated field is picked up at all.
|
|
180
|
-
- [Compass MR/MA Order Approval & Status Gate](mr-ma-order-approval-and-status.md) — the approval
|
|
181
|
-
half of `_status()`, which this change did not touch.
|
|
@@ -165,15 +165,22 @@ separate, related client (see its own profile).
|
|
|
165
165
|
shared by Compass USA + Canada) counts **shipped only** — picked/packed never advance a Compass
|
|
166
166
|
order. Contrast Quad, which gets the full picked/packed machine. See
|
|
167
167
|
[IF stage lifecycle & order status](../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
|
|
168
|
-
-
|
|
169
|
-
order-level ordered-total vs shipped-total comparison
|
|
170
|
-
gap, so
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
168
|
+
- **⚠ Fulfilled vs Partially Fulfilled is still the old order-total rule, on purpose (2026-08-27).**
|
|
169
|
+
The order-level ordered-total vs shipped-total comparison lets one line's surplus hide another
|
|
170
|
+
line's gap, so USA orders read **Fulfilled** while partly shipped. A per-line fix was built,
|
|
171
|
+
measured (5,223 orders would move) and **deliberately reverted**, because Office Depot sends
|
|
172
|
+
**duplicate ASN messages** — 10,012 ODP PO lines are credited over quantity against only 8 genuine
|
|
173
|
+
split shipments — so line-level shipped quantity cannot be trusted yet. `_status()`'s fulfillment
|
|
174
|
+
branch does live in `_fulfillmentStatus()`, but **only Compass Canada overrides it**; the USA body
|
|
175
|
+
is the original logic verbatim. **⚠ Do not port Canada's clean per-line rule:** 1,550 USA orders
|
|
176
|
+
have no expected-to-ship line at all, so it passes vacuously and would create *new* false
|
|
177
|
+
Fulfilleds. Any earlier "5,234 orders fixed" note is void — that commit was reverted in git.
|
|
176
178
|
[Fulfilled vs Partially Fulfilled](features/order-fulfillment-status-per-line.md).
|
|
179
|
+
- **⚠ The approval gate strands far more orders than the fulfillment rule mislabels (2026-08-27).**
|
|
180
|
+
**11,341** Compass USA orders sit at a pending-approval status and never reach the fulfillment
|
|
181
|
+
branch at all (Office Depot 9,926, Compass 950, Agilant 465), growing 1,200–2,000 a month. MR is
|
|
182
|
+
clean; a **live Agilant Drop Ship spike** started 2026-08-11.
|
|
183
|
+
[MR/MA Order Approval & Status Gate](features/mr-ma-order-approval-and-status.md).
|
|
177
184
|
- Built on the shared 2.0 Recursive Item Fulfillments engine (upstream mirroring).
|
|
178
185
|
- **`Items.isFulfillable` gates the storefront Qty Fulfilled cell (2026-07).** Sourced from
|
|
179
186
|
NetSuite onto the Agilant source item during the 1.0 item sync
|
package/package.json
CHANGED