toga-ai 1.0.602 → 1.0.604
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/features/email-template-sending.md +17 -1
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/environment-variable-drives-underscore-branch.md +26 -1
- 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
|
|
@@ -7,7 +7,7 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-08-03
|
|
10
|
-
owners: ["jcardinal", "bala", "mhammontree"]
|
|
10
|
+
owners: ["jcardinal", "bala", "mhammontree", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/EmailTemplate.php
|
|
13
13
|
- _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php
|
|
@@ -98,6 +98,16 @@ can keep using `sendEmail($api, ...)`.
|
|
|
98
98
|
2026-06-15 the only entry point was `sendEmail(&$api, ...)`, so callers with no API
|
|
99
99
|
context faked one: `$api = (object)['client' => (object)['clientIdentifier' => …]]`.
|
|
100
100
|
That hack is obsolete — pass the identifier to `send()` instead.
|
|
101
|
+
- **⚠ Template vars MUST be spread into the variadic, never passed as a bare array.** Both
|
|
102
|
+
`sendEmail(&$api, $uuid, $to, $cc, $bcc, ...$args)` and `send(...)` collect the template
|
|
103
|
+
variables via `...$args`. Pass them **spread** so PHP 8.1+ named-argument collection yields
|
|
104
|
+
string keys/string values (`sendEmail($api, $uuid, $to, [], [], ...['userName'=>$name, ...])`
|
|
105
|
+
→ `$args = ['userName'=>$name, ...]`). Passing the map as a single positional argument
|
|
106
|
+
(`sendEmail($api, $uuid, $to, [], [], $vars)`) makes `$args = [0 => [...]]` — a numeric key
|
|
107
|
+
whose value is an *array*. `replaceTemplateVariables()` then runs `str_replace('{0}', <array>, …)`
|
|
108
|
+
→ `TypeError: str_replace(): Argument #2 must be of type string, array given`, surfacing as a
|
|
109
|
+
production 500 / **EO-1**. `Model/Compass/*` gets this right (spread); the four `Model/Quad/*`
|
|
110
|
+
call sites were copied from Compass but dropped the `...` — fixed 2026-08-18.
|
|
101
111
|
- **`sendEmail()`'s signature is load-bearing for scripted APIs** — the Record Script engine
|
|
102
112
|
(`api2/Component/Api/V2/V2.php`, ~line 3594) calls the method with `api` as a named
|
|
103
113
|
argument, so the first param must stay `&$api`. Do not "clean it up" by removing it.
|
|
@@ -151,6 +161,12 @@ worker method) in-process instead.
|
|
|
151
161
|
|
|
152
162
|
## Change history
|
|
153
163
|
|
|
164
|
+
- 2026-08-18 — Fixed the Quad order-approval/rejection emails' production 500
|
|
165
|
+
(`str_replace(): Argument #2 must be string`, EO-1): the four `Model/Quad/ApprovalDecision.php`
|
|
166
|
+
+ `Model/Quad/SalesOrder.php` call sites passed the template-vars map as a bare positional
|
|
167
|
+
array, so `...$args` collected `[0 => [...]]` and `replaceTemplateVariables()` fed an array to
|
|
168
|
+
`str_replace`. Fix = spread the array (`...[...]`) to match the working `Model/Compass/*`
|
|
169
|
+
pattern. Added the **"template vars must be spread, never a bare array"** gotcha. (apeterson)
|
|
154
170
|
- 2026-08-03 — Refined the `CharSet` gotcha: the **transmit** side doesn't set it either, so the
|
|
155
171
|
effective wire charset is **iso-8859-1**, and a body that needs accents should escape with
|
|
156
172
|
`htmlentities($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false)` (details on
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| [API Payload Interceptors (metadata-registered prePost/postPut model hooks)](features/api-payload-interceptors.md) | A `_Model`'s `prePost()` / `postPost()` / `prePut()` / `postPut()` hooks are **not** called by the model. | api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Compass/PurchaseOrder.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderItem.php, dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql, dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql, dbchanges2/Client_Compass/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql, dbchanges2/Client_CompassCanada/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql |
|
|
7
7
|
| [Multi-Client (Cross-Client) Data Retrieval](features/cross-client-data-retrieval.md) | A single authenticated V2 GET listing can return records across **many** clients (designed for 1000+) that the caller is entitled to, honoring **each target cli | api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Cache/Table.php, _underscore/Model/Cache/Tables/Client.php, _underscore/Model/Core/Record.php, worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Cache/2026-06-30a - MultiClientCacheTables.sql, dbchanges2/Core/2026-06-30b - CacheClusterRegistrationAndRecordTtl.sql, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
|
|
8
8
|
| [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
|
|
9
|
-
| [ENVIRONMENT (not the EB environment name) decides the _underscore branch and Config file](features/environment-variable-drives-underscore-branch.md) | An api2 Elastic Beanstalk instance decides **which `_underscore` branch it clones** and **which `Config/<env>.ini` it loads** from the EB environment property * | api2/.ebextensions/git.php, api2/.ebextensions/php_include_underscore.config, api2/.ebextensions/git.sandbox-dev.json, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, api2/Component/Api/V2/V2.php |
|
|
9
|
+
| [ENVIRONMENT (not the EB environment name) decides the _underscore branch and Config file](features/environment-variable-drives-underscore-branch.md) | An api2 Elastic Beanstalk instance decides **which `_underscore` branch it clones** and **which `Config/<env>.ini` it loads** from the EB environment property * | api2/.ebextensions/git.php, api2/.ebextensions/php_include_underscore.config, api2/.ebextensions/git.sandbox-dev.json, api2/.ebextensions/git.sandbox-client.json, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, api2/Component/Api/V2/V2.php |
|
|
10
10
|
| [Health-check endpoint (/health liveness short-circuit)](features/health-check-endpoint.md) | `_Controller_Index::api()` short-circuits **liveness/health-probe** requests to an HTTP 200 **before** any routing, DB bootstrap, or V2 engine work runs. | api2/Controller/Index.php |
|
|
11
11
|
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text — `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
|
|
12
12
|
| [/auth/login resolves the client from the email domain, not the Bearer token (cross-client user path)](features/login-cross-client-user-resolution.md) | `POST /v2/auth/login` (email/password user login) can silently swap the target client mid-request. | api2/Component/Api/V2/V2.php, _underscore/String.php |
|
|
@@ -7,11 +7,12 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-08-06
|
|
10
|
-
owners: ["bala", "mhammontree"]
|
|
10
|
+
owners: ["bala", "mhammontree", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/.ebextensions/git.php
|
|
13
13
|
- api2/.ebextensions/php_include_underscore.config
|
|
14
14
|
- api2/.ebextensions/git.sandbox-dev.json
|
|
15
|
+
- api2/.ebextensions/git.sandbox-client.json
|
|
15
16
|
- api2/Config/beta.ini
|
|
16
17
|
- api2/Config/sandbox-dev.ini
|
|
17
18
|
- api2/Component/Api/V2/V2.php
|
|
@@ -116,12 +117,36 @@ one.
|
|
|
116
117
|
`_underscore`. (Contrast: the 1.0 **tools** app works cleanly because its helper *and* its config
|
|
117
118
|
live in the same repo, and it clones `library` from a **fixed** branch, `_production`, rather
|
|
118
119
|
than an `ENVIRONMENT`-derived one.)
|
|
120
|
+
- **⚠ A `git.<env>.json` `"branch"` key can pin a tier to the WRONG `_underscore` branch, silently.**
|
|
121
|
+
When a code fix pushed to a branch does not appear after an api2 deploy, before debugging the
|
|
122
|
+
code verify which branch that EB environment's `.ebextensions/git.<env>.json` actually clones:
|
|
123
|
+
a present `"branch"` value overrides the `'_' . strtolower(ENVIRONMENT)` default. **Concrete
|
|
124
|
+
(fixed 2026-08-18):** `git.sandbox-client.json` had `"branch": "_production"`, so the
|
|
125
|
+
`sandbox-client` environment ran `_underscore` from `_production` — never `_sandbox-client` —
|
|
126
|
+
which is why fixes merged to `_sandbox-client` never showed up there. Confirmed two ways: the
|
|
127
|
+
config file value, and live runtime behavior matching `_production` (unfixed) rather than
|
|
128
|
+
`_sandbox-client` (fixed). Fix = remove the `"branch"` key so it falls back to `_sandbox-client`.
|
|
129
|
+
- **Deploy path is build-time, so a git push alone does not update running instances.** The
|
|
130
|
+
`_underscore` clone happens in the prebuild hook at build time: commit the fix on the feature
|
|
131
|
+
branch, merge that branch into the deploy-channel branch (`_` + `ENVIRONMENT`, e.g.
|
|
132
|
+
`_sandbox-client`), push, **then redeploy the EB environment** to re-run the clone.
|
|
133
|
+
- **⚠ SECURITY — every `.ebextensions/git.*.json` carries a plaintext GitHub PAT** committed in
|
|
134
|
+
the repo (used to clone `_underscore`). Pre-existing leak; treat the committed tokens as
|
|
135
|
+
compromised, rotate, and move them to EB environment properties / SSM. Do **not** copy the token
|
|
136
|
+
value anywhere — this note records only that the credential lives in these files.
|
|
119
137
|
- **Duplicate integration code is the trap this creates.** When the same capability is needed in
|
|
120
138
|
both tiers it must exist twice (once per framework) — keep the credential/config location
|
|
121
139
|
documented in both places so a rotation updates both.
|
|
122
140
|
|
|
123
141
|
## Change history
|
|
124
142
|
|
|
143
|
+
- 2026-08-18 — Recorded a second concrete `"branch"`-pin trap: `git.sandbox-client.json` pinned
|
|
144
|
+
`"branch": "_production"`, so the `sandbox-client` EB environment cloned `_underscore` from
|
|
145
|
+
`_production` and never received `_sandbox-client` merges (confirmed via both the config value and
|
|
146
|
+
live runtime matching the unfixed `_production` code); fix = drop the `"branch"` key. Added the
|
|
147
|
+
general diagnostic ("fix not appearing after deploy → check `git.<env>.json` branch pin first"),
|
|
148
|
+
the build-time deploy note (merge into `_`+ENVIRONMENT then redeploy — a push alone is inert),
|
|
149
|
+
and the ⚠ plaintext-GitHub-PAT-in-`git.*.json` security gotcha (location only). (apeterson)
|
|
125
150
|
- 2026-08-06 — TRUE-80282 deploy: independently re-confirmed the mapping from a second tier
|
|
126
151
|
(`api.beta.togahub.com` = `ENVIRONMENT=beta` → `Config/beta.ini` → branch `_beta`) at a cost of two
|
|
127
152
|
deploy cycles, and recorded **why** the branch derives: there is deliberately **no
|
|
@@ -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