toga-ai 1.0.658 → 1.0.660
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/1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md +19 -1
- package/knowledge/2.0/apps/_underscore/INDEX.md +2 -2
- package/knowledge/2.0/apps/_underscore/features/calculated-sql-fields.md +45 -0
- package/knowledge/2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md +37 -3
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +11 -1
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +57 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +74 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-canada/INDEX.md +1 -0
- package/knowledge/clients/compass-canada/features/order-fulfillment-status-per-line.md +130 -0
- package/knowledge/clients/compass-canada/profile.md +13 -3
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/mr-ma-order-approval-and-status.md +11 -4
- package/knowledge/clients/compass-usa/features/order-fulfillment-status-per-line.md +181 -0
- package/knowledge/clients/compass-usa/profile.md +10 -0
- package/knowledge/clients/elite/profile.md +2 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/compass/update_salesorder_status_from_odp.php
|
|
@@ -19,6 +19,8 @@ files:
|
|
|
19
19
|
- library/app/client/compasscanada.php
|
|
20
20
|
related:
|
|
21
21
|
- ../../../clients/compass-canada/features/french-order-email-localization.md
|
|
22
|
+
- ../../../clients/compass-usa/features/order-fulfillment-status-per-line.md
|
|
23
|
+
- ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
22
24
|
---
|
|
23
25
|
|
|
24
26
|
## Summary
|
|
@@ -134,6 +136,17 @@ the tracking number as emailed (it retries next run).
|
|
|
134
136
|
- **Pick the template from the language of the person actually being emailed.** The manager-approval
|
|
135
137
|
reminder cron (`compass_email_reminders.php`) selected the template from the **requester's**
|
|
136
138
|
language even though the mail goes to the **manager**. Fixed 2026-08-03.
|
|
139
|
+
- **⚠ Each cron keeps its OWN duplicated copy of the 2.0 `_status` CASE — a standing divergence
|
|
140
|
+
risk.** `compass/update_salesorder_status_from_odp.php`,
|
|
141
|
+
`compasscanada/update_salesorder_status_from_odp.php` and
|
|
142
|
+
`compass/workflow/test_partial_in_transit_email.php` each re-implement the model's status SQL as
|
|
143
|
+
`computedStatusSlug`. Today that is safe by luck of scope: the copies only ever produce canceled /
|
|
144
|
+
pendingApprovalUnknown / pendingFulfillment / shipped and they filter on
|
|
145
|
+
`computedStatusSlug = 'shipped'`, so the 2026-08-26 rewrite of the fulfilled-vs-partiallyFulfilled
|
|
146
|
+
rule (which left the "has an ASN" pendingFulfillment gate byte-identical) did not touch them. **Any
|
|
147
|
+
future change to the pendingFulfillment or canceled branches of
|
|
148
|
+
`_Model_Compass_SalesOrder::_status()` must be mirrored into all three crons by hand.** See
|
|
149
|
+
[Compass Fulfilled vs Partially Fulfilled](../../../clients/compass-usa/features/order-fulfillment-status-per-line.md).
|
|
137
150
|
- **⚠ Conflict with the new 2.0 centralized tracking-status refresh.** The delivered-email cron
|
|
138
151
|
(`worker/crons/toga2/compass/send_delivered_email.php`) polls UPS itself, sends the delivered
|
|
139
152
|
template (uuid `fe961c7c-277b-4a89-be1d-3d318d8c7718`) via api2 `GET /email-templates/sendEmail`,
|
|
@@ -148,6 +161,11 @@ the tracking number as emailed (it retries next run).
|
|
|
148
161
|
|
|
149
162
|
## Change history
|
|
150
163
|
|
|
164
|
+
- 2026-08-26 — Flagged the **duplicated `_status` CASE** in the three crons as a standing divergence
|
|
165
|
+
risk after the 2.0 Compass fulfillment rule was rewritten. Verified the crons are unaffected: their
|
|
166
|
+
copies only emit canceled / pendingApprovalUnknown / pendingFulfillment / shipped and filter on
|
|
167
|
+
`shipped`, and the pendingFulfillment gate was left byte-identical. No code change here. (bala)
|
|
168
|
+
|
|
151
169
|
- 2026-08-24 — Recorded the **conflict with the new 2.0 platform-wide tracking-status refresh**: that
|
|
152
170
|
refresh writes `TrackingNumbers.status` via api2 PUT for all clients, and this cron's delivered
|
|
153
171
|
UPDATE is guarded by `WHERE status <> 'DELIVERED'` — so if the refresh marks a Compass row
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php |
|
|
11
11
|
| [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
|
|
12
12
|
| [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
|
|
13
|
-
| [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent | _underscore/Model.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/ServiceRequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Quad/Item.php, _underscore/Model/Quad/VendorItem.php, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
13
|
+
| [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent | _underscore/Model.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/ServiceRequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, api2/Component/Api/V2/V2.php, _underscore/Model/Quad/Item.php, _underscore/Model/Quad/VendorItem.php, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
14
14
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | test/@Mark/true-80824-fedex-inflate-test.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
15
15
|
| [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | A throwaway CLI script (a data check, a backfill dry-run, a render harness) that wants the real 2.0 framework — `_Model`, `_Query`, `_Database` — is **not** the | _underscore/Database.php, _underscore/Environment.php, api2/Initialize.php |
|
|
16
16
|
| [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
| [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 |
|
|
26
26
|
| [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 |
|
|
27
27
|
| [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 |
|
|
28
|
-
| [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/Elite/SalesOrder.php, _underscore/Model/Elite/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
|
+
| [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/Canada/SalesOrder.php, _underscore/Model/Compass/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/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 |
|
|
29
29
|
| [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 |
|
|
30
30
|
| [_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 |
|
|
31
31
|
| [_Model::save() parent FK cascade — stored-SQL-field recompute deadlocks](features/model-save-parent-cascade-stored-field-deadlock.md) | `_Model::save()` runs a **generic parent foreign-key cascade**: inserting (or saving) a child row that carries an FK to a parent causes `_Model` to **re-load an | _underscore/Model.php, _underscore/Model/Client/PurchaseOrder.php, _underscore/Model/Client/AdvanceShippingNotice.php |
|
|
@@ -13,12 +13,17 @@ files:
|
|
|
13
13
|
- _underscore/Model/Client/SalesOrder.php
|
|
14
14
|
- _underscore/Model/Client/ServiceRequest.php
|
|
15
15
|
- _underscore/Model/Elite/SalesOrder.php
|
|
16
|
+
- _underscore/Model/Compass/SalesOrder.php
|
|
17
|
+
- _underscore/Model/Compass/Canada/SalesOrder.php
|
|
18
|
+
- api2/Component/Api/V2/V2.php
|
|
16
19
|
- _underscore/Model/Quad/Item.php
|
|
17
20
|
- _underscore/Model/Quad/VendorItem.php
|
|
18
21
|
- dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql
|
|
19
22
|
- dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql
|
|
20
23
|
related:
|
|
21
24
|
- ../architecture.md
|
|
25
|
+
- ./item-fulfillment-stage-lifecycle-and-order-status.md
|
|
26
|
+
- ../../../clients/compass-usa/features/order-fulfillment-status-per-line.md
|
|
22
27
|
- ./acl-permission-chain.md
|
|
23
28
|
- ../../api2/features/v2-api-error-codes.md
|
|
24
29
|
- ./model-magic-field-access.md
|
|
@@ -136,6 +141,38 @@ The ACL convention for these rows: grants go to the **`Base` and `API` roles onl
|
|
|
136
141
|
NAME**. Most other `SalesOrders` custom fields carry `Base` alone, so `Base + API` is already the
|
|
137
142
|
generous end; `Developer` and `Public` get no ACL row for any field.
|
|
138
143
|
|
|
144
|
+
## Overriding a calculated field per client (and per sub-client)
|
|
145
|
+
|
|
146
|
+
**A client subclass override of a calculated field IS picked up, including a sub-client one.** Two
|
|
147
|
+
mechanisms have to line up, and both do:
|
|
148
|
+
|
|
149
|
+
1. **api2 resolves the model class from `Core.Clients.clientIdentifier`** with
|
|
150
|
+
`str_replace('_Model_Client_', '_Model_' . $clientIdentifier . '_', get_class($model))`
|
|
151
|
+
(`api2/Component/Api/V2/V2.php`, ~10 call sites), falling back to the `_Model_Client_*` class when
|
|
152
|
+
the candidate does not exist. So `Compass_Usa` (id 2) and `Compass_Canada` (id 43) resolve to
|
|
153
|
+
`_Model_Compass_Usa_SalesOrder` / `_Model_Compass_Canada_SalesOrder`.
|
|
154
|
+
2. **The field expression is invoked late-bound** — `get_called_class()::$field($this::TABLE)` in
|
|
155
|
+
`Model.php` (`getSqlFieldValue` / `buildSqlFieldValue`) — so whichever class api2 resolved supplies
|
|
156
|
+
the SQL.
|
|
157
|
+
|
|
158
|
+
**The safe way to give one region its own behaviour is to split the field's body into a
|
|
159
|
+
`protected static` helper and call it with `static::`.** The region subclass then overrides only the
|
|
160
|
+
helper, leaving the shared parts of the expression in one place. This is how
|
|
161
|
+
`_Model_Compass_SalesOrder::_status()` isolates its fulfillment branch into
|
|
162
|
+
`_fulfillmentStatus()` so Compass Canada can change the fulfilled-vs-partial rule with provably zero
|
|
163
|
+
effect on Compass USA (see
|
|
164
|
+
[Compass fulfillment status](../../../clients/compass-usa/features/order-fulfillment-status-per-line.md)).
|
|
165
|
+
|
|
166
|
+
**Prove an extraction is behaviour-preserving by diffing the emitted SQL**, not by reading the diff:
|
|
167
|
+
simulate the PHP string concatenation for both versions, normalize whitespace, and compare lengths
|
|
168
|
+
and content. The `_status` extraction above came out whitespace-identical at 7,548 normalized
|
|
169
|
+
characters — that is the standard of evidence for touching a field that runs on 112k live rows.
|
|
170
|
+
|
|
171
|
+
> **⚠ `self::` defeats the whole mechanism.** A *different* method on the parent that calls
|
|
172
|
+
> `self::_status()` (rather than `static::_status()`) always evaluates the **parent's** expression,
|
|
173
|
+
> even for a client whose subclass overrides it. `_Model_Compass_SalesOrder` has exactly this in its
|
|
174
|
+
> ApprovalDecision notification query. Late static binding only happens with `static::`.
|
|
175
|
+
|
|
139
176
|
## Gotchas / known issues
|
|
140
177
|
|
|
141
178
|
- **⚠ Never add a parameter TYPE to an override whose parent declares the parameter untyped - it
|
|
@@ -173,6 +210,14 @@ generous end; `Developer` and `Public` get no ACL row for any field.
|
|
|
173
210
|
[save-cascade stored-field deadlocks](./model-save-parent-cascade-stored-field-deadlock.md).
|
|
174
211
|
|
|
175
212
|
## Change history
|
|
213
|
+
- 2026-08-26 - Documented **how to override a calculated field for one client / sub-client**: api2
|
|
214
|
+
resolves the model class by `str_replace('_Model_Client_', '_Model_' . $clientIdentifier . '_', ...)`
|
|
215
|
+
(`Component/Api/V2/V2.php`), and because the expression is invoked as
|
|
216
|
+
`get_called_class()::$field(...)` the subclass version wins - so `Compass_Usa` / `Compass_Canada`
|
|
217
|
+
each get their own SQL. Added the recommended shape (split the varying branch into a
|
|
218
|
+
`protected static` helper called via **`static::`**), the emitted-SQL diff as the standard of proof
|
|
219
|
+
for such an extraction, and the **`self::` trap** (another parent method calling `self::_status()`
|
|
220
|
+
never sees a subclass override - live example in `_Model_Compass_SalesOrder`). (bala)
|
|
176
221
|
- 2026-08-26 - **`FIELDOPT_SQL_TYPE` casts the returned VALUE, not just sort/pagination cursors.**
|
|
177
222
|
`getSqlFieldValue()` (`Model.php` ~L509-520) hands the fetched value to `formatValueForField()`
|
|
178
223
|
(~L728), which `(float)`s a `FIELD_DECIMAL` field - so a text-returning calculated field declared
|
package/knowledge/2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
CHANGED
|
@@ -6,12 +6,13 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: [bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/SalesOrder.php
|
|
13
13
|
- _underscore/Model/Quad/SalesOrder.php
|
|
14
14
|
- _underscore/Model/Compass/SalesOrder.php
|
|
15
|
+
- _underscore/Model/Compass/Canada/SalesOrder.php
|
|
15
16
|
- _underscore/Model/Compass/SalesOrderStatus.php
|
|
16
17
|
- _underscore/Model/Elite/SalesOrder.php
|
|
17
18
|
- _underscore/Model/Elite/SalesOrderStatus.php
|
|
@@ -29,6 +30,8 @@ related:
|
|
|
29
30
|
- ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
30
31
|
- ../../../clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md
|
|
31
32
|
- ../../../clients/compass-usa/features/asn-to-item-fulfillment.md
|
|
33
|
+
- ../../../clients/compass-usa/features/order-fulfillment-status-per-line.md
|
|
34
|
+
- ../../../clients/compass-canada/features/order-fulfillment-status-per-line.md
|
|
32
35
|
---
|
|
33
36
|
|
|
34
37
|
## Summary
|
|
@@ -43,7 +46,11 @@ This makes the *picked/packed* portion of the fulfillment lifecycle visible (pre
|
|
|
43
46
|
Partially Fulfilled → Fulfilled — so non-Compass clients see the picked/packed lifecycle.
|
|
44
47
|
- **Compass USA + Compass Canada:** all IF stages are *imported*, but order status counts
|
|
45
48
|
**shipped only** — picked/packed never advance a Compass order's status. Compass walks
|
|
46
|
-
Pending → Partially Fulfilled → Fulfilled by shipped quantity alone.
|
|
49
|
+
Pending → Partially Fulfilled → Fulfilled by shipped quantity alone. **The shipped-only *stage*
|
|
50
|
+
policy is shared, but since 2026-08-26 the fulfilled-vs-partiallyFulfilled *rule* is per region:**
|
|
51
|
+
the fulfillment branch lives in `_fulfillmentStatus()` and Canada overrides it. See
|
|
52
|
+
[Compass USA](../../../clients/compass-usa/features/order-fulfillment-status-per-line.md) and
|
|
53
|
+
[Compass Canada](../../../clients/compass-canada/features/order-fulfillment-status-per-line.md).
|
|
47
54
|
|
|
48
55
|
Independently of order status, **inventory movement (`qtyFulfilled` / `qtyCommitted`) is
|
|
49
56
|
shipped-only for ALL clients** — picked/packed quantities never count as inventory movement.
|
|
@@ -59,6 +66,11 @@ shipped-only for ALL clients** — picked/packed quantities never count as inven
|
|
|
59
66
|
- **`Model/Compass/SalesOrder.php`** — Compass `_status`, shipped-only: both the existence
|
|
60
67
|
`COUNT` and the shipped-qty `SUM` subqueries `INNER JOIN ItemFulfillmentStages →
|
|
61
68
|
ItemFulfillmentStatuses` filtered to the shipped status slug. Shared by Compass USA + Canada.
|
|
69
|
+
Its **fulfillment half is a separate `protected static _fulfillmentStatus()`** called via
|
|
70
|
+
`static::` — that is the per-region seam (see the Compass docs above). The approval gate stays in
|
|
71
|
+
`_status()` and is region-independent.
|
|
72
|
+
- **`Model/Compass/Canada/SalesOrder.php`** — Canada's `_fulfillmentStatus()` override (per-line,
|
|
73
|
+
ASN-bridge only).
|
|
62
74
|
- **`Model/Compass/SalesOrderStatus.php`** — adds `const SLUG__PICKED = 'picked'` and
|
|
63
75
|
`SLUG__PACKED = 'packed'`.
|
|
64
76
|
- **`Model/Client/{SalesOrderItem,Item,PurchaseOrderItem}.php`** — the `_qtyFulfilled` /
|
|
@@ -98,7 +110,7 @@ fulfillment statuses — filter widened to `['_picked','_packed','_shipped']`:
|
|
|
98
110
|
| Client | Statuses walked | Driven by |
|
|
99
111
|
|---|---|---|
|
|
100
112
|
| Base, Quad (non-SA) | Pending → Picked → Packed → Partially Fulfilled → Fulfilled | all IF stages |
|
|
101
|
-
| Compass USA + Canada | Pending → Partially Fulfilled → Fulfilled | **shipped IF stage only
|
|
113
|
+
| Compass USA + Canada | Pending → Partially Fulfilled → Fulfilled | **shipped IF stage only**; fulfilled-vs-partial rule differs per region (`_fulfillmentStatus()`) |
|
|
102
114
|
| Elite | pendingFulfillment → partiallyFulfilled → fulfilled → shipped → delivered (+ canceled) | fulfillment + shipment state; **no approval stage** |
|
|
103
115
|
|
|
104
116
|
Quad `SA%` orders delegate to the base machine; an explicit `salesOrderStageId` overrides the
|
|
@@ -163,11 +175,29 @@ Fan-out (`Client/`, every client) + a Compass-Canada-specific set. Execution is
|
|
|
163
175
|
IFs changes a Compass order's status — by design it does not (the Compass supply UI must show
|
|
164
176
|
shipped-only progression). This supersedes the old "stages 1/2 are unused in prod" assumption in
|
|
165
177
|
the Compass order-lifecycle workflow.
|
|
178
|
+
- **⚠ An order-level "ordered total vs shipped total" comparison is not a fulfillment test.** A
|
|
179
|
+
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 exactly what the Compass `_status` did until
|
|
181
|
+
2026-08-26 (its two sides did not even count the same lines — the ordered side filtered
|
|
182
|
+
`parentSalesOrderItemId IS NULL`, the shipped side did not), and it mislabeled **5,234** USA and
|
|
183
|
+
**115** Canada orders on prod. Any client `_status` that judges completeness must compare **per
|
|
184
|
+
line**. See the two Compass docs linked above.
|
|
185
|
+
- **A `self::`-qualified call to a calculated field inside the same model always uses the parent's
|
|
186
|
+
logic.** `_Model_Compass_SalesOrder` calls `self::_status()` in its ApprovalDecision notification
|
|
187
|
+
query, so that one call site never sees a region subclass override. Use `static::` unless you
|
|
188
|
+
specifically want the parent.
|
|
166
189
|
- The base/Compass picked check is a string literal `'picked'` (no `SLUG__PICKED` on
|
|
167
190
|
`_Model_Client_ItemFulfillmentStatus`); only the shipped check uses a constant. Adding a base IF
|
|
168
191
|
picked/packed constant would let the literal be removed.
|
|
169
192
|
|
|
170
193
|
## Change history
|
|
194
|
+
- 2026-08-26 — Recorded that the Compass **fulfilled-vs-partiallyFulfilled** rule is now **per
|
|
195
|
+
region**: the fulfillment branch of `_Model_Compass_SalesOrder::_status()` was extracted into a
|
|
196
|
+
`protected static _fulfillmentStatus()` invoked via `static::` (approval gate untouched, emitted
|
|
197
|
+
SQL proved identical), and `_Model_Compass_Canada_SalesOrder` overrides it. The shipped-only
|
|
198
|
+
**stage** policy is unchanged for both regions. Added the general gotcha that an order-level
|
|
199
|
+
ordered-total vs shipped-total comparison is not a fulfillment test, and the `self::` vs `static::`
|
|
200
|
+
trap. (bala)
|
|
171
201
|
- 2026-08-18 - Added **Elite** as a third order-status policy: `_Model_Elite_SalesOrder::_status`
|
|
172
202
|
walks a six-status ladder (pendingFulfillment / partiallyFulfilled / fulfilled / shipped /
|
|
173
203
|
delivered / canceled) with four private helper builders and two new `SalesOrderStatuses` rows
|
|
@@ -193,3 +223,7 @@ Fan-out (`Client/`, every client) + a Compass-Canada-specific set. Execution is
|
|
|
193
223
|
— the stage-3-Shipped invariant updated by this work.
|
|
194
224
|
- [Compass ASN → ItemFulfillment Auto-Creation](../../../clients/compass-usa/features/asn-to-item-fulfillment.md)
|
|
195
225
|
— ASN-created IFs now resolve the shipped stage.
|
|
226
|
+
- [Compass USA — Fulfilled vs Partially Fulfilled](../../../clients/compass-usa/features/order-fulfillment-status-per-line.md)
|
|
227
|
+
— the per-region fulfillment rule (order aggregate AND per-line).
|
|
228
|
+
- [Compass Canada — Fulfilled vs Partially Fulfilled](../../../clients/compass-canada/features/order-fulfillment-status-per-line.md)
|
|
229
|
+
— Canada's per-line, ASN-bridge-only override.
|
|
@@ -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-26
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/Language.php
|
|
@@ -79,6 +79,12 @@ with `const DATABASE`/`TABLE`, typed public-prop fields, and array FKs declaring
|
|
|
79
79
|
`FIELDOPT_FOREIGNKEY_MODEL` (cross-DB FKs are soft — no SQL constraint). Modeled on
|
|
80
80
|
`RecordField.php`/`TableView.php`.
|
|
81
81
|
|
|
82
|
+
**That Core/Client split is a CLUSTER boundary, not just a schema one.** `SurfaceOverrides` lives in
|
|
83
|
+
`DB_CLIENT` (there is no `Model/Core/SurfaceOverride.php`), so a question like "how many clients
|
|
84
|
+
override this element?" **cannot be answered with a join** on prod — resolve the element ids in Core
|
|
85
|
+
first, then count per client DB with those ids as literals. Same rule that forces client override
|
|
86
|
+
seeds to inline Core ids (below).
|
|
87
|
+
|
|
82
88
|
## How `resolve()` works
|
|
83
89
|
|
|
84
90
|
Inputs come from `$api` (the same accessor `_Model_Client_TableView::meta` uses):
|
|
@@ -606,6 +612,10 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
|
|
|
606
612
|
match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
|
|
607
613
|
|
|
608
614
|
## Change history
|
|
615
|
+
- 2026-08-26 — Recorded that the Core/Client model split is a **cluster** boundary: `SurfaceOverrides`
|
|
616
|
+
is a CLIENT-DB model, so override counts across clients must be a two-step (Core ids → per-client
|
|
617
|
+
count with literal ids), never a join. Matters when deciding whether to fork or share a surface,
|
|
618
|
+
since a fork starts with **zero** overrides. (apeterson)
|
|
609
619
|
- 2026-08-25 — Fixed a **whole-platform gap**: Surface message translations were inert for **every
|
|
610
620
|
API-authenticated request, for every client**. `_resolveLanguageId` returned `0` when the JWT carried
|
|
611
621
|
neither `id.client.languageId` nor `id.language`, and `0` makes `_loadMessages` skip the
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
|
|
6
6
|
| [Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql |
|
|
7
|
-
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql |
|
|
7
|
+
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql |
|
|
8
8
|
| [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
|
|
9
9
|
| [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
|
|
10
10
|
| [Local vs prod MySQL config parity — why “it passed locally” is not evidence](workflows/local-vs-prod-mysql-config-parity.md) | Several migration failures that look like "prod-only bugs" are actually **per-machine MySQL server-configuration differences**. | |
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql
|
|
@@ -86,6 +86,8 @@ files:
|
|
|
86
86
|
- dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql
|
|
87
87
|
- dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql
|
|
88
88
|
- dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql
|
|
89
|
+
- dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql
|
|
90
|
+
- dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql
|
|
89
91
|
related:
|
|
90
92
|
- ../../_underscore/features/surface-resolver.md
|
|
91
93
|
---
|
|
@@ -730,7 +732,61 @@ rule resumes.
|
|
|
730
732
|
Element 32 is likewise `isEnabled=0` (appears but disabled until an `IS_ENABLED`/`ENABLED_RULE`
|
|
731
733
|
override is added). **Open follow-up.**
|
|
732
734
|
|
|
735
|
+
## Forking a surface — Service Request record surfaces (Core 45/46, 2026-08-26)
|
|
736
|
+
|
|
737
|
+
The Service Request modal needed header chrome and a details section that **diverge** from the sales
|
|
738
|
+
order's, so two surfaces were forked (not the whole set — `Surfaces` are per-region, so forking one
|
|
739
|
+
region is the right granularity).
|
|
740
|
+
|
|
741
|
+
| File | Ids | Forked from |
|
|
742
|
+
|---|---|---|
|
|
743
|
+
| `Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql` | `Surfaces` **45** (`service-request-record-header`), `SurfaceElements` **161–165**, `recordId` **35** (`Records` `service-requests`, hardcoded) | `sales-order-record-header` (Surfaces 11 / elements 25–28) |
|
|
744
|
+
| `Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql` | `Surfaces` **46** (`service-request-details`), `SurfaceElements` **166–169** | the `order-details` section |
|
|
745
|
+
|
|
746
|
+
The header adds a modal tag reading "Service Request" (new `Core.Messages` key
|
|
747
|
+
`serviceRequest.header.tag`) and a new **Type** FIELD element (165) between order date and status
|
|
748
|
+
badge. Element **164**'s `labelTemplate` still reads `'Order {order.number}'` — **intentional**,
|
|
749
|
+
confirmed by the developer; do not "fix" it.
|
|
750
|
+
|
|
751
|
+
The details surface adds Type and drops Cost Center: Customer (`contact._name`), PO Number
|
|
752
|
+
(`_purchaseOrders`), Type (`serviceRequest.serviceRequestType.name`), Description (`description`).
|
|
753
|
+
A **Provider** field was requested but **not seeded** — no `provider` field exists on
|
|
754
|
+
`_Model_Client_SalesOrder` or `_Model_Client_ServiceRequest` and nothing in `toga25-supply`
|
|
755
|
+
references one. Still unresolved.
|
|
756
|
+
|
|
757
|
+
> Valuekey trap: section elements use **bare** keys while header elements use `order.`-prefixed keys,
|
|
758
|
+
> and the SR modal actually fetches the **linked Sales Order** — see
|
|
759
|
+
> [surface-frontend](../../toga25-supply/features/surface-frontend.md#gotchas).
|
|
760
|
+
|
|
761
|
+
### 🚨 Forking a surface silently DROPS every client override of the original
|
|
762
|
+
`SurfaceOverrides` rows are keyed **per surface element**, so a fork starts with zero overrides — every
|
|
763
|
+
per-client customization of the original section is silently gone on the new surface. **The fork-vs-share
|
|
764
|
+
decision should be driven by an override count**, taken before writing the seed.
|
|
765
|
+
|
|
766
|
+
Counting them is **not a join**: `Surfaces`/`SurfaceElements` are **Core** models
|
|
767
|
+
(`Model/Core/Surface.php`, `Model/Core/SurfaceElement.php` → `DB_CORE`) but `SurfaceOverrides` is a
|
|
768
|
+
**Client** model (`Model/Client/SurfaceOverride.php` → `DB_CLIENT`; there is no
|
|
769
|
+
`Model/Core/SurfaceOverride.php`). "How many clients override this element?" spans the prod-core and
|
|
770
|
+
prod-client **clusters**, so it must be two steps: resolve the element ids in Core, then count per
|
|
771
|
+
client DB using those ids as **literals**.
|
|
772
|
+
|
|
773
|
+
### `Surfaces`/`SurfaceElements` ids are per-ENVIRONMENT AUTO_INCREMENT — re-verify before every run
|
|
774
|
+
House style seeds these with literal ids, but they are **not** among the two team-maintained-id tables
|
|
775
|
+
(`Records`/`RecordFields`), so the next free value **can differ per environment** and must be
|
|
776
|
+
re-verified before a migration runs anywhere new. On 2026-08-26 local and production happened to agree
|
|
777
|
+
exactly (`MAX(Surfaces.id)` 44, `MAX(SurfaceElements.id)` 160) — that was **verified, not assumed**.
|
|
778
|
+
Using literals is still correct: a collision then fails loudly on the primary key instead of silently
|
|
779
|
+
mis-wiring (which is what `INSERT IGNORE` did in the 2026-08-24 incident above).
|
|
780
|
+
|
|
733
781
|
## Change history
|
|
782
|
+
- 2026-08-26 — Seeded the **Service Request** record surfaces as a **fork** of the sales-order ones:
|
|
783
|
+
`Core/2026-08-26a` (Surfaces **45** / elements **161–165**, recordId 35) and `Core/2026-08-26b`
|
|
784
|
+
(Surfaces **46** / elements **166–169**). Recorded three durable rules: forking a surface **drops
|
|
785
|
+
every client `SurfaceOverride`** of the original, so fork-vs-share should be decided by an override
|
|
786
|
+
count; that count **cannot be a join** because `SurfaceOverrides` is a **CLIENT**-DB model while
|
|
787
|
+
`Surfaces`/`SurfaceElements` are **CORE**; and `Surfaces`/`SurfaceElements` ids are per-environment
|
|
788
|
+
`AUTO_INCREMENT` (not team-maintained), so the free block must be re-verified per environment —
|
|
789
|
+
local and prod agreed at 44/160 this time, verified not assumed. (apeterson)
|
|
734
790
|
- 2026-08-25 — Added a **worked before/after cluster-isolation example** on a real client migration:
|
|
735
791
|
`Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql` joins **`Core.Messages` to
|
|
736
792
|
`Client_Quad.Languages`**, so it runs locally (all-in-one endpoint) and can **never** run on prod or
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | On large deployments the backend bumps the Core parameter `META_LAST_REFRESH_DATETIME`. | toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/App.tsx |
|
|
11
11
|
| [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
|
|
12
12
|
| [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts |
|
|
13
|
-
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/App.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
13
|
+
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/App.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
14
14
|
| [Talos Integration (AppLayout host + adapter wiring)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
|
|
15
15
|
| [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json |
|
|
16
16
|
| [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
|
|
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/App.tsx
|
|
@@ -17,6 +17,11 @@ files:
|
|
|
17
17
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx
|
|
18
18
|
- toga25-supply/src/surface/useFetchSurfaceMeta.ts
|
|
19
19
|
- toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
|
|
20
|
+
- toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx
|
|
21
|
+
- toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx
|
|
22
|
+
- toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx
|
|
23
|
+
- toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json
|
|
24
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx
|
|
20
25
|
- toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts
|
|
21
26
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx
|
|
22
27
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx
|
|
@@ -674,7 +679,75 @@ developer asked for the **whole component**, not just badges. The old
|
|
|
674
679
|
- **`tsc` not yet run** this session (the private `@agilant/toga-blox` registry needs npm creds);
|
|
675
680
|
treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
|
|
676
681
|
|
|
682
|
+
- **🚨 The Service Request modal renders the LINKED SALES ORDER, not the ServiceRequest record.**
|
|
683
|
+
`ServiceRequestRecordModalLayout.tsx` resolves the SR uuid to a SalesOrder uuid via
|
|
684
|
+
`SalesOrders.serviceRequestId` and fetches `/sales-orders/{uuid}`. So **any SR-specific field is one
|
|
685
|
+
hop further out**: the Type element was first seeded as `order.serviceRequestType.name`, which
|
|
686
|
+
resolves to nothing (SalesOrders has `serviceRequestId`, not `serviceRequestTypeId`) — the correct
|
|
687
|
+
path is `order.serviceRequest.serviceRequestType.name`. And **both halves are required**: the field
|
|
688
|
+
must ALSO be added to the modal's `apiFields.json` `fields` list or the API never returns it, and the
|
|
689
|
+
element renders blank with no error either way.
|
|
690
|
+
- **🚨 Header surfaces use `order.`-prefixed valueKeys; section surfaces use BARE keys.** The record
|
|
691
|
+
modal top bar builds its surface record as `{ order }`, so header elements read `order.dateOrder`,
|
|
692
|
+
`order._status`. Section surfaces (`order-details` etc.) resolve against the order **directly**, so
|
|
693
|
+
their keys are bare: `contact._name`, `_purchaseOrders`, `description`. The same field therefore has
|
|
694
|
+
two different valueKeys depending on which surface it lives in, and copying one into the other
|
|
695
|
+
renders blank with no error.
|
|
696
|
+
|
|
697
|
+
## Service Request record modal — forked from the SalesOrder modal (2026-08-26)
|
|
698
|
+
|
|
699
|
+
The Service Request (SR) modal was split off the SalesOrder record modal rather than being made a
|
|
700
|
+
mode of it. Three seams carry the fork; nothing else in the surface plumbing changed.
|
|
701
|
+
|
|
702
|
+
**1. Its own view model.** `useServiceRequestRecordModalLayoutModel.tsx` is a fork of
|
|
703
|
+
`useSalesOrderRecordModalViewModel` that keeps the record fetch, items, bundle items, invoices,
|
|
704
|
+
shipments, surface sections and contact-VIP, and **drops** `useFetchApprovalStages` + the approvals
|
|
705
|
+
surface gate, `usePurchaseOrderDetails` and the `meta.surface.poNumber.isComplete` derivation, and
|
|
706
|
+
assignee-VIP enrichment. It uses a distinct React Query cache scope **`"service-request-modal"`** so
|
|
707
|
+
it can never clobber the SO modal's `"record-modal"` scope — the two modals fetch the *same* record
|
|
708
|
+
shape, so a shared key would cross-contaminate.
|
|
709
|
+
|
|
710
|
+
**2. Its own view.** `ServiceRequestsView.tsx` drops `SalesOrderApprovalModalsLayout`,
|
|
711
|
+
`EnterPoNumberModal`, `AdminNotesSection`, the `approvalActive` gate and the approval/standard
|
|
712
|
+
summary-grid ternary.
|
|
713
|
+
|
|
714
|
+
**3. A parameterized slug map (the FE decides which tenantFields slot a section fills).**
|
|
715
|
+
`surfaceBundleToTenantFields(surfaces, slugToKey = SECTION_SLUG_TO_KEY)` now takes the map as an
|
|
716
|
+
argument. `SERVICE_REQUEST_SECTION_SLUG_TO_KEY` maps `service-request-details` → the **same**
|
|
717
|
+
`orderDetails` slot, so a forked surface renders in the existing `SalesOrderSummaryGrid` with no grid
|
|
718
|
+
change. `SalesOrderTopBar` gained an optional `headerSurfaceSlug` prop (default
|
|
719
|
+
`sales-order-record-header`) so the SR view passes `service-request-record-header`.
|
|
720
|
+
|
|
721
|
+
> Interim seam, deliberately. Extracting the top bar into a generic `RecordModalTopBar` was explicitly
|
|
722
|
+
> deferred by the developer — do not "clean this up" unprompted.
|
|
723
|
+
|
|
724
|
+
### Decision — FE-passed slug map, NOT a backend-declared `config.sectionKey`
|
|
725
|
+
Three options were weighed for how the FE maps a section surface to a tenantFields slot:
|
|
726
|
+
- **(a) derive the key from the slug** (kebab→camel) — rejected: breaks *silently* the moment slugs
|
|
727
|
+
diverge, which is exactly what forking does.
|
|
728
|
+
- **(b) key off `config.cardType`** — rejected: cardType is a *renderer* type shared by several
|
|
729
|
+
sections, so it cannot identify a slot.
|
|
730
|
+
- **(c) have each surface declare `config.sectionKey` and make the adapter generic** — drafted,
|
|
731
|
+
including a Core backfill migration for the 11 existing section surfaces, then **rejected by the
|
|
732
|
+
developer because of the backfill**. It remains the cleaner long-term design if the backfill is
|
|
733
|
+
ever accepted.
|
|
734
|
+
|
|
735
|
+
Final: the parameterized FE map above.
|
|
736
|
+
|
|
737
|
+
### Open — the SR modal still resolves `sales-order-record-actions`
|
|
738
|
+
`SurfaceActionBar` in the SR modal still resolves the sales-order actions surface, so every action
|
|
739
|
+
button except **View Log** renders and does nothing on click. Whether the SR modal gets its own
|
|
740
|
+
actions surface is **still an open decision**, not an oversight.
|
|
741
|
+
|
|
677
742
|
## Change history
|
|
743
|
+
- 2026-08-26 — Forked the **Service Request record modal** off the SalesOrder modal: own view model
|
|
744
|
+
(`"service-request-modal"` query scope; no approvals/PO-details/assignee-VIP), own view (no approval
|
|
745
|
+
modals, EnterPoNumberModal or AdminNotesSection), and a parameterized
|
|
746
|
+
`surfaceBundleToTenantFields(surfaces, slugToKey)` + `SalesOrderTopBar.headerSurfaceSlug` so forked
|
|
747
|
+
surfaces render in the existing grid. Recorded the decision to use an FE-passed slug map over a
|
|
748
|
+
backend `config.sectionKey` (backfill rejected), and two gotchas: the SR modal actually renders the
|
|
749
|
+
**linked Sales Order**, and header vs section surfaces use `order.`-prefixed vs **bare** valueKeys.
|
|
750
|
+
SR modal still resolves `sales-order-record-actions` — open. (apeterson)
|
|
678
751
|
- 2026-08-24 — **Corrected the surface-meta cache gotcha (supersedes the prior "a hard reload /
|
|
679
752
|
re-login is required" wording, which was wrong enough to misdirect debugging).** The React Query
|
|
680
753
|
cache is **persisted to `localStorage`**: `App.tsx` uses `PersistQueryClientProvider` +
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ —
|
|
21
|
+
- **_underscore** (_Underscore) _(framework core)_ — 69 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 57 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 11 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -5,5 +5,6 @@
|
|
|
5
5
|
| [French (fr-CA) Item Feature Translations (Compass Canada)](features/french-item-feature-translations.md) | 2.0 | Renders item **feature** text on the Compass Canada French storefront — feature names, feature-group headers (e.g. | _underscore/Model/Compass/Canada/Feature.php, _underscore/Model/Compass/Canada/ItemCategoryFeatureGroup.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, worker2/Worker/Etilize/ItemTranslations.php, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql, dbchanges2/Client_CompassCanada/2026-07-13 - FeatureAttributeCustomFields.sql, dbchanges2/Client_CompassCanada/2026-07-13 - DedupeItemFeaturesAndGroups.sql, dbchanges2/Client_CompassCanada/2026-07-13 - SeedFrenchFeatureTranslations.sql |
|
|
6
6
|
| [French (fr-CA) Order Email Localization (Compass Canada)](features/french-order-email-localization.md) | 2.0 | Compass Canada order emails (order requested, manager-approval request, approved/rejected, in-transit, delivered, reminders) are sent in **each recipient's own | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/ApprovalDecision.php, _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/String.php, library/app/client/compasscanada.php, worker/crons/toga2/compasscanada/send_delivered_email.php, worker/crons/toga2/compasscanada/compass_email_reminders.php, worker/crons/toga2/compasscanada/compass_cancel_pending_approval_orders.php, worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php, worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_email_previews_prod.php |
|
|
7
7
|
| [Grand & Toy ASN Import (Compass Canada)](features/grand-and-toy-asn-import.md) | 2.0 | Imports Grand & Toy (G&T) Advance Shipping Notices for Compass Canada. | api2/Component/Api/Cxml/Cxml.php, worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php, worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/Canada/AdvanceShippingNotice.php, dbchanges2/Client_CompassCanada/ |
|
|
8
|
+
| [Compass Canada — Fulfilled vs Partially Fulfilled (per-line, ASN bridge only)](features/order-fulfillment-status-per-line.md) | 2.0 | TOGa Supply showed **Fulfilled** on Compass Canada orders that were only partly shipped. | _underscore/Model/Compass/Canada/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php |
|
|
8
9
|
| [Compass Canada](profile.md) | 2.0 | Compass Canada is the Canadian arm of the Compass account — a separate TOGA tenant, related to but distinct from Compass USA. | |
|
|
9
10
|
| [Grand & Toy ASN Backfill (cXML replay + CSV-to-JSON)](workflows/grand-and-toy-asn-backfill.md) | 2.0 | How to recover Compass Canada shipments whose ASN never landed — used on 2026-08-20 to backfill the 67-day Grand & Toy outage (**518 shipments**: 171 via cXML r | worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php, api2/Component/Api/Cxml/Cxml.php, _underscore/Model/Compass/AdvanceShippingNotice.php |
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compass Canada — Fulfilled vs Partially Fulfilled (per-line, ASN bridge only)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: compass-canada
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Compass/Canada/SalesOrder.php
|
|
13
|
+
- _underscore/Model/Compass/SalesOrder.php
|
|
14
|
+
related:
|
|
15
|
+
- ../../compass-usa/features/order-fulfillment-status-per-line.md
|
|
16
|
+
- ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
17
|
+
- ../../../2.0/apps/_underscore/features/calculated-sql-fields.md
|
|
18
|
+
- grand-and-toy-asn-import.md
|
|
19
|
+
- ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
TOGa Supply showed **Fulfilled** on Compass Canada orders that were only partly shipped. Reported on
|
|
25
|
+
`SAC100664` (`SalesOrders.id 665`): line 1 the ZBOOK `C40QYUC#ABA` ordered 1 / shipped 0, line 2 the
|
|
26
|
+
mouse `9VA80AA#ABA` ordered 1 / shipped 1, and the badge read **Fulfilled**.
|
|
27
|
+
|
|
28
|
+
Root cause was in the shared parent `_Model_Compass_SalesOrder::_status()`, which compared two
|
|
29
|
+
**order-level grand totals** whose two sides did not count the same lines: the ordered side filtered
|
|
30
|
+
`SalesOrderItems.parentSalesOrderItemId IS NULL` (dropping bundle child lines) while the shipped side
|
|
31
|
+
summed **every** ASN quantity on the order with no such filter. On `SAC100664` the mouse is a child
|
|
32
|
+
of the ZBOOK (`parentSalesOrderItemId = 1131`), so ordered = 1 and shipped = 1 and it read fulfilled.
|
|
33
|
+
Confirmed by running the exact expression on prod: orderedSide `1.00`, shippedSide `1`.
|
|
34
|
+
|
|
35
|
+
Canada's fix is a clean **per-line** rule in its own subclass. Prod impact: **115 Canada orders
|
|
36
|
+
corrected Fulfilled to Partially Fulfilled, zero orders moving the other way**, and
|
|
37
|
+
pendingFulfillment / canceled / pendingApproval / pendingInitialApproval counts all unchanged.
|
|
38
|
+
|
|
39
|
+
> **Canada's rule is NOT the USA rule, and the two must not be merged.** Compass USA needs a second,
|
|
40
|
+
> aggregate condition on top of the per-line check; porting Canada's clean rule to USA creates
|
|
41
|
+
> thousands of *new* false Fulfilleds. See
|
|
42
|
+
> [the USA doc](../../compass-usa/features/order-fulfillment-status-per-line.md).
|
|
43
|
+
|
|
44
|
+
## Key files / entry points
|
|
45
|
+
|
|
46
|
+
- **`_underscore/Model/Compass/Canada/SalesOrder.php`** — `_fulfillmentStatus()`, a `protected static`
|
|
47
|
+
override of the shared parent method. This is the whole Canada rule.
|
|
48
|
+
- **`_underscore/Model/Compass/SalesOrder.php`** — the parent `_status()` (approval gate untouched)
|
|
49
|
+
now calls `static::_fulfillmentStatus()`, which is what makes the Canada override reachable. It
|
|
50
|
+
also supplies the `_qtyShippedByAsn()` helper Canada reuses.
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
pendingFulfillment WHEN the order has no ASN at all
|
|
56
|
+
partiallyFulfilled WHEN any line with price > 0 has quantity > that line's own shipped quantity
|
|
57
|
+
fulfilled otherwise
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- **No ASN** is `COUNT(*) = 0` over `AdvanceShippingNotices` joined to the order through
|
|
61
|
+
`SalesOrders_PurchaseOrders`.
|
|
62
|
+
- **Per-line shipped quantity** comes from `_qtyShippedByAsn()` on the shared parent, correlated to
|
|
63
|
+
the individual `SalesOrderItems.id`.
|
|
64
|
+
- No parent/child line filter, so bundle children are judged on their own merits — that is the fix.
|
|
65
|
+
|
|
66
|
+
### The per-line shipped source (ASN bridge, and only the ASN bridge)
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
AdvanceShippingNoticeItems.purchaseOrderItemId
|
|
70
|
+
-> SalesOrderItems_PurchaseOrderItems.purchaseOrderItemId
|
|
71
|
+
-> SalesOrderItems_PurchaseOrderItems.salesOrderItemId
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This mapping is **100% complete in `Client_CompassCanada`**: all **947** ASN items have a
|
|
75
|
+
`purchaseOrderItemId` and every one resolves to a sales-order line. Both join columns are indexed.
|
|
76
|
+
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 here — Canada
|
|
78
|
+
needs none of USA's TOGa Tech / Office Depot fulfillment chain.
|
|
79
|
+
|
|
80
|
+
## Design decisions worth keeping
|
|
81
|
+
|
|
82
|
+
- **⚠ Do NOT switch Canada to the local `ItemFulfillments` / `ItemFulfillmentItems` tables**, even
|
|
83
|
+
though the line-level `_qtyFulfilled` UI column does read them. They are **incomplete** in Canada:
|
|
84
|
+
**47** orders have ASN shipments with no local `ItemFulfillment` at all and **52** more disagree on
|
|
85
|
+
quantity, so moving the order-level rule onto the base `_Model_Client_SalesOrder` logic would have
|
|
86
|
+
regressed roughly **99** orders.
|
|
87
|
+
- **Keep the `price > 0` filter.** Only **9** of Canada's **69** zero-price lines ever receive an ASN,
|
|
88
|
+
so requiring them to ship would strand orders in partiallyFulfilled forever.
|
|
89
|
+
- **Canada deliberately omits any Compass-vendor exclusion.** `VENDOR_ID__COMPASS = 26` on the parent
|
|
90
|
+
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 — so the inherited constant
|
|
92
|
+
was a latent wrong-id no-op. Do not "restore" it here.
|
|
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.
|
|
95
|
+
|
|
96
|
+
## Gotchas / known issues
|
|
97
|
+
|
|
98
|
+
- **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. Canada's in-transit email cron
|
|
100
|
+
(`worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php`) keeps its **own
|
|
101
|
+
duplicated copy** of the `_status` CASE, but it only ever produces canceled /
|
|
102
|
+
pendingApprovalUnknown / pendingFulfillment / shipped and filters on
|
|
103
|
+
`computedStatusSlug = 'shipped'` — and the pendingFulfillment gate was left byte-identical, so it
|
|
104
|
+
is unaffected.
|
|
105
|
+
- **⚠ Canada's order status is no longer identical to Compass USA.** Older notes (and the profile)
|
|
106
|
+
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.**
|
|
108
|
+
- **The ApprovalDecision notification query on the parent calls `self::_status()`**, not `static::`,
|
|
109
|
+
so it always evaluates the **parent** fulfillment rule even for Canada. Harmless today (it only
|
|
110
|
+
branches on pendingApproval / canceled / pendingFulfillment, and pendingFulfillment is identical
|
|
111
|
+
in both regions) but it is a trap if Canada ever changes those.
|
|
112
|
+
|
|
113
|
+
## Change history
|
|
114
|
+
- 2026-08-26 — Fixed the false **Fulfilled** on partly-shipped Canada orders (reported via
|
|
115
|
+
`SAC100664`): the shared parent compared two order-level totals whose ordered side dropped bundle
|
|
116
|
+
child lines while the shipped side did not. Added a `_fulfillmentStatus()` override in
|
|
117
|
+
`_Model_Compass_Canada_SalesOrder` — pendingFulfillment with no ASN, otherwise partiallyFulfilled
|
|
118
|
+
if any `price > 0` line is short of its **own** ASN-bridge shipped quantity. Prod: 115 orders
|
|
119
|
+
corrected Fulfilled to Partially Fulfilled, zero promoted, all other status counts unchanged.
|
|
120
|
+
Recorded why the ASN bridge (100% complete, 947 items) is the source and the local
|
|
121
|
+
`ItemFulfillments` tables are not (would regress ~99 orders), why `price > 0` stays, and that the
|
|
122
|
+
inherited `VENDOR_ID__COMPASS = 26` is a US-only id and a no-op here. Not committed or pushed; not
|
|
123
|
+
yet exercised through the Supply UI. (bala)
|
|
124
|
+
|
|
125
|
+
## Related docs
|
|
126
|
+
- [Compass USA — order aggregate AND per-line](../../compass-usa/features/order-fulfillment-status-per-line.md)
|
|
127
|
+
— the shared parent rule, and why USA cannot use this page's rule.
|
|
128
|
+
- [Grand & Toy ASN Import](grand-and-toy-asn-import.md) — where Canada's ASN rows come from.
|
|
129
|
+
- [IF Stage Lifecycle & Order Status](../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md)
|
|
130
|
+
— the shipped-only stage policy shared by both Compass regions.
|
|
@@ -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-26
|
|
20
20
|
owners: [jcardinal, bala, tcox, apeterson]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
@@ -29,6 +29,7 @@ related:
|
|
|
29
29
|
- ../compass-usa/features/people-file-user-lifecycle.md
|
|
30
30
|
- ../compass-usa/features/isfulfillable-data-quality-and-type-rule.md
|
|
31
31
|
- features/french-order-email-localization.md
|
|
32
|
+
- features/order-fulfillment-status-per-line.md
|
|
32
33
|
- ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
|
|
33
34
|
- ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
34
35
|
- ../../2.0/apps/_underscore/features/surface-resolver.md
|
|
@@ -143,10 +144,19 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
143
144
|
207 → 1. See
|
|
144
145
|
[isFulfillable — Data Quality & the Type-Derived Rule](../compass-usa/features/isfulfillable-data-quality-and-type-rule.md).
|
|
145
146
|
- The 2026-06-08 ASN → ItemFulfillment work was for **Compass USA**, not Compass Canada.
|
|
146
|
-
- **Order status is shipped-only
|
|
147
|
-
|
|
147
|
+
- **Order status is shipped-only** (shared `_Model_Compass_SalesOrder`). Compass Canada's own IF
|
|
148
|
+
lifecycle stages (picked/packed/shipped) + shipped backfill are seeded by
|
|
148
149
|
`dbchanges2/Client_CompassCanada/2026-06-30a - ItemFulfillmentLifecycleAndShippedBackfill.sql`.
|
|
149
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 fulfilled-vs-partiallyFulfilled rule
|
|
152
|
+
diverged 2026-08-26.** Canada overrides `_fulfillmentStatus()` in
|
|
153
|
+
`_Model_Compass_Canada_SalesOrder` with a clean **per-line** rule sourced from the **ASN bridge
|
|
154
|
+
only** (`AdvanceShippingNoticeItems.purchaseOrderItemId` -> `SalesOrderItems_PurchaseOrderItems`,
|
|
155
|
+
100% complete here). Do not read Canada's local `ItemFulfillments` tables for this (incomplete:
|
|
156
|
+
~99 orders would regress), and do not port USA's rule or its `VENDOR_ID__COMPASS = 26` (a US
|
|
157
|
+
vendor id; all 901 Canada POs are Grand & Toy `vendorId 1`). Fixed 115 falsely-Fulfilled prod
|
|
158
|
+
orders. See
|
|
159
|
+
[Fulfilled vs Partially Fulfilled](features/order-fulfillment-status-per-line.md).
|
|
150
160
|
- **Item titles in customer emails** come from the `ItemTranslations` sidecar (prod: 220 fr-CA rows,
|
|
151
161
|
100% coverage of every item ever ordered). Resolve the English/translated fallback **in PHP**, never
|
|
152
162
|
as a SQL `COALESCE` — the two columns' collations differ and differ *per client DB*.
|
|
@@ -14,6 +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 aggregate AND per-line)](features/order-fulfillment-status-per-line.md) | 2.0 | TOGa Supply showed **Fulfilled** on Compass orders that were only partly shipped. | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php |
|
|
17
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 |
|
|
18
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 |
|
|
19
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 |
|
|
@@ -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-26
|
|
10
10
|
owners: ["rgirish", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/SalesOrder.php
|
|
@@ -14,6 +14,7 @@ files:
|
|
|
14
14
|
- worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
|
|
15
15
|
related:
|
|
16
16
|
- mits-po-to-so-item-linking.md
|
|
17
|
+
- order-fulfillment-status-per-line.md
|
|
17
18
|
- ../workflows/odp-order-pipeline-to-netsuite.md
|
|
18
19
|
- ../workflows/order-lifecycle-and-data-integrity.md
|
|
19
20
|
- ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
@@ -32,9 +33,11 @@ source (auto-approval + item population), **not** in the status SQL.
|
|
|
32
33
|
|
|
33
34
|
## Key files / entry points
|
|
34
35
|
- **`_underscore/Model/Compass/SalesOrder.php`**
|
|
35
|
-
- `_status` calculated field — approval gate runs before the
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
- `_status` calculated field — approval gate runs before the fulfillment machine. **Since
|
|
37
|
+
2026-08-26 the fulfillment half is a separate `protected static _fulfillmentStatus()` called via
|
|
38
|
+
`static::`;** the approval gate described on this page is unchanged and stays in `_status()`. See
|
|
39
|
+
[Fulfilled vs Partially Fulfilled](order-fulfillment-status-per-line.md) and
|
|
40
|
+
[IF stage lifecycle & order status](../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
|
|
38
41
|
- `postPost` — the `if ($isMrOrder)` block loops `foreach ([1,2] as $step)` and issues two
|
|
39
42
|
`internalApiRequest('POST', '/approval-decisions', …)` with `isApproved=true`,
|
|
40
43
|
note `'Auto-approved: MR order'`. **Went live 2026-06-03.**
|
|
@@ -120,6 +123,10 @@ Safe scoping baked in — **decisions only, never Approvals**:
|
|
|
120
123
|
from the sibling ODP SO first).
|
|
121
124
|
|
|
122
125
|
## Change history
|
|
126
|
+
- 2026-08-26 — Noted that `_status()`'s fulfillment half moved into `_fulfillmentStatus()` (per-region
|
|
127
|
+
seam); the approval gate this page documents is untouched, and the "do not fix stuck MR/MA orders in
|
|
128
|
+
`_status`" ruling still stands — the stuck orders are empty, so no fulfillment rule helps them.
|
|
129
|
+
(bala)
|
|
123
130
|
- 2026-08-05 — Recorded an **open, unfixed** defect in the MR auto-approval path: the
|
|
124
131
|
`ApprovalTemplateStage`-not-found `throw new _Exception(...)` passes 1 argument to a 5-argument
|
|
125
132
|
constructor, so that failure raises `ArgumentCountError` and loses its own diagnostic message.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compass USA — Fulfilled vs Partially Fulfilled (order aggregate AND per-line)
|
|
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-26
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Compass/SalesOrder.php
|
|
13
|
+
- _underscore/Model/Compass/Canada/SalesOrder.php
|
|
14
|
+
related:
|
|
15
|
+
- ../../compass-canada/features/order-fulfillment-status-per-line.md
|
|
16
|
+
- ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
17
|
+
- ../../../2.0/apps/_underscore/features/calculated-sql-fields.md
|
|
18
|
+
- ../../../2.0/apps/_underscore/features/sales-order-status-filter-surface.md
|
|
19
|
+
- mr-ma-order-approval-and-status.md
|
|
20
|
+
- asn-to-item-fulfillment.md
|
|
21
|
+
- ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
|
|
26
|
+
TOGa Supply showed **Fulfilled** on Compass orders that were only partly shipped. The cause was
|
|
27
|
+
structural, not a data problem: the fulfillment half of `_Model_Compass_SalesOrder::_status()`
|
|
28
|
+
decided fulfilled vs. partiallyFulfilled by comparing **two order-level grand totals** — an
|
|
29
|
+
"ordered" sum against a "shipped" sum. A grand total lets one line's surplus cancel another line's
|
|
30
|
+
gap, so an order with line 1 short and line 2 over-shipped read as complete.
|
|
31
|
+
|
|
32
|
+
**"Everything shipped" is a per-line fact.** As of 2026-08-26 an order is Fulfilled only when the
|
|
33
|
+
legacy aggregate condition still passes **AND** no purchased line is short of its own ordered
|
|
34
|
+
quantity. Prod impact across all ~112k `Client_Compass` orders: **Fulfilled fell by exactly 5,234**
|
|
35
|
+
(26,856 to 21,622), partiallyFulfilled rose by the same amount, and **nothing** moved
|
|
36
|
+
partiallyFulfilled to Fulfilled.
|
|
37
|
+
|
|
38
|
+
Compass Canada has the same false-Fulfilled bug but a **different** correct rule — see the
|
|
39
|
+
[Canada doc](../../compass-canada/features/order-fulfillment-status-per-line.md). USA cannot use
|
|
40
|
+
Canada's rule; that is the subject of the design gotcha below and is the most important thing on
|
|
41
|
+
this page.
|
|
42
|
+
|
|
43
|
+
## Key files / entry points
|
|
44
|
+
|
|
45
|
+
- **`_underscore/Model/Compass/SalesOrder.php`**
|
|
46
|
+
- `_status()` — unchanged approval gate; its fulfillment branch now calls
|
|
47
|
+
**`static::_fulfillmentStatus($alias_SalesOrders)`**.
|
|
48
|
+
- `_fulfillmentStatus()` (`protected static`, new) — the pendingFulfillment / partiallyFulfilled /
|
|
49
|
+
fulfilled decision. Shared parent, used by Compass USA; overridden by Canada.
|
|
50
|
+
- `_isPurchasedLine()` — is this line expected to ship at all?
|
|
51
|
+
- `_qtyShippedByAsn()` — per-line shipped qty from the ASN bridge.
|
|
52
|
+
- `_qtyShippedByTogaTech()` — per-line shipped qty through the Compass to ODP to TOGa Tech item chain.
|
|
53
|
+
- `const VENDOR_ID__COMPASS = 26` — the **US** Compass vendor id, excluded from "purchased" lines.
|
|
54
|
+
- Front end (labels only): toga2-supply `FILTERFIELDS.ts` (user-selectable status filter) and
|
|
55
|
+
`renderBadge.tsx` (badge text).
|
|
56
|
+
|
|
57
|
+
## How it works
|
|
58
|
+
|
|
59
|
+
### The seam: `_status()` vs `_fulfillmentStatus()`
|
|
60
|
+
|
|
61
|
+
`_status()` keeps its original three-part shape — an explicit `salesOrderStageId` wins; then the
|
|
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.**
|
|
67
|
+
|
|
68
|
+
### pendingFulfillment (byte-identical to the old logic)
|
|
69
|
+
|
|
70
|
+
Still `COUNT(shipped TOGa Tech ItemFulfillments) + COUNT(ASNs on the order) = 0`. This gate was
|
|
71
|
+
deliberately left untouched, which is why the in-transit email crons are unaffected (see
|
|
72
|
+
*Blast radius*).
|
|
73
|
+
|
|
74
|
+
### Fulfilled now requires two independent conditions
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
partiallyFulfilled WHEN NOT ( legacyAggregateCondition ) OR ( any purchased line is short )
|
|
78
|
+
fulfilled otherwise
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- **legacyAggregateCondition** — the original order-level `orderedSum <= shippedSum`, kept verbatim.
|
|
82
|
+
- **per-line condition** — `COUNT(*) > 0` over `SalesOrderItems` where `price > 0` **AND**
|
|
83
|
+
`_isPurchasedLine()` **AND** `quantity > (_qtyShippedByAsn() + _qtyShippedByTogaTech())`.
|
|
84
|
+
|
|
85
|
+
Because *new-fulfilled implies legacy-fulfilled* by construction, the change is **strictly
|
|
86
|
+
corrective**: it can only downgrade a wrong Fulfilled, never promote an order that used to read
|
|
87
|
+
Partially Fulfilled. That property is the whole reason the change was safe to make against 112k
|
|
88
|
+
live orders, and **anyone editing this method must preserve it.**
|
|
89
|
+
|
|
90
|
+
### Telling a shippable line from a service line
|
|
91
|
+
|
|
92
|
+
A line is expected to ship only when it has a `SalesOrderItems_PurchaseOrderItems` link reaching a
|
|
93
|
+
`PurchaseOrders` row whose `vendorId <> VENDOR_ID__COMPASS`.
|
|
94
|
+
|
|
95
|
+
**Neither `price > 0` nor `Items.inventoryType` separates shippable from non-shippable** — HYBRID
|
|
96
|
+
dominates both groups. Verified on prod: `PC-KIT-OPT2`, `Tech Support`, `LEASE REFRESH`,
|
|
97
|
+
`LT-FREIGHT`, `LT-PROPERTY TAX`, `LTX-SALES TAX`, `LT-HPWARRANTY`, `TabAdminSupp`,
|
|
98
|
+
`CG-MEDKITTING-NWN-APPLE` and `ODP-COMPASS_MERAKI` have **zero** PO links, while real hardware does
|
|
99
|
+
(`C40QYUC`: 1,208 of 1,362 lines; `MD4A4LL/A-S`: 130 of 137).
|
|
100
|
+
|
|
101
|
+
> **The same `partNumber` can exist as several `Items` rows, some purchased and some not.** The test
|
|
102
|
+
> must be **per line**, never per part number.
|
|
103
|
+
|
|
104
|
+
## Gotchas / known issues
|
|
105
|
+
|
|
106
|
+
- **⚠ Do NOT "simplify" this to Canada's clean per-line rule.** Tested against prod: a straight port
|
|
107
|
+
of the Canada rule to USA would move **3,727 orders from partiallyFulfilled to Fulfilled** — i.e.
|
|
108
|
+
create thousands of *new* false Fulfilleds. Cause: many USA **priced hardware** lines have no
|
|
109
|
+
purchase-order-item link at all, so the purchased-line filter skips them and an order with zero
|
|
110
|
+
eligible lines passes **vacuously**. Worked example: `SA127967` has five priced hardware lines
|
|
111
|
+
(`AW5M5UT-EW`, `S24D402GAN`, `HDMM6---ODP`, `9VA80AA`, `9SR37UT`), none PO-linked, and a pure
|
|
112
|
+
per-line rule returns fulfilled. Keeping **both** conditions is what makes the direction of change
|
|
113
|
+
one-way.
|
|
114
|
+
- **Duplicate ASNs over-credit a line.** A line can be credited twice, so one line shipping double
|
|
115
|
+
masks another shipping nothing. Confirmed on `SA135272` (line 1 ordered 4 / shipped 8, line 2
|
|
116
|
+
ordered 4 / shipped 0 — the old rule said fulfilled) and `SA135308` (line 3 ordered 1 / shipped 0);
|
|
117
|
+
PO item **207281** carries two ASNs each claiming qty 4. Neither order has bundle children, so
|
|
118
|
+
this is a defect distinct from the parent/child line mismatch. The per-line rule reduces but does
|
|
119
|
+
not eliminate it — a genuinely short line can still read fulfilled when its own ASNs double-count.
|
|
120
|
+
- **The old ordered side explicitly INCLUDED lines with no PO link**, i.e. it expected tax, freight
|
|
121
|
+
and service lines to ship. That clause is what made the aggregate unreachable for many orders.
|
|
122
|
+
- **The old ordered side filtered `parentSalesOrderItemId IS NULL` while the shipped side did not** —
|
|
123
|
+
the two sides never counted the same lines. USA has **62,865** child lines, so this was widespread.
|
|
124
|
+
Canada's `SAC100664` is the minimal reproduction; see the Canada doc.
|
|
125
|
+
- **4.3% of USA ASN shipped quantity cannot be attributed to a sales-order line**, and this fix does
|
|
126
|
+
not solve it: **5,536** `AdvanceShippingNoticeItems` have a NULL `purchaseOrderItemId`, and
|
|
127
|
+
**12,163** more point at a `purchaseOrderItemId` with no `SalesOrderItems_PurchaseOrderItems` link
|
|
128
|
+
(**26,770 of 617,166 units**). Treat per-line shipped quantity as a floor, not a truth.
|
|
129
|
+
- **The legacy order-level TOGa Tech shipped sum is inflated by bridge fan-out**, because it
|
|
130
|
+
correlates nothing at item level: on `SA136116` the order-level chain returns **48** where the
|
|
131
|
+
per-item chain returns **32**. `_qtyShippedByTogaTech()` is the correlated (correct) version; the
|
|
132
|
+
aggregate inside `_fulfillmentStatus()` is the legacy one and was kept only because removing it
|
|
133
|
+
would break the one-way-change guarantee.
|
|
134
|
+
|
|
135
|
+
## Blast radius — fulfilled vs partiallyFulfilled is display-only
|
|
136
|
+
|
|
137
|
+
Every consumer was audited before shipping, which is why this carries no downstream risk:
|
|
138
|
+
|
|
139
|
+
- **The status is computed live and never stored.** No backfill; reverting the code fully reverts the
|
|
140
|
+
behaviour.
|
|
141
|
+
- **In-transit / delivered email crons are unaffected.**
|
|
142
|
+
`worker/crons/toga2/compass/update_salesorder_status_from_odp.php`,
|
|
143
|
+
`worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php` and
|
|
144
|
+
`compass/workflow/test_partial_in_transit_email.php` each keep their **own duplicated copy** of the
|
|
145
|
+
`_status` CASE, but those copies only ever produce canceled / pendingApprovalUnknown /
|
|
146
|
+
pendingFulfillment / shipped and they filter on `computedStatusSlug = 'shipped'`. They never
|
|
147
|
+
compute fulfilled or partiallyFulfilled, and the "has an ASN" condition they rely on was left
|
|
148
|
+
byte-identical. **That duplication is itself a divergence risk** — see the
|
|
149
|
+
[in-transit email doc](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
|
|
150
|
+
- **The MA sales-order exception report**
|
|
151
|
+
(`compass/workflow/7_generate_ma_sales_order_exception_report.php`) filters the **stored**
|
|
152
|
+
`salesOrderStageId` column, not the computed status.
|
|
153
|
+
- **toga2-supply** sends only the approvals filter values (`pendingInitialApproval` /
|
|
154
|
+
`pendingApproval`) to the server; `fulfilled` and `partiallyFulfilled` appear only in
|
|
155
|
+
`FILTERFIELDS.ts` (user-selectable filter) and `renderBadge.tsx` (a label).
|
|
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.
|
|
161
|
+
|
|
162
|
+
## Change history
|
|
163
|
+
- 2026-08-26 — Fixed the false **Fulfilled** on partly-shipped orders. Split the fulfillment half of
|
|
164
|
+
`_status()` into `protected static _fulfillmentStatus()` called via `static::` (proved
|
|
165
|
+
SQL-identical), then made Fulfilled require the legacy order-level aggregate **AND** no short
|
|
166
|
+
purchased line, with new helpers `_isPurchasedLine()`, `_qtyShippedByAsn()`,
|
|
167
|
+
`_qtyShippedByTogaTech()`. Fixed two USA-only defects along the way: duplicate ASNs double-crediting
|
|
168
|
+
a line, and the ordered side explicitly *including* non-PO-linked tax/freight/service lines. Prod:
|
|
169
|
+
Fulfilled 26,856 to 21,622 (down 5,234), zero orders promoted. Recorded why a clean per-line port
|
|
170
|
+
of Canada's rule is **wrong** here (3,727 new false Fulfilleds), how to identify a shippable line
|
|
171
|
+
(PO link, not price or `inventoryType`), and the ASN attribution limits that bound accuracy. Not
|
|
172
|
+
committed or pushed; not yet exercised through the Supply UI. (bala)
|
|
173
|
+
|
|
174
|
+
## Related docs
|
|
175
|
+
- [Compass Canada — per-line, ASN-only rule](../../compass-canada/features/order-fulfillment-status-per-line.md)
|
|
176
|
+
- [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
|
+
- [FIELD_SQL calculated fields](../../../2.0/apps/_underscore/features/calculated-sql-fields.md)
|
|
179
|
+
— 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.
|
|
@@ -30,6 +30,7 @@ related:
|
|
|
30
30
|
- workflows/granting-persona-bundle-access.md
|
|
31
31
|
- features/mits-sales-order-transmission-alerting.md
|
|
32
32
|
- features/asn-to-item-fulfillment.md
|
|
33
|
+
- features/order-fulfillment-status-per-line.md
|
|
33
34
|
- features/cost-centers.md
|
|
34
35
|
- features/isfulfillable-data-quality-and-type-rule.md
|
|
35
36
|
- features/approval-decision-flow.md
|
|
@@ -163,6 +164,15 @@ separate, related client (see its own profile).
|
|
|
163
164
|
shared by Compass USA + Canada) counts **shipped only** — picked/packed never advance a Compass
|
|
164
165
|
order. Contrast Quad, which gets the full picked/packed machine. See
|
|
165
166
|
[IF stage lifecycle & order status](../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
|
|
167
|
+
- **Fulfilled requires the order aggregate AND every purchased line (2026-08-26).** The old
|
|
168
|
+
order-level ordered-total vs shipped-total comparison let one line's surplus hide another line's
|
|
169
|
+
gap, so 5,234 prod orders read **Fulfilled** while partly shipped. `_status()`'s fulfillment branch
|
|
170
|
+
now lives in `_fulfillmentStatus()` and requires the legacy aggregate **plus** no short purchased
|
|
171
|
+
line — strictly corrective, it can only downgrade. **⚠ Do not "simplify" it to Compass Canada's
|
|
172
|
+
clean per-line rule:** many USA priced hardware lines have no PO-item link, so a pure per-line rule
|
|
173
|
+
passes vacuously and would create 3,727 *new* false Fulfilleds. Full evidence, the shippable-line
|
|
174
|
+
test, and the ASN attribution limits:
|
|
175
|
+
[Fulfilled vs Partially Fulfilled](features/order-fulfillment-status-per-line.md).
|
|
166
176
|
- Built on the shared 2.0 Recursive Item Fulfillments engine (upstream mirroring).
|
|
167
177
|
- **`Items.isFulfillable` gates the storefront Qty Fulfilled cell (2026-07).** Sourced from
|
|
168
178
|
NetSuite onto the Agilant source item during the 1.0 item sync
|
|
@@ -6,6 +6,7 @@ apps:
|
|
|
6
6
|
- worker2
|
|
7
7
|
- library
|
|
8
8
|
- toga2-supply
|
|
9
|
+
- toga25-supply
|
|
9
10
|
- dbchanges2
|
|
10
11
|
- _underscore
|
|
11
12
|
- api2
|
|
@@ -15,7 +16,7 @@ project: Worker
|
|
|
15
16
|
client: elite
|
|
16
17
|
type: profile
|
|
17
18
|
status: active
|
|
18
|
-
updated: 2026-08-
|
|
19
|
+
updated: 2026-08-26
|
|
19
20
|
owners: [snaredla, apeterson, tcox, bala]
|
|
20
21
|
files:
|
|
21
22
|
- worker2/Worker/Elite.php
|
package/package.json
CHANGED