toga-ai 1.0.809 → 1.0.810

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.
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-14
9
+ updated: 2026-09-15
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -1270,6 +1270,50 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
1270
1270
  Note `SalesOrderItems.lineNumber` is a **`smallint`**, so the parked base must stay within smallint
1271
1271
  range for the client's line counts (headroom 1000 is safe at observed volumes).
1272
1272
 
1273
+ - **⚠ A NetSuite-removed line that still holds an UPSTREAM customer-PO link blocked the renumber and
1274
+ re-froze Compass SALES_ORDERS (fixed 2026-09-15, `library` only, Compass-gated).** Compass USA's
1275
+ SALES_ORDERS was stuck at `1-RUNNING` (`Logs.Issue` 749, clientId 2) throwing *"unresolvable
1276
+ NetSuite<->DB lineNumber conflict … reconcile the orphaned/undeletable row"* whenever a
1277
+ NetSuite-removed line sat on a `lineNumber` a surviving line had to move onto. Root cause: the
1278
+ **existing** stale-line prune (`toga2.php` ~L1582) runs **AFTER** the renumber **and** skips any
1279
+ line carrying a PO link (it cannot delete a PO-linked line), so those removed-but-PO-linked lines
1280
+ survived and occupied a needed target number — and the renumber threw first anyway.
1281
+ - **Fix — a Compass-gated prune BEFORE the renumber** in `syncSalesOrderFromNetsuite`. For each SO
1282
+ line whose `c_netsuiteLineUniqueKey` is **non-empty** and **absent** from the current NetSuite
1283
+ line set (a line NetSuite removed) that was **never fulfilled and never invoiced**: delete its
1284
+ Agilant-side SO↔PO **bridge LINK** rows in **both** directions (routes
1285
+ `/purchase-order-item-sales-order-items/{uuid}` and `/sales-order-item-purchase-order-items/{uuid}`),
1286
+ then DELETE the `SalesOrderItem` (`/sales-order-items/{uuid}`), then drop it from the in-memory
1287
+ `$salesOrderItemsInToga`. It **NEVER** deletes/modifies the `PurchaseOrder`/`PurchaseOrderItem`
1288
+ itself (the Compass **customer** order is off-limits) and never touches another order record.
1289
+ - **Guardrails (owner decision 2026-09-15; cto AGREE; php-reviewer clean; cso SAFE TO SHIP):**
1290
+ (a) non-empty key **absent from NetSuite** only — an empty key was never matched to NetSuite, so
1291
+ treating it as "removed" is a false positive; (b) never a shipped/invoiced line (checked against
1292
+ the fulfillment/invoice maps built ~L990-1032); (c) **Compass-gated** (`CLIENT_UUID_COMPASS`);
1293
+ (d) **re-check that one line's fulfillment/invoice status immediately before the deletes** to
1294
+ close a TOCTOU gap (the maps are a start-of-run snapshot); (e) the final `SalesOrderItem` DELETE
1295
+ stays **throwing** (fail-loud) if any unhandled attachment remains — mirrors the PO-line cleanup
1296
+ in `syncPurchaseOrderFromNetsuite` (~L2793-2806) and the mirror-NetSuite decision (2026-09-02).
1297
+ - **Every FK child of `Client_Compass.SalesOrderItems` blocks a delete.** All are `NO ACTION` or
1298
+ `RESTRICT`, and InnoDB treats `NO ACTION` as `RESTRICT`: `ItemFulfillmentItems`, `InvoiceItems`,
1299
+ `PurchaseOrderItems_SalesOrderItems`, `SalesOrderItems_PurchaseOrderItems`,
1300
+ `SalesOrderItems_CommittedUnits`, `SalesOrderItems_Tags`, `SalesOrderItems`
1301
+ (`parentSalesOrderItemId` kit children), `SalesOrderItems_TransferOrderItems`,
1302
+ `TransferOrderItems_SalesOrderItems`. This is why the prune must clear the bridge links first and
1303
+ only ever touches never-fulfilled/never-invoiced lines.
1304
+ - **Proven (evidence):** NetSuite sales order 271929 (internal id 6926956), customer "6096 ODP
1305
+ Veyer (B2B)"; NetSuite reorganized it to 9 lines and removed 6 kitting lines (lineUniqueKeys
1306
+ 20024694/695/696/702/703/704) that never shipped and were never invoiced but each still held an
1307
+ upstream customer-PO link — those 6 blocked the renumber.
1308
+ - **⚠ OPEN, NOT FIXED — a DUPLICATE `SalesOrders` record on one NetSuite internal id.** `SalesOrders`
1309
+ number **467435086001** shares NetSuite internal id **6926956** with order 271929 (two TOGa
1310
+ records for one NetSuite sales order; 467435086001 has 12 keyless lines with downstream vendor-PO
1311
+ links, none shipped). The main SO lookup in `syncSalesOrderFromNetsuite` (~L946-974) filters
1312
+ Compass by `locationId 615286` / `customerId 3`, which currently narrows to 271929, so the prune
1313
+ only ever touches 271929 — but **two records on one NetSuite id is an anomaly that could make the
1314
+ lookup bind to the wrong record on other orders.** Needs its own ticket. Deploy: `library` only;
1315
+ the worker redeploys to pick it up (worker clones library on deploy).
1316
+
1273
1317
  - **⚠ Canon + Endeavor Health ITEM_RECEIPTS froze because `fetchPurchaseOrderById` never populated the
