toga-ai 1.0.601 → 1.0.603
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/netsuite-togasupply-per-client-sync.md +39 -1
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/recursive-item-fulfillments.md +49 -6
- package/knowledge/clients/compass-usa/features/sales-order-line-renumbering.md +58 -2
- 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-18
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -27,6 +27,7 @@ related:
|
|
|
27
27
|
- ../../library/features/toga2-api-client-and-bridge.md
|
|
28
28
|
- ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
29
29
|
- ../../../clients/elite/features/netsuite-togasupply-sync.md
|
|
30
|
+
- ../../../clients/compass-usa/features/sales-order-line-renumbering.md
|
|
30
31
|
---
|
|
31
32
|
|
|
32
33
|
## Summary
|
|
@@ -402,9 +403,46 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
402
403
|
`itemFulfillmentItems` or `invoiceItems` reverse-hasMany collections (only the PO-link bridges), so
|
|
403
404
|
the fix keys off the recorded "could-not-delete" set rather than trusting those collections — see
|
|
404
405
|
[App_Api_Toga2 client & bridge](../../library/features/toga2-api-client-and-bridge.md).
|
|
406
|
+
- **⚠ A second, distinct poison-order variant froze SALES_ORDERS again on SO 14106 (fixed
|
|
407
|
+
2026-08-18) — this one was on the api2/2.0 side, not in `library`.** Fixing the 2026-08-17
|
|
408
|
+
renumber-DELETE bug let Compass USA's sync advance past SO 7447 (`06:13:26`) to SO **14106**
|
|
409
|
+
(`06:30:41`), which then threw for a completely different reason: the api2 `PUT /sales-orders`
|
|
410
|
+
fired `_Model_Compass_SalesOrder::postPut()` → `_Model_Compass_SalesOrderItem::renumberLineNumbers()`,
|
|
411
|
+
whose **single-UPDATE renumber raised MySQL 1062 duplicate-key** on any order whose `id`-order ≠
|
|
412
|
+
`lineNumber`-order (SO 14106: `id 86674` line 19→6 collided with `id 86675` still on 6). The 1062
|
|
413
|
+
fataled the api2 request, `App_Api_Toga2::send()` threw, and — same as before — the
|
|
414
|
+
`try/catch`-less SALES_ORDERS loop aborted the whole section:
|
|
415
|
+
`NETSUITE_EXECUTION_MODE_SALES_ORDERS='1-RUNNING'`, watermark frozen at `2026-08-12 06:30:41`
|
|
416
|
+
(~6 days), other five sections healthy at `432000-IDLE`. **Root-cause fix is in `_underscore`
|
|
417
|
+
(2.0), not this worker** — the renumber is now two-phase (park negative, flip positive); full
|
|
418
|
+
detail in [Compass sales-order line-number renumbering](../../../clients/compass-usa/features/sales-order-line-renumbering.md).
|
|
419
|
+
The worker/1.0 side is only the trigger path (unchanged). **Lesson: the missing SALES_ORDERS
|
|
420
|
+
per-record `try/catch` means every future api2-side write error on a single order re-freezes the
|
|
421
|
+
section** — the poison-order class is not closed by fixing individual orders.
|
|
422
|
+
- **⚠ Diagnostic trap: a fatal api2 PUT leaves NO row in `Logs_<Client>.Api` — check `Logs.Issue`
|
|
423
|
+
instead.** When the api2 request fatals *before* the request-logging middleware (as the 1062
|
|
424
|
+
renumber collision does), it writes **nothing** to `Logs_Compass.Api`. Do **not** conclude "no
|
|
425
|
+
failing api2 call happened" from an empty per-client Api log. The error surfaces only in the shared
|
|
426
|
+
`Logs.Issue` tracker (here **Issue 246**, first seen 2026-08-17 22:16), with the full trace
|
|
427
|
+
pointing at `renumberLineNumbers ← postPut ← V2.php processRoutePairs`. Localize the stalled
|
|
428
|
+
section from `Client_<Client>.Parameters` (`NETSUITE_EXECUTION_MODE_*` RUNNING-vs-IDLE +
|
|
429
|
+
`NETSUITE_LAST_SYNC_DATETIME_*` frozen watermark), then read `Logs.Issue` — not `Logs_<Client>.Api`
|
|
430
|
+
— for the throwing order.
|
|
405
431
|
|
|
406
432
|
## Change history
|
|
407
433
|
|
|
434
|
+
- 2026-08-18 — Recorded the **next poison-order variant** that re-froze Compass USA's SALES_ORDERS
|
|
435
|
+
section on SO **14106** right after the 2026-08-17 fix let the sync advance past SO 7447. This one
|
|
436
|
+
is 2.0-side: the api2 `PUT /sales-orders` → `postPut` → `renumberLineNumbers()` **single-UPDATE**
|
|
437
|
+
renumber threw MySQL **1062** on any order whose `id`-order ≠ `lineNumber`-order, fataling the api2
|
|
438
|
+
request; the `try/catch`-less SALES_ORDERS loop aborted the section (mode `1-RUNNING`, watermark
|
|
439
|
+
stuck `2026-08-12 06:30:41`, ~6 days). Root-cause fix is in **`_underscore`** (two-phase renumber —
|
|
440
|
+
see the [renumbering doc](../../../clients/compass-usa/features/sales-order-line-renumbering.md));
|
|
441
|
+
this worker is only the trigger and is unchanged. Also recorded the **diagnostic trap**: the fatal
|
|
442
|
+
PUT wrote **no** `Logs_Compass.Api` row (it died before the logging middleware), so the failure was
|
|
443
|
+
visible only in shared `Logs.Issue` (Issue 246) — do not read an empty per-client Api log as "no
|
|
444
|
+
failing call." Reinforced that the missing SALES_ORDERS per-record `try/catch` keeps the
|
|
445
|
+
poison-order class open. Diagnosis only on this repo (read-only). (jcardinal)
|
|
408
446
|
- 2026-08-17 — Fixed a Compass USA ~5-day SALES_ORDERS freeze: an **invoiced** line that NetSuite
|
|
409
447
|
renumbered could not be DELETEd (FK `InvoiceItems.salesOrderItemId` `RESTRICT` → MySQL 1451 →
|
|
410
448
|
api2 EV-11), and because the SALES_ORDERS loop has **no per-record `try/catch`** the throw aborted
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Model.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
|
|
36
36
|
| [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
|
|
37
37
|
| [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
|
|
38
|
-
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
|
|
38
|
+
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql, dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql |
|
|
39
39
|
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
|
|
40
40
|
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.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, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
|
|
41
41
|
| [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-18
|
|
10
10
|
owners: [jcardinal, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/ItemFulfillment.php
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
- dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql
|
|
18
18
|
- dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql
|
|
19
19
|
- dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql
|
|
20
|
+
- dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql
|
|
20
21
|
related:
|
|
21
22
|
- ../architecture.md
|
|
22
23
|
- ../../api2/architecture.md
|
|
@@ -55,8 +56,10 @@ fire those records' interceptors) no-op. A `while` loop walks UP one level at a
|
|
|
55
56
|
1. Load downstream IF; no `salesOrderId` (transfer order) → return null (out of scope).
|
|
56
57
|
2. Resolve upstream SO via the **header walk** (bridge tables); none → null (top of chain).
|
|
57
58
|
3. Build desired upstream items: map each downstream IFI's SOI up via the **item walk**,
|
|
58
|
-
GROUP BY upstream SOI, sum fulfilled qty, then **scale into upstream order units**
|
|
59
|
-
|
|
59
|
+
GROUP BY upstream SOI, sum fulfilled qty, then **scale into upstream order units** — but
|
|
60
|
+
only for genuine multi-component bundles, keyed on **distinct downstream items** (see
|
|
61
|
+
Gotchas → Bundle scaling). A single distinct downstream item (incl. accepted over-ship)
|
|
62
|
+
mirrors unscaled.
|
|
60
63
|
4. Resolve-or-create the upstream IF (**eager** — header-only POST still builds the chain):
|
|
61
64
|
load existing via `upstreamItemFulfillmentId` and PUT changed header fields, or POST a
|
|
62
65
|
new IF with mirrored header + upstream SO then PUT the downstream IF's link.
|
|
@@ -95,9 +98,36 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
|
|
|
95
98
|
`…Put.sql` for POST/PUT) against that env's Core DB — the PHP won't fire on PUT otherwise.
|
|
96
99
|
- **`depth: -1` always** on orchestration calls. `depth => 0` means *unlimited* FK
|
|
97
100
|
traversal → OOM. Never use 0.
|
|
98
|
-
- **Bundle scaling
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
- **Bundle scaling is gated on DISTINCT DOWNSTREAM ITEMS, not downstream SOI lines
|
|
102
|
+
(corrected 2026-08-18).** A "Recipe" query, per upstream SOI, takes **one ordered-qty slot
|
|
103
|
+
per distinct downstream `itemId`** (MAX ordered qty across duplicate same-item sourcing
|
|
104
|
+
lines) and counts the downstream lines. Scaling
|
|
105
|
+
(`upstreamQty = totalDownstreamFulfilled × upstreamOrderedQty / Σ distinctDownstreamOrderedQty`,
|
|
106
|
+
**capped at the raw sum** so it can only reduce, never inflate) applies **only when
|
|
107
|
+
`distinctDownstreamItemCount > 1`** — a genuine multi-component bundle. A qty-1 bundle of 6
|
|
108
|
+
fully-shipped distinct components still → upstream qty 1 (6×1/6); 3×2/6=1 preserved. When a
|
|
109
|
+
single distinct downstream item is sourced (incl. an accepted over-shipment split across
|
|
110
|
+
several downstream SOs/SOIs), the raw fulfilled qty mirrors up **unscaled** (round() applied
|
|
111
|
+
symmetrically on this branch too).
|
|
112
|
+
- **Same-item over-shipment used to mis-scale into fractions (root cause, fixed 2026-08-18).**
|
|
113
|
+
Pre-fix the denominator summed ordered qty over *distinct downstream SOI lines*. A vendor
|
|
114
|
+
(e.g. Office Depot / ODP) can create **multiple downstream SalesOrders for the SAME item off
|
|
115
|
+
ONE upstream PO line** (accepted over-ship / re-send); each duplicate is another distinct
|
|
116
|
+
downstream SOI, inflating the denominator and shrinking the factor. Compass SA135471's hero
|
|
117
|
+
SOI (item 2682, qty 1) mapped to two ODP SOIs (same item, each qty 1) via one POI, so factor
|
|
118
|
+
= 1/2 and each of two customer IFs showed **qty 0.50** ("Quantity Fulfilled" 0.5 + 0.5). Two
|
|
119
|
+
physically distinct serials had shipped — a real over-ship, not a phantom duplicate. Fix:
|
|
120
|
+
distinct-item denominator (above) mirrors the raw qty (→ 2). Data repaired by
|
|
121
|
+
`dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql`.
|
|
122
|
+
- **Mixed topology (a real bundle whose component is ALSO sourced across multiple downstream
|
|
123
|
+
lines) is logged, not silently averaged.** When the Recipe's `lineCount > distinctItemCount`,
|
|
124
|
+
the engine `error_log()`s a diagnostic instead of averaging — same log-don't-swallow rule as
|
|
125
|
+
the broken-bridge guard.
|
|
126
|
+
- **Diagnosing over-ship vs. phantom duplicate:** traverse the bridge — upstream SOI →
|
|
127
|
+
`SalesOrderItems_PurchaseOrderItems` → POI → `PurchaseOrders_SalesOrders` → downstream ODP
|
|
128
|
+
SOs (`customerId=1`); compare downstream vs. upstream `itemId` (**same itemId ⇒ over-ship /
|
|
129
|
+
duplicate sourcing, NOT a bundle**); then check `ItemFulfillmentItemUnits` serials — distinct
|
|
130
|
+
serials ⇒ genuine over-shipment, identical ⇒ phantom duplicate.
|
|
101
131
|
- **DELETE propagation is NOT implemented.** The engine sets `$outData = null` on DELETE so
|
|
102
132
|
post-delete hooks never fire; a standalone downstream DELETE doesn't immediately remove its
|
|
103
133
|
upstream mirror. A later reconciling PUT cleans up stale records via set comparison. Full
|
|
@@ -131,6 +161,19 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
|
|
|
131
161
|
upstream record is written at most once — idempotent). Fine for normal fulfillment sizes.
|
|
132
162
|
|
|
133
163
|
## Change history
|
|
164
|
+
- 2026-08-18 — **Bundle scaling now discriminates on distinct downstream items, not SOI lines
|
|
165
|
+
(CTO-approved "Option B").** `reconcileUpstreamLevel`'s Recipe query takes one ordered-qty
|
|
166
|
+
slot per distinct downstream `itemId` and scales only when `distinctDownstreamItemCount > 1`;
|
|
167
|
+
a single distinct item (incl. accepted over-ship split across multiple downstream SOs)
|
|
168
|
+
mirrors the raw fulfilled qty unscaled. Mixed topology (recipe lineCount > distinctItemCount)
|
|
169
|
+
is `error_log`'d, not averaged. Added `(int)` casts on the method's trusted-PK SQL
|
|
170
|
+
interpolations and round() symmetry on the unscaled branch. Root-caused on Compass prod
|
|
171
|
+
SA135471: the hero item (2682, qty 1) sourced via two ODP SalesOrders off one PO line showed
|
|
172
|
+
"Quantity Fulfilled" 0.5 + 0.5 across two customer IFs (old denominator summed distinct SOI
|
|
173
|
+
lines → factor 1/2); two distinct serials confirmed a genuine over-ship. Data repaired by the
|
|
174
|
+
idempotent retro migration `dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql`
|
|
175
|
+
(IFI 302121 / 302126 quantity 0.50 → 1.00). Deploy order: `_underscore` code first, then the
|
|
176
|
+
SQL against the Compass DB. Neither deployed at capture time. (jcardinal)
|
|
134
177
|
- 2026-07-02 — Added a **broken-bridge guard** to `reconcileUpstreamLevel`: when `$desiredItems`
|
|
135
178
|
is empty but the downstream IF has items (broken SOI↔POI bridge), it logs and returns null
|
|
136
179
|
instead of creating an empty header-only duplicate upstream IF. Root-caused on Compass SA133377
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["bala"]
|
|
9
|
+
updated: 2026-08-18
|
|
10
|
+
owners: ["bala", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/SalesOrderItem.php
|
|
13
13
|
- _underscore/Model/Compass/SalesOrder.php
|
|
@@ -43,6 +43,29 @@ Live production wiring (`ApiPayloadInterceptors`, Compass):
|
|
|
43
43
|
Add/edit renumbering rides the **parent** (`sales-orders`) hooks; delete renumbering rides the
|
|
44
44
|
**child**'s `preDelete`.
|
|
45
45
|
|
|
46
|
+
### How the renumber SQL must run — two phases, never one UPDATE (2026-08-18)
|
|
47
|
+
|
|
48
|
+
Lines are numbered `1..N` by `ROW_NUMBER() OVER (ORDER BY id)`, but the write **cannot** be a
|
|
49
|
+
single `UPDATE … JOIN (SELECT id, ROW_NUMBER() … AS rowNumber) SET lineNumber = rowNumber`.
|
|
50
|
+
`SalesOrderItems` carries `UNIQUE(salesOrderId, lineNumber)`, which MySQL enforces **per row as
|
|
51
|
+
each row updates**, not deferred to statement end. Whenever `id`-order ≠ current `lineNumber`-order,
|
|
52
|
+
a row that takes a number another **not-yet-updated** row still holds raises **MySQL 1062
|
|
53
|
+
duplicate-key** mid-statement and the whole UPDATE (and its request) fatals.
|
|
54
|
+
|
|
55
|
+
> Observed: SO **14106**, `id 86674` moving line 19→**6** collided with `id 86675` still sitting on
|
|
56
|
+
> line 6 → `Duplicate entry '14106-6' for key 'salesOrderId'`.
|
|
57
|
+
|
|
58
|
+
The collision-free form (same numbering outcome, `ROW_NUMBER() OVER (ORDER BY id)` unchanged) is a
|
|
59
|
+
**two-phase** renumber:
|
|
60
|
+
|
|
61
|
+
1. **Park negative** — `SET lineNumber = -Temporary.rowNumber` for every line. Negatives never
|
|
62
|
+
collide with the positive originals or finals, and each `-rowNumber` is distinct.
|
|
63
|
+
2. **Flip positive** — `SET lineNumber = -lineNumber WHERE … AND lineNumber < 0`.
|
|
64
|
+
|
|
65
|
+
Values only ever range `-N..N`, safe for the `SMALLINT` `lineNumber` column. Both UPDATEs run in
|
|
66
|
+
the **same api2 request → same `DB_CLIENT` transaction**, so the intermediate negative state is
|
|
67
|
+
never externally visible and the pair rolls back together on failure. Signature unchanged (PHP 8.1).
|
|
68
|
+
|
|
46
69
|
## Why delete-time renumbering CANNOT be a `postDelete`
|
|
47
70
|
|
|
48
71
|
A `postDelete` interceptor can never fire at all. api2's post-processing interceptor block runs
|
|
@@ -87,9 +110,42 @@ fire reliably as the request's **first-resolved record**. Verified healthy after
|
|
|
87
110
|
subclasses inherit it. Do not add per-region copies.
|
|
88
111
|
- **These interceptor rows have no route and no audit trail.** They are direct SQL only; nothing
|
|
89
112
|
records who added or removed one. Snapshot Compass's row set into the ticket before changing it.
|
|
113
|
+
- **⚠ The renumber must never be a single collision-prone UPDATE.** `UNIQUE(salesOrderId,
|
|
114
|
+
lineNumber)` is enforced per-row mid-statement, so a one-shot `SET lineNumber = rowNumber` throws
|
|
115
|
+
MySQL 1062 the moment any row takes a number a not-yet-updated row still holds. Renumber in two
|
|
116
|
+
phases (park negative, then flip positive) — see *How it works*.
|
|
117
|
+
- **Both Compass regions are affected.** `renumberLineNumbers()` lives on the shared
|
|
118
|
+
`_Model_Compass_SalesOrderItem` base, so **Compass Canada** (`compass-canada`) inherits the same
|
|
119
|
+
bug and the same fix; there is no per-region copy to touch.
|
|
120
|
+
- **⚠ The `preDelete` (delete-a-middle-line) path is a pre-existing latent collision — unchanged by
|
|
121
|
+
this fix.** When a middle line is deleted, the row being removed keeps its positive `lineNumber`
|
|
122
|
+
during the renumber, so `preDelete` can still hit the same 1062 as the old add/edit code did. It
|
|
123
|
+
is healthy in practice only because deletes are almost always the **last** line. If middle-line
|
|
124
|
+
deletes become common, `preDelete` needs the same two-phase treatment.
|
|
125
|
+
- **This was not a one-order anomaly.** As of 2026-08-18, **38,947 of 105,169** Compass orders (37%)
|
|
126
|
+
have `lineNumber`-order ≠ `id`-order (`ROW_NUMBER() OVER (PARTITION BY salesOrderId ORDER BY id)`)
|
|
127
|
+
— any of them ran the buggy renumber on its next PUT/edit and could collide. No mass data retrofix
|
|
128
|
+
is needed for the freeze: the two-phase renumber normalizes each order as the sync re-touches it.
|
|
129
|
+
A post-deploy sweep of that set could confirm whether any were left with gap'd numbering (which
|
|
130
|
+
MITS/PO linking depends on — see [MITS PO → SO item linking](./mits-po-to-so-item-linking.md))
|
|
131
|
+
from silently-rolled-back renumbers.
|
|
90
132
|
|
|
91
133
|
## Change history
|
|
92
134
|
|
|
135
|
+
- 2026-08-18 — **Fixed** the `renumberLineNumbers()` single-UPDATE **1062 duplicate-key collision**.
|
|
136
|
+
The renumber ran as one `UPDATE … JOIN (SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rowNumber)
|
|
137
|
+
SET lineNumber = rowNumber`; because `UNIQUE(salesOrderId, lineNumber)` is enforced per-row
|
|
138
|
+
mid-statement, any order whose `id`-order ≠ `lineNumber`-order fataled with `Duplicate entry`
|
|
139
|
+
(observed SO 14106, `id 86674` line 19→6 vs `id 86675` still on line 6). Replaced with a
|
|
140
|
+
**two-phase** renumber (Phase 1 parks each line at `-rowNumber`, Phase 2 flips positive) that keeps
|
|
141
|
+
the exact `ROW_NUMBER() OVER (ORDER BY id)` outcome, never collides, stays within the `SMALLINT`
|
|
142
|
+
range, and runs both UPDATEs in the same api2 `DB_CLIENT` transaction (atomic, no visible negative
|
|
143
|
+
state). Shared base class → **Compass Canada inherits the fix**. Systemic scope: 38,947/105,169
|
|
144
|
+
(37%) Compass orders currently have out-of-order numbering and were all exposed. Noted the
|
|
145
|
+
pre-existing `preDelete` middle-line-delete path can still collide identically (unchanged, latent).
|
|
146
|
+
This collision was the root cause of the Compass USA SALES_ORDERS sync freeze on SO 14106 — cascade
|
|
147
|
+
+ diagnosis recorded in the [NetSuite → TOGa Supply per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
148
|
+
(Fix confirmed live in prod; local working tree still shows the pre-fix single UPDATE.) (jcardinal)
|
|
93
149
|
- 2026-08-11 — Documented the as-built wiring and the reasoning behind it after a production
|
|
94
150
|
post-mortem (investigation only, no code change): add/edit renumbering hangs off
|
|
95
151
|
`_Model_Compass_SalesOrder::postPost`/`postPut` (record **14** `sales-orders`), delete-time
|
package/package.json
CHANGED