toga-ai 1.0.657 → 1.0.659
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/dbchanges2/INDEX.md +1 -0
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +139 -0
- package/knowledge/INDEX.md +2 -2
- 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 +2 -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/features/persona-model-and-levy-gating.md +25 -7
- package/knowledge/clients/compass-usa/profile.md +23 -3
- package/knowledge/clients/compass-usa/workflows/granting-persona-bundle-access.md +163 -0
- package/knowledge/clients/compass-usa/workflows/persona-refactor-migration.md +34 -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.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
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
|
+
| [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 |
|
|
6
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
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/ |
|
|
8
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 |
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
|
|
13
|
+
related:
|
|
14
|
+
- ../architecture.md
|
|
15
|
+
- ../../_underscore/features/acl-permission-chain.md
|
|
16
|
+
- ../../_underscore/features/tracking-number-bridges.md
|
|
17
|
+
- ../../../../clients/compass-usa/workflows/granting-persona-bundle-access.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
Most `dbchanges2` files are **additive data grants** run by hand against production, often more than
|
|
23
|
+
once (once per environment, or twice because someone was not sure it took). Two mechanics decide
|
|
24
|
+
whether that is safe: how you generate the `uuid` every 2.0 table demands, and how you guard the
|
|
25
|
+
insert. Both have a trap that fails **silently** rather than erroring.
|
|
26
|
+
|
|
27
|
+
Every 2.0 table carries a `uuid` column that is `char(36) NOT NULL` **with no default** — the model
|
|
28
|
+
layer fills it in application code, so a hand-written `INSERT` must supply it itself.
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
### Generating a real UUID4 inline
|
|
33
|
+
|
|
34
|
+
The standard is "UUID4, random — never MySQL `UUID()`" (`UUID()` is v1: MAC address + timestamp, and
|
|
35
|
+
sequential). MySQL 8's `RANDOM_BYTES()` gives a genuinely random, correctly **version-4-shaped** value
|
|
36
|
+
in one expression:
|
|
37
|
+
|
|
38
|
+
```sql
|
|
39
|
+
LOWER(CONCAT(HEX(RANDOM_BYTES(4)), '-', HEX(RANDOM_BYTES(2)), '-4', RIGHT(HEX(RANDOM_BYTES(2)), 3), '-', SUBSTRING('89ab', FLOOR(1 + RAND() * 4), 1), RIGHT(HEX(RANDOM_BYTES(2)), 3), '-', HEX(RANDOM_BYTES(6))))
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The two literals are what make it v4 rather than just random hex: the `'-4'` pins the version nibble,
|
|
43
|
+
and `SUBSTRING('89ab', …)` picks the variant nibble. Verified working on prod MySQL **8.0.39**.
|
|
44
|
+
|
|
45
|
+
**Existing rows are not the pattern to copy.** Plenty of live rows across these tables are random hex
|
|
46
|
+
that is *not* v4-shaped (older migrations concatenated `RANDOM_BYTES` without the version/variant
|
|
47
|
+
nibbles). They are fine and must not be "fixed" — but do not use them as the template for new SQL.
|
|
48
|
+
|
|
49
|
+
### ⚠ `DISTINCT` cannot dedupe a row that carries a random uuid
|
|
50
|
+
|
|
51
|
+
This is the silent one. In an `INSERT … SELECT`, putting the uuid expression inside a
|
|
52
|
+
`SELECT DISTINCT` **disables the dedupe entirely** — the per-row random uuid makes every candidate
|
|
53
|
+
row unique, so `DISTINCT` has nothing to collapse and every duplicate id passes through.
|
|
54
|
+
|
|
55
|
+
```sql
|
|
56
|
+
# WRONG — DISTINCT never removes anything; the uuid differs on every row
|
|
57
|
+
INSERT INTO Personas_Items (uuid, personaId, itemId)
|
|
58
|
+
SELECT DISTINCT
|
|
59
|
+
LOWER(CONCAT(HEX(RANDOM_BYTES(4)), …)),
|
|
60
|
+
@personaId,
|
|
61
|
+
bi.itemId
|
|
62
|
+
FROM BundleItems bi
|
|
63
|
+
INNER JOIN Bundles b ON b.id = bi.bundleId
|
|
64
|
+
WHERE b.number = '243';
|
|
65
|
+
|
|
66
|
+
# CORRECT — dedupe the id set in a derived table, generate the uuid in the OUTER select
|
|
67
|
+
INSERT INTO Personas_Items (uuid, personaId, itemId)
|
|
68
|
+
SELECT
|
|
69
|
+
LOWER(CONCAT(HEX(RANDOM_BYTES(4)), …)),
|
|
70
|
+
@personaId,
|
|
71
|
+
src.itemId
|
|
72
|
+
FROM (
|
|
73
|
+
SELECT DISTINCT bi.itemId
|
|
74
|
+
FROM BundleItems bi
|
|
75
|
+
INNER JOIN Bundles b ON b.id = bi.bundleId
|
|
76
|
+
WHERE
|
|
77
|
+
b.number = '243' AND
|
|
78
|
+
bi.isActive = 1 AND
|
|
79
|
+
bi.itemId IS NOT NULL
|
|
80
|
+
) src;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The same shape is the fix for the self-referencing `INSERT … NOT EXISTS` error 1093 (MySQL will not
|
|
84
|
+
let you read the table you are inserting into) — wrap the read in a derived table.
|
|
85
|
+
|
|
86
|
+
### Guarding the insert: know whether the table has a unique key
|
|
87
|
+
|
|
88
|
+
Whether a re-run is loud or silent depends entirely on the target table's indexes, and bridge tables
|
|
89
|
+
in 2.0 are **inconsistent** about this — some have a composite unique key, some do not. Check
|
|
90
|
+
before you write, not after.
|
|
91
|
+
|
|
92
|
+
- **Unique key present** — a re-run errors. Loud, recoverable, but it aborts the rest of the file.
|
|
93
|
+
Add a guard if you want a clean no-op.
|
|
94
|
+
- **No unique key** — a re-run **silently duplicates every row**, and nothing tells you. Always guard
|
|
95
|
+
these.
|
|
96
|
+
|
|
97
|
+
Both guard shapes are in use and equivalent:
|
|
98
|
+
|
|
99
|
+
```sql
|
|
100
|
+
# LEFT JOIN … IS NULL
|
|
101
|
+
FROM Users u
|
|
102
|
+
INNER JOIN Roles r ON r.name = 'SuperUser'
|
|
103
|
+
LEFT JOIN Users_Roles ur ON ur.userId = u.id AND ur.roleId = r.id
|
|
104
|
+
WHERE
|
|
105
|
+
u.email = '…' AND
|
|
106
|
+
ur.id IS NULL;
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**A guarded insert that inserts zero rows is a success, not a wasted file** — see
|
|
110
|
+
[Client-DB grant migrations are re-runnable NO-OPs](../../_underscore/features/acl-permission-chain.md).
|
|
111
|
+
|
|
112
|
+
### `LAST_INSERT_ID()` into a session variable ties the whole file to one session
|
|
113
|
+
|
|
114
|
+
The common shape — `INSERT` a parent, `SET @parentId = LAST_INSERT_ID();`, then insert children
|
|
115
|
+
against `@parentId` — means **every phase must run in the same connection**. Reconnecting between
|
|
116
|
+
phases leaves `@parentId` `NULL` and the child inserts either fail on the FK or write `NULL`. Say so
|
|
117
|
+
in the file header, and make the unguarded parent insert the one phase you skip on a re-run (set
|
|
118
|
+
`@parentId` to the existing row by hand instead).
|
|
119
|
+
|
|
120
|
+
## Gotchas
|
|
121
|
+
|
|
122
|
+
- **A "re-run safe" file is usually only re-run safe *per phase*.** If Phase 1 creates the parent row
|
|
123
|
+
with no `NOT EXISTS` guard, re-running the *whole file* creates a **second parent** and then hangs
|
|
124
|
+
a full set of children off it. Re-run safety of the child phases does not make the file idempotent.
|
|
125
|
+
- **`number` columns are strings.** `Bundles.number`, `Personas.number` and friends are varchar, not
|
|
126
|
+
int — quote them (`WHERE b.number = '243'`) and `CAST(… AS UNSIGNED)` when you need numeric sort
|
|
127
|
+
order. An unquoted comparison forces a string→number coercion that will not use the index.
|
|
128
|
+
- **`Client_*` files may not reach into `Core`.** Unrelated to uuids but it bites the same kind of
|
|
129
|
+
file: separate production clusters mean a `Core.` reference is unrunnable in prod even though it
|
|
130
|
+
works locally. Hardcode `Core.Records` / `Core.RecordFields` ids as literals — see
|
|
131
|
+
[dbchanges2 architecture](../architecture.md).
|
|
132
|
+
|
|
133
|
+
## Change history
|
|
134
|
+
- 2026-08-26 — Initial: recorded the inline **UUID4** expression built on MySQL 8 `RANDOM_BYTES()`
|
|
135
|
+
(version/variant nibbles pinned; verified on prod 8.0.39) and the silent
|
|
136
|
+
**`SELECT DISTINCT` + random-uuid** trap that lets duplicate rows through — dedupe the id set in a
|
|
137
|
+
derived table and generate the uuid in the outer select. Added the unique-key/guard rule for bridge
|
|
138
|
+
tables (no unique key = silent duplicates on re-run) and the `LAST_INSERT_ID()` one-session
|
|
139
|
+
constraint. Extracted from the Compass Creative Studio persona grant. (bala)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -18,10 +18,10 @@ _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
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
24
|
+
- **dbchanges2** (Database Changes) _(framework core)_ — 11 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 4 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
27
27
|
- **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/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,12 +14,14 @@
|
|
|
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 |
|
|
20
21
|
| [Stranded Approval Reassignment (repointing approvals off dead duplicate Compass Users rows)](features/stranded-approval-reassignment.md) | 2.0 | A Compass employee who leaves and comes back after more than `CONTACT_UNLINK_GRACE_DAYS` (5) is **inserted as a brand-new `Users` row** by the PEOPLE importer i | _underscore/Model/Compass/ApprovalDecision.php, worker2/Worker/Client/Compass/ApprovalReassignment.php, worker2/Worker/Client/Compass/PeopleFile.php |
|
|
21
22
|
| [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
|
|
22
23
|
| [Compass Cross-Kit Bundle Corruption — Detection & Repair](workflows/cross-kit-bundle-corruption.md) | 2.0 | A frontend regression in `toga2-commerce`'s edit-order bundle submission mis-attributed bundle (kit) line items and **fees/warranties** to the **wrong kit**, pe | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts |
|
|
24
|
+
| [Granting a Compass user access to a bundle (persona grant) and the SuperUser role](workflows/granting-persona-bundle-access.md) | 2.0 | "Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a super user". | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql |
|
|
23
25
|
| [Compass Office Depot Duplicate PO-Line Cleanup (dual-catalog SKU)](workflows/odp-duplicate-po-line-cleanup.md) | 1.0 | The NetSuite fulfillment-sales-order importer duplicated Office Depot purchase-order lines on Compass USA because it reconciled the fulfillment SO against the e | library/app/api/toga2.php, dbchanges2/Client_Compass/2026-07-22a - CleanupOfficeDepotDuplicatePurchaseOrderItems.sql |
|
|
24
26
|
| [Recovering a Lost Compass ODP EDI 850 Import (re-drop from Logs.FileLog)](workflows/odp-edi-import-recovery.md) | 1.0 | How to recover a Compass **Office Depot EDI 850** import that failed partway — the case where cron **3a** created the ODP SalesOrder header, the follow-up item | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json |
|
|
25
27
|
| [Compass ODP Order Pipeline to NetSuite (numbered worker crons)](workflows/odp-order-pipeline-to-netsuite.md) | 1.0 | The end-to-end **Compass Office Depot (ODP) order → NetSuite** pipeline as it actually runs through the 1.0 `worker` crons under `worker/crons/toga2/compass/`, | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compass/edi/1_download_edi_s3_create_po_toga.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/4_transmit_office_depot_po_acknowledgements.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json, library/app/client/compass.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.
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
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: [bala]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Client/Compass/PeopleFile.php
|
|
@@ -15,6 +15,7 @@ related:
|
|
|
15
15
|
- ../profile.md
|
|
16
16
|
- ../workflows/persona-refactor-migration.md
|
|
17
17
|
- ../workflows/persona-population-env-comparison.md
|
|
18
|
+
- ../workflows/granting-persona-bundle-access.md
|
|
18
19
|
- ../../compass-canada/profile.md
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -40,16 +41,25 @@ below.
|
|
|
40
41
|
|
|
41
42
|
| Persona | id | Meaning |
|
|
42
43
|
|---|---|---|
|
|
43
|
-
| Base (was "All") | **1** | Printers only, after the refactor. Every US user holds it. |
|
|
44
|
+
| Base (was "All") | **1** | Printers only, after the refactor. Every US user holds it. **Still named "All" in prod** — the refactor is not live there (verified 2026-08-26). |
|
|
44
45
|
| Levy | **24** | Levy-sector catalogue. |
|
|
45
|
-
| Non-Levy | **resolved by name** — 38 on dev-sandbox
|
|
46
|
+
| Non-Levy | **resolved by name** — **38 on dev-sandbox only; does not exist in prod** | Everything that is not a printer. Granted to non-Levy users only. |
|
|
46
47
|
| VIP | 35 | VIP add-on. |
|
|
47
48
|
| CDL (MacBook / Surface) | 30 | `@compassdigital.io` + VIP users. |
|
|
48
49
|
|
|
49
|
-
**Never hardcode the Non-Levy id.** `Personas.AUTO_INCREMENT`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
**Never hardcode the Non-Levy id.** On dev-sandbox `Personas.AUTO_INCREMENT` was 38 (max existing id
|
|
51
|
+
37), so the migration's `number = '40'` yielded **id 38**, not 40. The original plan and the (now
|
|
52
|
+
closed) worker2 PR both hardcoded **40** and would have written to the wrong persona.
|
|
53
|
+
|
|
54
|
+
> **⚠ Correction (verified read-only against prod, 2026-08-26): "id 38" was never a prod fact, and is
|
|
55
|
+
> now definitively wrong there.** Prod has **no Non-Levy persona**, and `Personas.id` **38** is
|
|
56
|
+
> **"MyDining- KDS"** — prod's auto-increment moved past 38 after 2026-08-06. Persona **number 40** is
|
|
57
|
+
> taken by that same row. Whenever the migration does run in prod the persona will land on some higher
|
|
58
|
+
> id, which is precisely what name-resolution protects against. Current prod number map and the
|
|
59
|
+
> knock-on effect on the migration's Phase 1:
|
|
60
|
+
> [Persona Refactor](../workflows/persona-refactor-migration.md).
|
|
61
|
+
|
|
62
|
+
`PeopleFile` resolves it at runtime:
|
|
53
63
|
|
|
54
64
|
```php
|
|
55
65
|
const PERSONA_NAME_US_NON_LEVY = 'Non-Levy';
|
|
@@ -128,6 +138,14 @@ VIP (35) and CDL (30) handling is orthogonal and applies to both tenants unchang
|
|
|
128
138
|
constants from this file into tickets, docs, or chat transcripts.
|
|
129
139
|
|
|
130
140
|
## Change history
|
|
141
|
+
- 2026-08-26 — **Correction from a read-only prod check.** The "Non-Levy = id **38** on dev-sandbox
|
|
142
|
+
*and* prod" claim is wrong: prod has **no Non-Levy persona** and id 38 is **"MyDining- KDS"**, so
|
|
143
|
+
prod's auto-increment is past 38 and persona 1 is still named **"All"**. Reinforces resolve-by-name.
|
|
144
|
+
Also linked the new [bundle-grant workflow](../workflows/granting-persona-bundle-access.md), which
|
|
145
|
+
covers the *other* half of the persona model this doc does not: what a persona grant physically
|
|
146
|
+
consists of (`Personas_Bundles` + **`Personas_Items`** + `Personas_Assortments`) and that the
|
|
147
|
+
effective persona set is a UNION of the user's own personas, their location's, and their sector's.
|
|
148
|
+
(bala)
|
|
131
149
|
- 2026-08-13 — Split the `Users` row lifecycle out of this doc into
|
|
132
150
|
[PEOPLE-file User Lifecycle](./people-file-user-lifecycle.md) (duplicate accounts from the 5-day
|
|
133
151
|
lookup window; raw-SQL deactivation that fires no model hook). This doc stays scoped to persona /
|
|
@@ -18,7 +18,7 @@ project: _Underscore
|
|
|
18
18
|
client: compass-usa
|
|
19
19
|
type: profile
|
|
20
20
|
status: active
|
|
21
|
-
updated: 2026-08-
|
|
21
|
+
updated: 2026-08-26
|
|
22
22
|
owners: [jcardinal, bala, tcox, apeterson, dfranks]
|
|
23
23
|
files: []
|
|
24
24
|
related:
|
|
@@ -27,8 +27,10 @@ related:
|
|
|
27
27
|
- features/people-file-user-lifecycle.md
|
|
28
28
|
- workflows/persona-refactor-migration.md
|
|
29
29
|
- workflows/persona-population-env-comparison.md
|
|
30
|
+
- workflows/granting-persona-bundle-access.md
|
|
30
31
|
- features/mits-sales-order-transmission-alerting.md
|
|
31
32
|
- features/asn-to-item-fulfillment.md
|
|
33
|
+
- features/order-fulfillment-status-per-line.md
|
|
32
34
|
- features/cost-centers.md
|
|
33
35
|
- features/isfulfillable-data-quality-and-type-rule.md
|
|
34
36
|
- features/approval-decision-flow.md
|
|
@@ -132,8 +134,17 @@ separate, related client (see its own profile).
|
|
|
132
134
|
users got the full catalogue). Never trust pre-2026-08 Levy persona state as intentional.
|
|
133
135
|
- [Persona Refactor — 8-phase migration & deploy order](workflows/persona-refactor-migration.md)
|
|
134
136
|
(TRUE-75705) — splits persona 1 into Base (printers, everyone) + Non-Levy. **Run on dev-sandbox
|
|
135
|
-
only; production is NOT migrated
|
|
136
|
-
|
|
137
|
+
only; production is NOT migrated** (re-verified 2026-08-26: prod persona 1 is still "All", there is
|
|
138
|
+
no Non-Levy persona, and id 38 is now "MyDining- KDS"). worker2 must deploy first; Phase 8 (ACL
|
|
139
|
+
expression 235) ships with the toga2-commerce hack removal.
|
|
140
|
+
- [Granting a user access to a bundle (persona grant) + SuperUser](workflows/granting-persona-bundle-access.md)
|
|
141
|
+
— the recurring "give user X sight of kit N" request. **Five tables, not one**: a bundle grant
|
|
142
|
+
without `Personas_Items` renders a **half-visible kit**, because item-level ACL hides kit lines
|
|
143
|
+
whose item the persona cannot see. Also: a Compass persona needs **no** settings rows (all nine
|
|
144
|
+
persona-settings tables are empty across all 38 prod personas), "bundle 243" is `Bundles.number`
|
|
145
|
+
not `id`, and **`Users` has no `username` column** — it is `c_hrEmpUsername`, stored lowercase.
|
|
146
|
+
Super user = `Roles.id 5` added **additively** via `Users_Roles`, never replacing Base /
|
|
147
|
+
Compass Base.
|
|
137
148
|
- [Prod ↔ dev-sandbox persona comparison](workflows/persona-population-env-comparison.md) — the
|
|
138
149
|
full-population diff technique, the **join-on-email** rule (`Users.id` means different people per
|
|
139
150
|
environment), and the drift bundles (188/191/242) that mimic a catalogue grant.
|
|
@@ -153,6 +164,15 @@ separate, related client (see its own profile).
|
|
|
153
164
|
shared by Compass USA + Canada) counts **shipped only** — picked/packed never advance a Compass
|
|
154
165
|
order. Contrast Quad, which gets the full picked/packed machine. See
|
|
155
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).
|
|
156
176
|
- Built on the shared 2.0 Recursive Item Fulfillments engine (upstream mirroring).
|
|
157
177
|
- **`Items.isFulfillable` gates the storefront Qty Fulfilled cell (2026-07).** Sourced from
|
|
158
178
|
NetSuite onto the Agilant source item during the 1.0 item sync
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Granting a Compass user access to a bundle (persona grant) and the SuperUser role
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: compass-usa
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
|
|
13
|
+
related:
|
|
14
|
+
- ../features/persona-model-and-levy-gating.md
|
|
15
|
+
- ./persona-refactor-migration.md
|
|
16
|
+
- ./persona-population-env-comparison.md
|
|
17
|
+
- ../profile.md
|
|
18
|
+
- ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
|
|
19
|
+
- ../../../2.0/apps/_underscore/features/acl-permission-chain.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
"Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a
|
|
25
|
+
super user". It looks like a one-row insert into `Personas_Bundles`. It is not — a bundle grant
|
|
26
|
+
without the matching **item** grant renders a **half-visible kit**, because item-level ACL hides any
|
|
27
|
+
kit line whose item the persona cannot see.
|
|
28
|
+
|
|
29
|
+
A complete grant touches **five tables**. `dbchanges2/Client_Compass/2026-08-26a -
|
|
30
|
+
CompassCreativeStudioPersona.sql` is the working template: a 6-phase additive script that creates the
|
|
31
|
+
persona "Compass Creative Studio" (number 41), grants bundle 243, its 19 items and the 6 assortments
|
|
32
|
+
those items live in, assigns the persona to one user, and adds SuperUser.
|
|
33
|
+
|
|
34
|
+
All facts below were verified read-only against **production `Client_Compass` on 2026-08-26**.
|
|
35
|
+
|
|
36
|
+
## The five tables
|
|
37
|
+
|
|
38
|
+
| Table | Grants | Composite unique key? | Optional? |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| `Personas` (`id`, `uuid`, `name`, `number`) | the persona itself | — | no |
|
|
41
|
+
| `Personas_Bundles` (`personaId`, `bundleId`) | the kit appears | **yes** — re-run **errors** | no |
|
|
42
|
+
| `Personas_Items` (`personaId`, `itemId`) | the kit's **lines** are visible | **no** — re-run **silently duplicates** | **no — see below** |
|
|
43
|
+
| `Personas_Assortments` (`personaId`, `assortmentId`) | browsing the accessory categories | **no** — re-run **silently duplicates** | no |
|
|
44
|
+
| `Users_Personas` (`userId`, `personaId`) | the user holds the persona | **yes** — re-run **errors** | no |
|
|
45
|
+
|
|
46
|
+
That asymmetry is the thing to remember: the two bridges that most need a guard are exactly the two
|
|
47
|
+
that will not protect themselves.
|
|
48
|
+
|
|
49
|
+
### `Personas_Items` is not optional
|
|
50
|
+
|
|
51
|
+
Every `BundleItems.itemId` of the bundle must also be granted to the persona. Concretely: of bundle
|
|
52
|
+
243's **19** active line items, persona 1 ("All", which the user already held) covered only **2** —
|
|
53
|
+
so a bundle-only grant would have shown the kit with 17 of its lines missing.
|
|
54
|
+
|
|
55
|
+
### Derive the assortment set — never hardcode it
|
|
56
|
+
|
|
57
|
+
An assortment qualifies when it holds one of the bundle's items **and** the persona that owns the
|
|
58
|
+
bundle today already grants it. Joining it out means the new persona inherits exactly the browse
|
|
59
|
+
scope the current owner has, with no list to maintain:
|
|
60
|
+
|
|
61
|
+
```sql
|
|
62
|
+
SELECT DISTINCT ai.assortmentId
|
|
63
|
+
FROM Bundles b
|
|
64
|
+
INNER JOIN BundleItems bi ON bi.bundleId = b.id AND bi.isActive = 1
|
|
65
|
+
INNER JOIN AssortmentItems ai ON ai.itemId = bi.itemId
|
|
66
|
+
INNER JOIN Personas_Bundles pbSource ON pbSource.bundleId = b.id AND pbSource.personaId <> @personaId
|
|
67
|
+
INNER JOIN Personas_Assortments pa ON pa.assortmentId = ai.assortmentId AND pa.personaId = pbSource.personaId
|
|
68
|
+
WHERE b.number = '243'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The `personaId <> @personaId` exclusion matters: by the time this phase runs, the **new** persona
|
|
72
|
+
also owns the bundle, and without it the join would feed on itself.
|
|
73
|
+
|
|
74
|
+
For bundle 243 this resolves to assortments **2** (Monitors), **4** (Computer Accessories), **6**
|
|
75
|
+
(Apple Accessories), **8** (Keyboard/Keypad & Mouse), **10** (Cables) and **19** (Computers).
|
|
76
|
+
**An inactive assortment in the set is normal** — 19 is `isActive = 0` and is still granted, because
|
|
77
|
+
the grant mirrors the source persona rather than judging the data.
|
|
78
|
+
|
|
79
|
+
## A new Compass persona needs NO settings rows
|
|
80
|
+
|
|
81
|
+
Verified across **all 38** personas in prod: `PersonaAppSettings`, `PersonaGlobalSettings`,
|
|
82
|
+
`PersonaPageSettings`, `PersonaRecordSettings`, `PersonaSectionSettings`,
|
|
83
|
+
`PersonaRecordFieldSettings`, `PersonaTranslations`, `Personas_Currencies` and
|
|
84
|
+
`Personas_VendorItems` are **all empty — 0 rows**. A Compass persona is purely **catalogue scoping**
|
|
85
|
+
(bundles / items / assortments). Do not go hunting for supporting configuration; there is none.
|
|
86
|
+
|
|
87
|
+
## Persona numbers in prod (2026-08-26)
|
|
88
|
+
|
|
89
|
+
**38 personas.** Numbers in use: **1-23, 27-40, and 100** ("QA Full Access", id 37). **The next free
|
|
90
|
+
number is 41.** `Personas.number` is a *string* column, so quote it.
|
|
91
|
+
|
|
92
|
+
## "Bundle 243" is `Bundles.number`, not `Bundles.id`
|
|
93
|
+
|
|
94
|
+
The business always quotes the **number**. Bundle **243** is `Bundles.id` **171** ("Compass Creative
|
|
95
|
+
Studio MacBook"). Resolve it in the query (`WHERE b.number = '243'`) rather than pasting an id, and
|
|
96
|
+
never assume the two match.
|
|
97
|
+
|
|
98
|
+
## Finding the user
|
|
99
|
+
|
|
100
|
+
**`Client_Compass.Users` has no `username` column** — the natural first query fails with
|
|
101
|
+
`Unknown column 'username'`. The Compass login / personnel username lives in **`c_hrEmpUsername`**,
|
|
102
|
+
and it is stored **lowercase**: people quote it uppercase ("KINGK01") but the row says `kingk01`.
|
|
103
|
+
Look users up by `email` or `c_hrEmpUsername` (lowercased), which is also how the nightly PEOPLE cron
|
|
104
|
+
matches them — see [PEOPLE-File User Lifecycle](../features/people-file-user-lifecycle.md).
|
|
105
|
+
|
|
106
|
+
## Making someone a super user
|
|
107
|
+
|
|
108
|
+
There is **no `isSuperUser` flag on `Users`** — it is a role row. `Client_Compass.Roles.id` **5**,
|
|
109
|
+
name **`SuperUser`** (113 holders as of 2026-08-26), granted **additively** through
|
|
110
|
+
`Users_Roles (userId, roleId)`, which is unique on the pair.
|
|
111
|
+
|
|
112
|
+
**SuperUser is never a replacement for the base roles.** The established prod pattern for a Compass
|
|
113
|
+
super user is **Base (1) + SuperUser (5) + Compass Base (7)**, optionally **+ Manager (8)**. Role
|
|
114
|
+
permissions resolve as a **union** (see
|
|
115
|
+
[ACL Permission Chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md)), so adding
|
|
116
|
+
role 5 while removing 1 or 7 takes access *away*.
|
|
117
|
+
|
|
118
|
+
## Verify the way the ACL actually resolves personas
|
|
119
|
+
|
|
120
|
+
A user's **effective** persona set is three UNIONed sources, not just `Users_Personas`:
|
|
121
|
+
|
|
122
|
+
1. `Users_Personas` — personas held directly.
|
|
123
|
+
2. `Locations_Personas` for the user's own `locationId`.
|
|
124
|
+
3. `Locations_Personas` on the user's **sector** — five `parentLocationId` hops up
|
|
125
|
+
(location → complex → district → region → division → sector).
|
|
126
|
+
|
|
127
|
+
**Check all three before adding anything** — the user may already reach the bundle through their
|
|
128
|
+
location, in which case the right answer is "no change needed". After the grant, re-run the same
|
|
129
|
+
resolution and confirm the bundle number appears; the verification block at the end of the template
|
|
130
|
+
file does exactly this. The same three-source resolution is what the full-population comparison
|
|
131
|
+
tooling uses — see
|
|
132
|
+
[Prod vs dev-sandbox persona comparison](./persona-population-env-comparison.md).
|
|
133
|
+
|
|
134
|
+
## Gotchas
|
|
135
|
+
|
|
136
|
+
- **⚠ Re-running the whole file creates a SECOND persona.** Phase 1's `INSERT INTO Personas` has no
|
|
137
|
+
guard, so a second run inserts another persona with the same name and number, and the child phases
|
|
138
|
+
then hang a full set of rows off it. The child phases are individually re-run safe; the **file is
|
|
139
|
+
not idempotent**. To repeat any phase, set `@personaId` to the existing persona id by hand and skip
|
|
140
|
+
Phase 1.
|
|
141
|
+
- **`@personaId` is a session variable — run every phase in ONE session.** A reconnect between phases
|
|
142
|
+
leaves it `NULL`.
|
|
143
|
+
- **`SELECT DISTINCT` will not dedupe a row carrying a generated uuid.** The per-row random value
|
|
144
|
+
makes every row distinct. Dedupe the id set in a derived table and generate the uuid in the outer
|
|
145
|
+
select — full explanation and the UUID4 expression in
|
|
146
|
+
[Re-runnable additive INSERTs](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md).
|
|
147
|
+
- **⚠ Do not write this SQL against the persona-refactor target state.** As of 2026-08-26 the refactor
|
|
148
|
+
has **not** shipped to prod: persona 1 is still named **"All"** (not "Base"), there is **no
|
|
149
|
+
"Non-Levy" persona**, and persona **number 40 belongs to id 38 "MyDining- KDS"**. See
|
|
150
|
+
[Persona Refactor](./persona-refactor-migration.md).
|
|
151
|
+
- **Everything here is additive.** The user keeps persona 1 and their existing roles; nothing is
|
|
152
|
+
removed. A "remove access" request is a different, destructive job and needs its own review.
|
|
153
|
+
|
|
154
|
+
## Change history
|
|
155
|
+
- 2026-08-26 — Initial: documented the five-table bundle grant (and that **`Personas_Items` is
|
|
156
|
+
mandatory** — item-level ACL hides kit lines whose item the persona cannot see; persona 1 covered
|
|
157
|
+
only 2 of bundle 243's 19 items), the unique-key asymmetry that makes `Personas_Items` /
|
|
158
|
+
`Personas_Assortments` duplicate silently on re-run, the **derived** assortment join, and that
|
|
159
|
+
**every persona settings table in prod is empty across all 38 personas** so a new persona needs no
|
|
160
|
+
config. Added the `Bundles.number` vs `Bundles.id` trap, the missing `Users.username` column
|
|
161
|
+
(`c_hrEmpUsername`, lowercase), the SuperUser role recipe (`Roles.id 5`, additive, Base + SuperUser
|
|
162
|
+
+ Compass Base), and the three-source effective-persona verification. Template file:
|
|
163
|
+
`2026-08-26a - CompassCreativeStudioPersona.sql` (written, not executed). (bala)
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-26
|
|
10
10
|
owners: [bala]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Compass/2026-08-06 - PersonaRefactor.sql
|
|
@@ -16,6 +16,7 @@ files:
|
|
|
16
16
|
related:
|
|
17
17
|
- ../features/persona-model-and-levy-gating.md
|
|
18
18
|
- ./persona-population-env-comparison.md
|
|
19
|
+
- ./granting-persona-bundle-access.md
|
|
19
20
|
- ../profile.md
|
|
20
21
|
---
|
|
21
22
|
|
|
@@ -29,6 +30,32 @@ Levy-sector users). One SQL file, **8 phases**, plus a companion verification sc
|
|
|
29
30
|
> (toga2-commerce #306, dbchanges2 #368, worker2 #43) were closed and the work rewritten against
|
|
30
31
|
> current code and data — treat any plan or skill text predating 2026-08-06 as stale.
|
|
31
32
|
|
|
33
|
+
### ⚠ Everything below Phase 1 describes a TARGET state. Prod is not in it (re-verified 2026-08-26)
|
|
34
|
+
|
|
35
|
+
The phase table, the persona ids and the post-migration figures in this doc describe what prod will
|
|
36
|
+
look like **after** the migration. Read read-only against production `Client_Compass` on
|
|
37
|
+
**2026-08-26**, prod is still pre-migration:
|
|
38
|
+
|
|
39
|
+
| Claim you might carry over | Production reality, 2026-08-26 |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Persona 1 is named "Base" | Persona 1 is still named **"All"** |
|
|
42
|
+
| A "Non-Levy" persona exists | **It does not exist in prod at all** |
|
|
43
|
+
| Non-Levy will land on `Personas.id` **38** | **Id 38 is already taken** — it is **"MyDining- KDS"** |
|
|
44
|
+
| Phase 1's hardcoded `number = '40'` is free | **Number 40 is in use** (by id 38). Numbers **1-23, 27-40 and 100** are taken across **38** personas; **41 is the next free number** |
|
|
45
|
+
|
|
46
|
+
Two consequences before this is ever run in prod:
|
|
47
|
+
|
|
48
|
+
1. **Phase 1's hardcoded `number = '40'` must be re-checked** — it would create a second persona
|
|
49
|
+
carrying a number that already belongs to "MyDining- KDS".
|
|
50
|
+
2. **The "id will be 38" reasoning no longer holds in prod.** `Personas.AUTO_INCREMENT` has moved
|
|
51
|
+
past 38 since 2026-08-06, so the new persona will get some higher id. This is exactly why
|
|
52
|
+
`PeopleFile` resolves Non-Levy **by name** — see
|
|
53
|
+
[Persona Model & Levy-Sector Gating](../features/persona-model-and-levy-gating.md). Never hardcode
|
|
54
|
+
the id, and re-read prod rather than this table before running.
|
|
55
|
+
|
|
56
|
+
**Any other Compass persona work must target prod-as-it-is, not this target state** — see
|
|
57
|
+
[Granting a Compass user access to a bundle](./granting-persona-bundle-access.md).
|
|
58
|
+
|
|
32
59
|
The persona semantics and the Levy-detection code this depends on live in
|
|
33
60
|
[Persona Model & Levy-Sector Gating](../features/persona-model-and-levy-gating.md). Read that first;
|
|
34
61
|
the migration is meaningless without the `isLevySector` fix.
|
|
@@ -130,6 +157,12 @@ All checks are written as **invariants** or as comparisons against a captured **
|
|
|
130
157
|
| Fixed verification user list (HAQIKAH.AARON, KYLE.AARON) | HAQIKAH.AARON is inactive, KYLE.AARON does not exist — **find test users dynamically** |
|
|
131
158
|
|
|
132
159
|
## Change history
|
|
160
|
+
- 2026-08-26 — **Correction, not new work.** Re-verified read-only against prod: the refactor is
|
|
161
|
+
**still not live**. Prod persona 1 is named **"All"** (not "Base"), there is **no Non-Levy
|
|
162
|
+
persona**, and **id 38 is now "MyDining- KDS"** — so the "Non-Levy = id 38" reasoning and Phase 1's
|
|
163
|
+
hardcoded `number = '40'` (number 40 belongs to id 38) are both stale for prod. Prod holds 38
|
|
164
|
+
personas using numbers 1-23, 27-40 and 100; next free number is 41. Added the target-state warning
|
|
165
|
+
block so nobody writes prod SQL against the post-migration table. (bala)
|
|
133
166
|
- 2026-08-06 — Initial: rewritten 8-phase migration (earlier draft's Phase 5+9 collapsed into one
|
|
134
167
|
Levy-excluding assignment; Phase 6 restricted to active users; Phase 7f deletes the retired persona
|
|
135
168
|
by name; Phase 8 makes ACL expression 235 purely additive). Documented the B2 verification-order
|
package/package.json
CHANGED