1274
1318
  PO vendor (deployed/verified 2026-08-29).** The PO shim's `->entity` was always null (its SuiteQL
1275
1319
  SELECT omitted `entity`, unlike `listPurchaseOrders`), so the fail-loud missing-entity guard in
@@ -1581,6 +1625,21 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
1581
1625
 
1582
1626
  ## Change history
1583
1627
 
1628
+ - 2026-09-15 — **Compass SALES_ORDERS unfrozen: a Compass-gated prune runs BEFORE the renumber.**
1629
+ `syncSalesOrderFromNetsuite` was throwing *"unresolvable NetSuite<->DB lineNumber conflict"*
1630
+ (`Logs.Issue` 749, clientId 2) because a NetSuite-removed line that still held an upstream
1631
+ customer-PO link survived the existing stale-line prune (which runs after the renumber and skips
1632
+ PO-linked lines) and occupied a `lineNumber` a surviving line needed. Added a prune BEFORE the
1633
+ renumber that, for each never-fulfilled/never-invoiced line whose non-empty `c_netsuiteLineUniqueKey`
1634
+ is absent from NetSuite, deletes its SO↔PO bridge links in both directions then the `SalesOrderItem`
1635
+ — never touching the `PurchaseOrder`/`PurchaseOrderItem` (Compass customer order). Guardrails:
1636
+ absent-from-NetSuite + non-empty key only, never shipped/invoiced, Compass-gated, immediate
1637
+ TOCTOU re-check, final DELETE stays fail-loud. Recorded that every FK child of
1638
+ `Client_Compass.SalesOrderItems` is `NO ACTION`/`RESTRICT` (all block a delete), and an OPEN
1639
+ duplicate-`SalesOrders`-record anomaly (467435086001 shares NetSuite internal id 6926956 with
1640
+ 271929). Proven on NetSuite SO 271929 (6 removed kitting lines blocked the renumber). `library`
1641
+ only, no SQL; worker redeploys to pick it up. cto AGREE; php-reviewer clean; cso SAFE TO SHIP.
1642
+ (jcardinal)
1584
1643
  - 2026-09-13 — **ITEM_FULFILLMENTS unfrozen for NYCHH: five stacked fulfillment-reconcile bugs fixed
1585
1644
  (library, deployed + verified in prod).** (1) **Unit duplication from a truncated nested GET** — the
1586
1645
  existing-units dedup map was built from the nested `itemFulfillmentItemUnits` of one
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-26
10
- owners: [apeterson, bala]
9
+ updated: 2026-09-15
10
+ owners: [apeterson, bala, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/PurchaseOrders/SalesOrder.php
13
13
  - _underscore/Model/Client/SalesOrders/PurchaseOrder.php
@@ -40,6 +40,18 @@ one returns `200` with an empty array, which reads like a permission or data bug
40
40
  Read the name as **"`<source>`\_`<thing created from it>`"**. The *first* table name is the
41
41
  origin; the second is what was generated from it.
42
42
 
43
+ **The line-item bridges follow the same rule (same name order, same directions):**
44
+
45
+ | Table | Direction | Meaning | API route |
46
+ |---|---|---|---|
47
+ | `PurchaseOrderItems_SalesOrderItems` | **UPSTREAM** | an SO line created **FROM** a customer PO line | `purchase-order-item-sales-order-items` |
48
+ | `SalesOrderItems_PurchaseOrderItems` | **DOWNSTREAM** | a vendor PO line created **FROM** an SO line | `sales-order-item-purchase-order-items` |
49
+
50
+ The 1.0 Compass NetSuite sync deletes these **item-level** link rows in **both** directions when it
51
+ prunes a NetSuite-removed SO line — see
52
+ [NetSuite → TOGa Supply per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)
53
+ (2026-09-15).
54
+
43
55
  ## How it works
44
56
 
45
57
  - Both `Records` rows are `aclDatabase = CLIENT`, `childPolicy = MATCH_UPSERT`, so both are
@@ -122,6 +134,11 @@ everywhere. Present is not populated.
122
134
  collapses many POs without multiplying rows.
123
135
 
124
136
  ## Change history
137
+ - 2026-09-15 — Added the **line-item** bridge parallel: `PurchaseOrderItems_SalesOrderItems`
138
+ (upstream, route `purchase-order-item-sales-order-items`) and `SalesOrderItems_PurchaseOrderItems`
139
+ (downstream, route `sales-order-item-purchase-order-items`) follow the same name-order rule as the
140
+ header bridges. Confirmed while building the Compass 1.0 sync's pre-renumber prune, which deletes
141
+ both item-level link directions for a NetSuite-removed SO line. (jcardinal)
125
142
  - 2026-08-26 — Added the **write** side: the two directions are populated by two different
126
143
  pipelines (2.0 `_Trait_Netsuite_SalesOrder` writes **upstream** customer POs from `otherRefNum`;
127
144
  the 1.0 `syncPurchaseOrderFromNetsuite` cron writes **downstream** vendor POs), and the api2 child
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.809",
3
+ "version": "1.0.810",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",