toga-ai 1.0.710 → 1.0.711

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.
@@ -17,5 +17,6 @@
17
17
  | [Staples cXML Order Import (SFTP → NetSuite)](features/staples-cxml-order-import.md) | `sync_staples_cxml.php` is an **hourly** cron (runs at **:45**) that imports Staples cXML purchase orders from SFTP into NetSuite as Sales Orders, then writes a | worker/crons/sync/staples/sync_staples_cxml.php |
18
18
  | [Diagnosing frozen 1.0 worker cron check-ins (Sentry "missed" flood)](workflows/diagnosing-frozen-cron-checkins.md) | When 1.0 worker cron timestamps freeze and Sentry project `worker1` fills with **`missed`** check-ins, the intuitive diagnosis — a wedged `App_Framework::isProc | worker/.ebextensions/cron.config, library/app/worker.php |
19
19
  | [isFulfillable Multi-Client Backfill (all togasupply clients)](workflows/isfulfillable-multi-client-backfill.md) | One-time backfill that catches up `Items.isFulfillable` on **existing** items across **all 17 togasupply clients** (AIG, Broward Sheriff, Canon, Endeavor Health | worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php, library/app/api/toga2.php |
20
+ | [Deduplicating NetSuite duplicate Items across client DBs (recursive MERGE)](workflows/netsuite-duplicate-items-dedup.md) | A one-time, per-client data cleanup that collapses **duplicate `Items` rows sharing a `partNumber`** in each NetSuite → TOGa Supply client DB down to a single c | library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php |
20
21
  | [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer (`worker/crons/toga2/netsuite/`). | worker/crons/toga2/netsuite/sync_togasupply.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql, dbchanges2/_modules/netsuite/2026-08-05 - CLEAN NETSUITE CLINET.SQL |
21
22
  | [Tracing a 1.0 worker cron run in production (no stdout, silent skips)](workflows/tracing-a-worker-cron-run-in-production.md) | How to answer *"did this cron actually run, and what did it do?"* on the 1.0 `worker` tier, where **there is no usable stdout** and **a skipped run leaves no tr | worker/ebs/cron.worker.php, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.infrastructure.json, worker/.ebextensions/009_setup_phpini.config, library/app/framework.php |
@@ -755,9 +755,55 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
755
755
  2**) and scope the `Units` lookup by it (returns exactly 1 → reuse). Latent data problem behind it:
756
756
  **duplicate `Items` with the same `partNumber`** should be reconciled later (the fix is robust to it).
757
757
  GroWrk ITEM_FULFILLMENTS went from stuck `1-RUNNING` (since Aug 21) to green.
758
+ - **⚠ SUPERSEDED 2026-08-31 — item-scoping ALONE was WRONG for NYCHH.** Where the serial's single
759
+ existing unit sits under a **different** duplicate `Item` sharing the part number, scoping the `Units`
760
+ lookup by `$uuidItemForLine` found nothing and re-created the unit → the SAME 1062 collision. Correct
761
+ rule: **a serial identifies a physical device, so query `Units` by `serialNumber` ONLY**, then *prefer*
762
+ the unit whose item matches `$uuidItemForLine` when several share the serial (GroWrk's 2-unit case),
763
+ else reuse the first; create only when **no** unit carries the serial. Handles the 1-unit (NYCHH),
764
+ 2-unit (GroWrk), and new-serial cases. (The duplicate `Items` themselves are the real defect — see the
765
+ Agilant-catalog root cause and the dedup workflow below.)
766
+
767
+ - **⚠ Invoice line numbers are NOT unique on multi-SO invoices — assign a sequential `lineNumber`
768
+ (fixed 2026-08-31).** A NetSuite invoice that bills **multiple sales orders** restarts `line` per source
769
+ SO, so `$nsItem->line` is not unique within one invoice → `InvoiceItems UNIQUE(invoiceId, lineNumber)` →
770
+ `POST /v2/invoices` `EV-10` / MySQL **1062**, and because the invoice POST is **atomic** the whole
771
+ invoice rolls back, freezing `INVOICES` at `1-RUNNING` (NYCHH). **Fix (`syncInvoiceFromNetsuite`):**
772
+ assign a unique **1..N sequential** `lineNumber` per imported line instead of `$nsItem->line` (identical
773
+ for the common single-SO invoice; differs only on multi-SO). Safe because nothing downstream keys off
774
+ invoice `lineNumber` — the SO link uses `orderLine`/`salesOrderItem`.
775
+ - **⚠ Transfer-order invoices are NOT imported (fixed 2026-08-31).** Under NYCHH stocking-flag routing a
776
+ $0 NetSuite Sales Order becomes a `TransferOrder`, not a `SalesOrder`; `Invoices` has only
777
+ `salesOrderId` (no `transferOrderId`), so such an invoice imported with a **null order link**. Team
778
+ decision: **do not import transfer-order invoices at all.** **Fix:** in the "billed order not found in
779
+ SalesOrders" branch, classify the order authoritatively (`App_Api_Netsuite_Rest::fetchSalesOrderByIdFull`
780
+ + `self::isTransferOrder`) and **return early** when it is a transfer order, gated on
781
+ `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS`. Existing TO-billed invoices were deleted by the developer.
782
+ - **⚠ ROOT CAUSE of the duplicate `Items`: the inventory-adjustment item-create OMITTED the catalog
783
+ (fixed 2026-08-31).** `getCreateItem` (`toga2.php:6233`) and the fulfillment create (~4951) stamp
784
+ `'catalog' => ['name' => 'Agilant']`; `syncInventoryAdjustmentFromNetsuite`'s item-create did **not** →
785
+ it produced **NULL-catalog `Items`**. The shared item lookup (`common_sync_togasupply.php` ~L313) is
786
+ populated **only from Agilant-catalog items**, so those NULL-catalog dupes are **invisible** to it → a
787
+ NEW duplicate item was created **every run** for the same part number — the source of every
788
+ `(itemId, serialNumber)` unit collision seen this session. **Fix:** stamp
789
+ `'catalog' => ['name' => 'Agilant']` on the inventory-adjustment item-create. Stops all new duplicates.
790
+ The one-time cleanup of the accumulated ~4,300 duplicate part numbers across all 17 clients is the
791
+ [NetSuite duplicate-Items dedup workflow](../workflows/netsuite-duplicate-items-dedup.md).
758
792
 
759
793
  ## Change history
760
794
 
795
+ - 2026-08-31 — **Invoice + serial-unit + duplicate-Item root-cause fixes (shared, `library`).**
796
+ `syncInvoiceFromNetsuite` now assigns a **sequential 1..N invoice `lineNumber`** (NetSuite reuses `line`
797
+ across multi-SO invoices → `InvoiceItems UNIQUE(invoiceId,lineNumber)` 1062 → atomic rollback → NYCHH
798
+ INVOICES `1-RUNNING`) and **skips transfer-order invoices** (classify via `fetchSalesOrderByIdFull` +
799
+ `isTransferOrder`, return early, gated on `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` — `Invoices` has no
800
+ `transferOrderId`; team decision not to import them). **Corrected** the 2026-08-30 item-scoped serial
801
+ unit lookup (which broke NYCHH): a serial identifies a physical device, so query `Units` by
802
+ `serialNumber` ONLY, prefer the unit matching `$uuidItemForLine` when several share it, else reuse first,
803
+ create only when none exists. Found + fixed the **root cause of the duplicate `Items`**: the
804
+ inventory-adjustment item-create omitted `catalog => Agilant`, producing NULL-catalog items invisible to
805
+ the Agilant-only sync lookup → a new dup every run. Companion one-time cleanup across all 17 clients:
806
+ [NetSuite duplicate-Items dedup workflow](../workflows/netsuite-duplicate-items-dedup.md). (jcardinal)
761
807
  - 2026-08-30 — **Inventory-adjustment serialized-unit path now auto-creates a missing unit warehouse
762
808
  (`library`).** `syncInventoryAdjustmentFromNetsuite` took the unit's location from the inventory
763
809
  number's `custitemnumber_location` but never created a missing warehouse (unlike
@@ -0,0 +1,102 @@
1
+ ---
2
+ title: "Deduplicating NetSuite duplicate Items across client DBs (recursive MERGE)"
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-08-31
10
+ owners: [jcardinal]
11
+ files:
12
+ - library/app/api/toga2.php
13
+ - worker/crons/toga2/netsuite/common_sync_togasupply.php
14
+ related:
15
+ - ../features/netsuite-togasupply-per-client-sync.md
16
+ - ../../library/features/netsuite-suiteql-rest-shim.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ A one-time, per-client data cleanup that collapses **duplicate `Items` rows sharing a `partNumber`**
22
+ in each NetSuite → TOGa Supply client DB down to a single canonical item, re-pointing every reference.
23
+ Duplicates accumulated because the NetSuite inventory-adjustment sync created **NULL-catalog `Items`**
24
+ that the sync's own lookup could not see, so a new duplicate was minted every run (root cause below).
25
+ Deleting a duplicate item is a **recursive MERGE**, not a simple `DELETE` — ~23 `itemId` tables plus
26
+ two FK cascades (`VendorItems`, `Units`) reference it. Run **per client, in its own transaction**,
27
+ after the code fix is deployed.
28
+
29
+ **Verified in prod (2026-08-31):** all **17** NetSuite clients now show **0 duplicate part numbers**
30
+ (e.g. `Client_Nychh` 3954→1818 items, `Client_Canon` 913→425; 0 orphans, 0 collisions).
31
+
32
+ ## Root cause — why the duplicates existed at all
33
+
34
+ `syncInventoryAdjustmentFromNetsuite`'s item-create **omitted the catalog** that
35
+ `getCreateItem`/`toga2.php:6233` and the fulfillment create both stamp
36
+ (`'catalog' => ['name' => 'Agilant']`), so it produced **NULL-catalog `Items`**. The shared sync item
37
+ lookup in `worker/crons/toga2/netsuite/common_sync_togasupply.php` (~L313) is populated **only from
38
+ Agilant-catalog items**, so a NULL-catalog dupe is **invisible** to it → the sync re-created the item
39
+ every run for the same part number. **The fix (stamp `catalog => Agilant` on that create) stops all NEW
40
+ duplicates** and must be deployed first — see
41
+ [per-client sync](../features/netsuite-togasupply-per-client-sync.md). This cleanup removes the
42
+ **existing** backlog (~4,300 duplicate part numbers / ~15,700 `Items` rows accumulated over time).
43
+
44
+ ## When to run
45
+
46
+ - After the Agilant-catalog item-create fix is deployed (otherwise the backlog regrows every sync run).
47
+ - Per client, once. Re-runnable safely (idempotent: a client already at 0 duplicates is a no-op).
48
+
49
+ ## Procedure
50
+
51
+ 1. **Report first (read-only).** Run `test/team/2.0 deployment/duplicate_items_merge_analysis.sql`
52
+ against the client DB — a per-client duplicate + collision report. Confirm the duplicate count and the
53
+ `(itemId, serialNumber)` / `(vendorId, itemId, vendorPartNumber)` collisions before mutating anything.
54
+ 2. **Pick the canonical item per `partNumber`** in this priority order: **Agilant + manufacturer**
55
+ preferred → else **Agilant** → else **has-manufacturer** → else **lowest `id`**.
56
+ 3. **Re-point every `itemId` reference** across the ~23 `itemId` tables **plus `Entitlements.saleItemId`**
57
+ from each non-canonical item to the canonical one.
58
+ 4. **Handle the two FK cascades** (children must move before their parent duplicate is deleted):
59
+ - **`VendorItems`** is referenced by `PurchaseOrderItems`, `VendorItemCosts`, `Personas_VendorItems`.
60
+ - **`Units`** is referenced by **10** `*ItemUnits` child tables.
61
+ 5. **Resolve unique-key collisions with a SURVIVOR-per-key approach**, NOT a plain "`UPDATE IGNORE` +
62
+ delete leftover" (that leaves child FKs dangling on dupe-vs-dupe collisions). For each colliding key
63
+ (`Units.(itemId, serialNumber)`, `VendorItems.(vendorId, itemId, vendorPartNumber)`): choose one
64
+ survivor row, **re-point the non-survivor rows' children onto the survivor**, delete the non-survivors,
65
+ then re-point the rest.
66
+ 6. **Delete the duplicate `Items` rows LAST** (after all references and cascades are moved).
67
+ 7. **Run each client in its own transaction**, continue-on-error (see the missing-table gotcha).
68
+
69
+ ## Validation
70
+
71
+ - Validate against **local copies of the client DBs before prod** (this session: `Client_Nychh`
72
+ 3954→1818, `Client_Canon` 913→425, **0 orphans, 0 collisions**).
73
+ - After each prod run, re-run the analysis SQL: the client must report **0 duplicate part numbers**.
74
+
75
+ ## Deliverables (live in the repo, not the knowledge base)
76
+
77
+ - `test/team/2.0 deployment/duplicate_items_merge_analysis.sql` — read-only per-client duplicate +
78
+ collision report.
79
+ - The per-client merge migration (the recursive MERGE implementing steps 2–6).
80
+
81
+ ## Gotchas / known issues
82
+
83
+ - **The sync item lookup is Agilant-catalog-only.** Any non-Agilant item is **invisible** to
84
+ `common_sync_togasupply.php` and will be **re-created** — this is the whole mechanism behind the
85
+ duplicates. Deploy the catalog-stamp fix before (and independently of) this cleanup.
86
+ - **`Items` has NO unique constraint on `partNumber`.** Duplicates persist silently; nothing at the DB
87
+ level prevents them, which is why this is a MERGE-and-clean rather than a rely-on-the-constraint fix.
88
+ - **Client DBs are schema-similar but NOT identical.** Some lack optional tables (e.g.
89
+ `EntitlementServiceItems`). Run the merge **continue-on-error** so a missing-table MySQL **1146** is a
90
+ **benign skip**, not a failed migration.
91
+ - **A plain `UPDATE IGNORE` + delete-leftover is insufficient.** Dupe-vs-dupe unique-key collisions leave
92
+ orphaned child FKs; only the survivor-per-key re-pointing (step 5) is correct.
93
+
94
+ ## Change history
95
+
96
+ - 2026-08-31 — Documented the one-time duplicate-`Items` dedup across all 17 NetSuite clients (recursive
97
+ MERGE over ~23 `itemId` tables + `Entitlements.saleItemId`, `VendorItems`/`Units` FK cascades,
98
+ survivor-per-key collision resolution), its root cause (the inventory-adjustment item-create omitted
99
+ `catalog => Agilant` → NULL-catalog items invisible to the Agilant-only sync lookup → a new dup every
100
+ run), and the durable gotchas (Agilant-only lookup, no `partNumber` unique constraint, per-client schema
101
+ drift → continue-on-error). Validated on local copies then run per client in prod; all 17 clients at 0
102
+ duplicate part numbers. (jcardinal)
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 23 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 31 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 32 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
10
10
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
11
11
  - **togadesk** (TOGa Desk) — 13 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.710",
3
+ "version": "1.0.711",
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",