toga-ai 1.0.563 → 1.0.564

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.
@@ -7,10 +7,10 @@
7
7
  | [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.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/send_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, library/app/client/compasscanada.php |
8
8
  | [Elite TOGA 2.0 → TOGaDeskSupport Standalone Attachment Sync](features/elite-togadesk-attachment-sync.md) | `sync_togadesk_elite_attachments.php` is a standalone cron (every 5 minutes) that syncs file attachments from TOGA 2.0 into TOGaDeskSupport for Elite. | worker/crons/toga2/elite/sync_togadesk_elite_attachments.php, worker/crons/toga2/elite/test_sync_togadesk_elite_attachments.php |
9
9
  | [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, worker2/Component/Forecast/SaleImport/SaleImport.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/reconcile_drift_2023plus.php, test/@dave/probe_invoice_gap_2026.php, test/@dave/probe_creditmemo_gap_detail.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
10
- | [NetSuite → TOGa Supply Per-Client Sync (thin wrappers)](features/netsuite-togasupply-per-client-sync.md) | Syncs NetSuite transactions (sales orders, purchase orders, invoices, item receipts, item fulfillments, inventory adjustments) into each TOGa Supply (2.0) clien | worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_canon.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql, library/app/api/netsuite/rest.php, library/app/systemmonitor/netsuiteintegration.php |
10
+ | [NetSuite → TOGa Supply Per-Client Sync (thin wrappers)](features/netsuite-togasupply-per-client-sync.md) | Syncs NetSuite transactions (sales orders, purchase orders, invoices, item receipts, item fulfillments, inventory adjustments) into each TOGa Supply (2.0) clien | worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_canon.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql, library/app/api/toga2.php, library/app/api/netsuite/rest.php, library/app/framework.php, library/app/systemmonitor/netsuiteintegration.php, test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php, test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php |
11
11
  | [OneUptime Server monitor + disk/memory hygiene on the 1.0 worker EB host](features/oneuptime-server-monitor-host-hygiene.md) | The 1.0 `agilant-worker` EB environment runs on the **legacy Amazon Linux 1 PHP 7.2 platform** (Apache httpd/prefork, s3fs mounts, cron) and repeatedly went dow | worker/.ebextensions/040_disk_memory_hygiene.config, worker/.ebextensions/045_oneuptime_agent.config, worker/ebs/cron.worker.php, worker/ebs/mount-s3fs-folders.php, worker/ebs/apache_settings.php, worker/ebs/setup_phpini.php |
12
12
  | [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL | library/app/worker.php, worker/crons/worker/worker_heartbeat.php |
13
13
  | [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
14
14
  | [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 |
15
15
  | [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 |
16
- | [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/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
16
+ | [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 |
@@ -6,20 +6,27 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-11
10
- owners: ["dfranks", "bala", "jcardinal", "mhammontree"]
9
+ updated: 2026-08-12
10
+ owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
13
13
  - worker/crons/toga2/netsuite/sync_togasupply_canon.php
14
+ - worker/crons/toga2/netsuite/sync_togasupply_elite.php
14
15
  - worker/schedules/cron.worker.sync.json
15
16
  - dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql
17
+ - library/app/api/toga2.php
16
18
  - library/app/api/netsuite/rest.php
19
+ - library/app/framework.php
17
20
  - library/app/systemmonitor/netsuiteintegration.php
21
+ - test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php
22
+ - test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php
18
23
  related:
19
24
  - ../architecture.md
20
25
  - forecast2-netsuite-reconciliation.md
26
+ - ../workflows/onboarding-client-to-netsuite-togasupply-sync.md
21
27
  - ../../library/features/toga2-api-client-and-bridge.md
22
28
  - ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
29
+ - ../../../clients/elite/features/netsuite-togasupply-sync.md
23
30
  ---
24
31
 
25
32
  ## Summary
@@ -54,11 +61,61 @@ through the public TOGa2 API (a 1.0↔2.0 bridge).
54
61
  Each section (sales orders, POs, invoices, item receipts, item fulfillments, inventory
55
62
  adjustments) uses **adaptive time-windowing** via `startModeIteration()`/`finishModeIteration()`:
56
63
  a window stored as `<seconds>-<STATE>` in the client's `Parameters` table shrinks ÷3 when the
57
- prior run was left `RUNNING` (didn't finish) and grows ×3 (capped at 432000s = 5 days) when it
64
+ prior run was left `RUNNING` (didn't finish) and grows ×3 (capped at
65
+ `MAX_TIME_WINDOW_TO_FETCH_FROM_NETSUITE_SECONDS`, default 432000s = 5 days) when it
58
66
  finished `IDLE`. The window state is written `RUNNING` at the **start** of each section, so a
59
67
  crash leaves it visibly RUNNING and the next run backs off. `LAST_SYNC_DATETIME_*` advances to
60
68
  the window's upper bound after each successful section.
61
69
 
70
+ ### The window cap is per-client overridable (2026-08-12)
71
+
72
+ `common_sync_togasupply.php:3` is
73
+ `defined('MAX_TIME_WINDOW_TO_FETCH_FROM_NETSUITE_SECONDS') || define(…, 432000)` — **not** a
74
+ `const` — so a wrapper may `const MAX_TIME_WINDOW_TO_FETCH_FROM_NETSUITE_SECONDS = …;`
75
+ *before* its `require_once` to widen its own catch-up window. Only Elite does (2592000 = 30 days,
76
+ while backfilling from 2024); the other 17 wrappers inherit the 5-day default. Use this only for a
77
+ deep backfill and put the reason in a comment next to it — at 5-day windows on a 5-minute cron,
78
+ ~858 days of history takes days of wall-clock to catch up; at 30 days it took ~90 minutes.
79
+
80
+ ### ⚠ A shrinking window is a BUG SIGNAL, not a load signal
81
+
82
+ The single most expensive misreading of this engine. `startModeIteration()` writes
83
+ `<seconds>-RUNNING` at the **start** of every section and `finishModeIteration()` writes `-IDLE` at
84
+ the end; the ÷3 `REDUCE_INTERVAL_FACTOR` shrink fires only because the *previous* run never reached
85
+ its IDLE write. **There is no timeout, memory cap, or resource limit anywhere in the script** — so
86
+ "didn't finish" can only mean **it threw**. A collapsing window therefore means *go read Sentry*,
87
+ never *tune the window size down*. (Corollary: a window that has grown back to the cap is positive
88
+ evidence the section is completing.)
89
+
90
+ ### ⚠ A per-record API failure does NOT stop the cursor — the record is skipped forever
91
+
92
+ The cursor write (`LAST_SYNC_DATETIME_*`) and `finishModeIteration()` both run **unconditionally
93
+ after** the record loop. A record that throws mid-loop is captured to Sentry and **stepped over**,
94
+ while the section still reports success and advances the cursor past its window. So
95
+ **"section IDLE + cursor advancing" is not proof of a complete import** — it only proves the run
96
+ didn't die.
97
+
98
+ Proven concretely on Elite: sales order **169441** (NetSuite internal id 4624062, trandate
99
+ 2024-04-15, customer PO `BEGGS/04152024/…`) was simply absent from `Client_Elite.SalesOrders`
100
+ because its window ran ~15 minutes before the missing-vendor fix (below) landed.
101
+
102
+ **Recovery: rewind the section's cursor.** Set `NETSUITE_LAST_SYNC_DATETIME_<SECTION>` in the
103
+ client's `Parameters` table back before the lost window. Re-import is **idempotent** — records match
104
+ on `c_netsuiteInternal*Id` and `SalesOrders.c_netsuiteInternalSalesOrderId` carries a unique index —
105
+ so a rewind re-PUTs what is already there and POSTs only what is missing.
106
+
107
+ ### Sections are NOT order-dependent — one cursor can be rewound alone
108
+
109
+ Each section's `startModeIteration()` reads only **its own** `Parameters` key, and the PO section
110
+ resolves its own sales order: `syncPurchaseOrderFromNetsuite()` calls
111
+ `syncSalesOrderFromNetsuite()` itself (`library/app/api/toga2.php:1813`) when a PO has an
112
+ originating order, and the link is conditional (`if ($salesOrder)` at :2221) so a PO whose order
113
+ cannot be resolved still imports. Consequences worth knowing before you "fix" a skew:
114
+
115
+ - A purchase-orders cursor running 50 days **ahead** of sales-orders is safe, not a defect.
116
+ - Rewinding one section without touching the others is safe.
117
+ - Which is also why the per-section operator script below is a legitimate tool rather than a hack.
118
+
62
119
  ### Adding a new client (the full recipe)
63
120
 
64
121
  A wrapper + schedule alone is **not enough** — the client must also be provisioned in three
@@ -127,6 +184,30 @@ them provisioned: `/customers`→`c_netsuiteInternalCustomerId`; `/countries`→
127
184
  `/shipping-methods`→`c_netsuiteInternalShipMethodId`. The names are a **convention**, not
128
185
  discovered per-client.
129
186
 
187
+ ### Manual operator scripts (built for Elite, reusable pattern)
188
+
189
+ Two CLI scripts written for the Elite backfill live under
190
+ `test/@srija/Elite Testing/Service Requests/` (they are operator tools, **not** scheduled crons —
191
+ do not add them to `cron.worker.sync.json`):
192
+
193
+ - **`test_sync_togasupply_elite_section.php`** — runs **one** section by enabling only its
194
+ `IS_ENABLED_INTEGRATION_*` flag with `define()` instead of `const`, so a slow section can be
195
+ backfilled without the earlier sections eating the run. **It is NOT a dry run — it writes the real
196
+ cursors.**
197
+ - **`test_diagnose_togasupply_elite.php`** — **read-only** per-record IMPORT/SKIP match reporter for
198
+ a given section and date range. It deliberately does **not** call
199
+ `App_Framework::cronInitialization()` (that takes the process lock and exits silently, and writes a
200
+ `CronJobExecutions` row) and it drains/disables output buffering so `echo` actually reaches the
201
+ terminal.
202
+
203
+ **⚠ A manual script does not mutually exclude with the scheduled cron.**
204
+ `App_Framework::isProcessRunning()` (`library/app/framework.php:833-842`) defaults to
205
+ `$_SERVER['SCRIPT_FILENAME']` and greps `ps -ef` for **its own filename** — so a per-section script
206
+ with a different filename runs happily *alongside* `sync_togasupply_<client>.php`. Both then race the
207
+ same `Parameters` cursors: whoever writes last wins and the other's window is silently skipped
208
+ (and windows shrink). Any manual backfill must either pass the cron's filename to
209
+ `isProcessRunning()` explicitly or be run with the cron's schedule entry disabled.
210
+
130
211
  ## Data model
131
212
 
132
213
  Per-client `Parameters` table (2.0 client DB, e.g. `Client_Canon.Parameters`; columns
@@ -149,6 +230,44 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
149
230
 
150
231
  ## Gotchas / known issues
151
232
 
233
+ - **A client with NEITHER a manufacturer NOR a catalog on its items used to crash the whole run
234
+ (fixed 2026-08-12).** `$lookupItemByClientUuidAndPartNumberUpper[$uuidClient]` and
235
+ `$lookupAllCatalogItemsByClientUuidAndPartNumberUpper[$uuidClient]` were only keyed **inside** the
236
+ items loop, so a sparse client never got the key created at all. Every `sync*FromNetsuite()` takes
237
+ them as `array &`, so the unset key passed as **null** →
238
+ `TypeError: Argument 8 passed to App_Api_Toga2::syncSalesOrderFromNetsuite() must be of the type
239
+ array, null given`. Both are now initialised to `[]` at `common_sync_togasupply.php:74-75`, before
240
+ the loop. This was **shared** code — the latent bug was reachable by any sparse client and Elite's
241
+ three initial items (both fields null) merely exposed it. Lesson for this engine: any per-client
242
+ lookup passed by reference must be initialised **outside** the loop that fills it.
243
+ - **Countries are auto-created from the NetSuite shipping address, with a malformed code and name.**
244
+ `library/app/api/toga2.php:696-730` (and a duplicate at :1868-1905): when
245
+ `$nsShippingAddress->country` is not in `$lookupCountryByNetsuiteCountry`, the sync **POSTs
246
+ `/countries`**, deriving the name by splitting on non-lowercase characters and the code as
247
+ `substr(word1,0,1) . substr(word2,1,1)`. NetSuite's **leading underscore is never stripped**, so
248
+ `_unitedKingdom` → name `_united Kingdom` / code `_i`; `_canada` → `_canada` / `_C`;
249
+ `_australia` → `_australia` / `_A`. **Codes can collide.** An address with an *empty* country sets
250
+ `$isInvalidAddress = true` instead. The rows then surface in any UI that shows a country.
251
+ **Remedy: pre-seed `Countries`** with the correct `code`/`name` **and** the exact
252
+ `c_netsuiteCountry` string before the first import — see the onboarding workflow.
253
+ Note the direction: **NetSuite → TOGa only.** Nothing in this cron ever writes to NetSuite (every
254
+ NetSuite call is a `list*`/`fetch*`), and NetSuite's country list is a fixed system list that
255
+ cannot be extended, so there is nothing to fix on the NetSuite side.
256
+ - **Item receipts are matched by the originating PO's customer, NOT by warehouse location.**
257
+ `common_sync_togasupply.php:844-905` uses the warehouse location ids only to **narrow** the SuiteQL
258
+ query; interest is decided from the receipt's `createdFrom` purchase order — its shipTo customer,
259
+ falling back to the custom field `NETSUITE_CUSTOM_FIELD_ID__END_USER_CUSTOMER`. So: a receipt from
260
+ a **stock PO** with no shipTo and no end-customer cannot be attributed to any client and is
261
+ skipped; and because the location filter is shared, receipts belonging to **other** clients at a
262
+ shared warehouse are returned and correctly skipped. Modelling receipts as "location-matched"
263
+ produces wrong diagnostics.
264
+ - **`Units.locationId` is never populated by this sync — transaction location lives on the item
265
+ rows.** All 229 `Client_Elite.Units` have `locationId` NULL. `locationId` exists only on `Units`
266
+ and `InventoryAdjustmentItems` (not on the `*ItemUnits` bridge tables), and
267
+ `InventoryAdjustmentItems.locationId` **is** correctly populated (Elite: 6 rows at NetSuite
268
+ location 196 "Elite"/Chicago, 1 at 5 "New York"). Aggregate `_qtyOnHand` derives from
269
+ `ItemReceiptItems` joined through `PurchaseOrderItems` → `VendorItems`, not from
270
+ `Units.locationId`. When building an inventory view, read location from the **transaction items**.
152
271
  - **⚠ OPEN, UNTRACKED (found 2026-08-11): the GroWrk item-receipt sync has been failing every 5
153
272
  minutes since 2026-08-06.** `Logs.Issue` **124** for `clientId` **33**, *"Invalid API Response"* from
154
273
  `App_Api_Toga2::send()` (`library/app/api/toga2.php:3245`) via
@@ -245,6 +364,24 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
245
364
 
246
365
  ## Change history
247
366
 
367
+ - 2026-08-12 — TRUE-80499 (Elite onboarding, shared-engine work). **Fixed** the sparse-client
368
+ `TypeError`: the two per-client item lookups are now initialised at
369
+ `common_sync_togasupply.php:74-75` instead of only inside the items loop, so a client whose items
370
+ have neither a manufacturer nor a catalog no longer passes `null` into an `array &` parameter.
371
+ **Built** the per-client window override (`MAX_TIME_WINDOW_TO_FETCH_FROM_NETSUITE_SECONDS` is now
372
+ `defined()||define()`, so a wrapper can widen its catch-up window; Elite uses 30 days, the other 17
373
+ stay at 5). Recorded four corrections that cost real diagnostic time: a **shrinking window is a bug
374
+ signal, not a load signal** (nothing in the script has a timeout — "didn't finish" means "threw");
375
+ a **per-record failure does not stop the cursor**, so the record is skipped permanently while the
376
+ section still reports IDLE and advances (recovery = rewind `NETSUITE_LAST_SYNC_DATETIME_<SECTION>`;
377
+ re-import is idempotent); **sections are independent** so one cursor can be rewound alone and a
378
+ skewed PO cursor is not a defect; and **item receipts are attributed via the `createdFrom` PO's
379
+ customer**, not by warehouse location. Also recorded the **malformed auto-created countries**
380
+ (`_unitedKingdom` → code `_i`), that `Units.locationId` is never populated by this sync, and the two
381
+ Elite operator scripts plus the `isProcessRunning()` own-filename trap that lets a manual script
382
+ race the scheduled cron. Client-specific results in
383
+ [Elite NetSuite → TOGa Supply sync](../../../clients/elite/features/netsuite-togasupply-sync.md).
384
+ (snaredla)
248
385
  - 2026-08-11 — TRUE-80824 (side finding, no code change): recorded an **open, untracked** failure —
249
386
  GroWrk (`clientId` 33) `syncItemReceiptFromNetsuite` has raised "Invalid API Response" from
250
387
  `App_Api_Toga2::send()` every 5 minutes since 2026-08-06 (`Logs.Issue` 124, ~288 events/day).
@@ -6,14 +6,19 @@ project: Worker
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-06-16
10
- owners: ["dfranks"]
9
+ updated: 2026-08-12
10
+ owners: ["dfranks", "snaredla"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/sync_togasupply.php
13
13
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
14
+ - worker/crons/toga2/netsuite/sync_togasupply_elite.php
14
15
  - worker/schedules/cron.worker.sync.json
15
16
  - dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql
16
- related: []
17
+ - dbchanges2/_modules/netsuite/2026-08-05 - CLEAN NETSUITE CLINET.SQL
18
+ related:
19
+ - ../features/netsuite-togasupply-per-client-sync.md
20
+ - ../../../clients/elite/features/netsuite-togasupply-sync.md
21
+ - ../../../2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md
17
22
  ---
18
23
 
19
24
  ## Summary
@@ -49,6 +54,71 @@ API (`App_Api_Toga2::send`), which lands in that client's `Client_<Name>` databa
49
54
  5. **Deploy order matters:** the Parameters seed must hit prod `Client_<Name>` **before or
50
55
  with** the cron going live, or the sync 404-aborts (and throws a Sentry error) every 5
51
56
  minutes.
57
+ 6. **Seed the shared synthetic "Agilant" vendor row** — see the dedicated section below. The
58
+ `netsuite` dbchanges2 module does **not** do it, and without it every order carrying a customer PO
59
+ number fails.
60
+ 7. **Grant `AclCustomFieldPermissions` for the module's custom fields** — also below. The `netsuite`
61
+ module registers `CustomRecordFields` rows and adds the `c_` columns but grants **no field-level
62
+ ACL**, so api2 answers `403 EV-2`.
63
+ 8. **Pre-seed `Countries`** for every country the client ships to, with the correct `code`/`name`
64
+ **and** the exact NetSuite `c_netsuiteCountry` string (`_unitedKingdom`, `_canada`, …). If you
65
+ skip this, the sync auto-creates the row from the NetSuite string and derives a **malformed**
66
+ name and a collision-prone 2-char code — mechanics and examples in the
67
+ [engine doc](../features/netsuite-togasupply-per-client-sync.md).
68
+ 9. **Widen the window only if you are backfilling deep history.** A wrapper may
69
+ `const MAX_TIME_WINDOW_TO_FETCH_FROM_NETSUITE_SECONDS = …;` before the `require_once` (Elite: 30
70
+ days while backfilling from 2024). At the 5-day default on a 5-minute cron, ~2.4 years of history
71
+ takes days to catch up.
72
+
73
+ ## The synthetic "Agilant" vendor must exist in the client's own DB (found 2026-08-06)
74
+
75
+ 13 of the 18 wrappers pass the **same** vendor uuid
76
+ `10ed18ec-0999-53d9-f9db-5892d90f09d4` ("Agilant") as
77
+ `CLIENT_CONFIGURATION['importCustomerPurchaseOrdersOnVendor']` — but `Vendors` is a **per-client
78
+ table**, so that uuid has to exist as a row in **every** `Client_<Name>.Vendors`. **Nothing in
79
+ `dbchanges2/_modules/netsuite/` seeds it** (verified: the uuid appears in no file in the repo).
80
+
81
+ Symptom when it is missing: **every** sales order that carries a customer PO number (NetSuite
82
+ `otherRefNum`) fails `POST /purchase-orders` with **400 `EV-12` "unable to find a unique match"** on
83
+ the nested vendor. Orders without a customer PO import fine, so the failure looks partial and
84
+ data-dependent rather than structural. On Elite this produced **191 Sentry events in one day**
85
+ (`WORKER1-420`) and `PurchaseOrders` stuck at **0**; after seeding the row it went to **234**.
86
+
87
+ Seed it with a `dbchanges2/Client_<Name>/<YYYY-MM-DD><letter> - <Name>AgilantVendor.sql` inserting
88
+ the vendor with that **exact** uuid (reuse it — a fresh UUID defeats the whole shared-uuid pattern).
89
+
90
+ > **The undocumented pattern to internalise: a uuid shared across clients still needs a row per
91
+ > client database.** It reads like a global id and behaves like a per-tenant one.
92
+
93
+ ## The `netsuite` module grants no field-level ACL (found 2026-08-06)
94
+
95
+ `dbchanges2/_modules/netsuite/2026-08-05 - CLEAN NETSUITE CLINET.SQL` inserts the
96
+ `CustomRecordFields` rows and adds the `c_` columns but contains **zero**
97
+ `AclCustomFieldPermissions` statements (`grep -c` = 0; the smaller
98
+ `2026-07-10a - UnitInventoryFields.sql` *does* grant, which is why the gap is easy to miss).
99
+
100
+ Symptom: api2 returns **403 `EV-2` "do not have the necessary permissions to read the specified
101
+ fields"** — a whole-request failure, not a silently dropped field.
102
+
103
+ Measured on Elite vs. a healthy client: **Elite 27 fields / 0 grants; Quad 26 / 25** grants to
104
+ **role 1 (Base), `isWritable = 1`**. Fixed with
105
+ `dbchanges2/Client_Elite/2026-08-06a - EliteNetsuiteCustomFieldAcl.sql`. Same failure class as the
106
+ GroWrk incidents — the general audit playbook is
107
+ [client schema-drift audit](../../../2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md).
108
+
109
+ **Post-onboarding check, per client:** every `CustomRecordFields` row added by the module should have
110
+ exactly one grant. Count fields vs. grants and diff against a known-good client before declaring the
111
+ onboarding done.
112
+
113
+ ## Recovering records the first run skipped
114
+
115
+ A record that throws mid-loop is logged to Sentry and **stepped over while the cursor still
116
+ advances** — so a clean-looking IDLE section can be missing records (see the engine doc). Recovery is
117
+ to rewind `NETSUITE_LAST_SYNC_DATETIME_<SECTION>` in the client's `Parameters` table to before the
118
+ bad window. Re-import is **idempotent** (matched on `c_netsuiteInternal*Id`;
119
+ `SalesOrders.c_netsuiteInternalSalesOrderId` has a unique index) and sections are independent, so one
120
+ cursor can be rewound alone. Expect to do this at least once on any onboarding where a provisioning
121
+ gap was found *after* the cron started running.
52
122
 
53
123
  ## Parameters seed (the required, easily-missed step)
54
124
  The sync reads/writes per-client sync state via the TOGa2 API `/parameters` endpoint, which
@@ -103,11 +173,30 @@ the directory; no `USE`).
103
173
  end-user matching. Always probe NetSuite first.
104
174
  - **Parameters not seeded in prod** → sync throws every 5 min; surfaces as a `worker1` Sentry
105
175
  error and zero imports.
106
- - **Missing catalog item** on an incoming order may throw mid-run depending on the section;
107
- the checkpoint does not advance past a throwing window, so it self-heals once the dependency
108
- exists.
176
+ - **⚠ CORRECTED 2026-08-12 — a failing record does NOT self-heal.** This section previously said
177
+ "the checkpoint does not advance past a throwing window, so it self-heals once the dependency
178
+ exists." **That is wrong for a per-record failure.** The cursor write and `finishModeIteration()`
179
+ run unconditionally *after* the record loop, so a record that throws is Sentry-logged, stepped
180
+ over, and **permanently skipped** while the section reports success. Only a throw that escapes the
181
+ loop (setup/lookup phase, transport) leaves the checkpoint behind. Fix a provisioning gap and you
182
+ must **rewind the cursor** — see *Recovering records the first run skipped* above.
183
+ - **⚠ OPEN: `dbchanges2/_modules/netsuite/2026-08-05 - CLEAN NETSUITE CLINET.SQL` sits in the
184
+ SHARED module folder** and will run against **all 18 opted-in clients** on the next unscoped run.
185
+ It also carries a `CLINET` typo and **no letter suffix**, which the dbchanges2
186
+ `YYYY-MM-DD<letter>` contract requires. Know it is there before you trigger a module run.
109
187
 
110
188
  ## Change history
189
+ - 2026-08-12 — TRUE-80499 (Elite): added the **three provisioning steps this workflow was missing**,
190
+ each found by a production failure rather than by reading the module. (1) The shared synthetic
191
+ **"Agilant" vendor** uuid `10ed18ec-…f09d4` must be seeded as a **row in every**
192
+ `Client_<Name>.Vendors` — nothing in the `netsuite` module does it, and without it every order with
193
+ a customer PO number 400s `EV-12` on the nested vendor (Elite: 191 Sentry events/day, PurchaseOrders
194
+ 0 → 234 after seeding). (2) The module registers `CustomRecordFields` and the `c_` columns but grants
195
+ **no `AclCustomFieldPermissions`** → 403 `EV-2` (Elite 27 fields / 0 grants vs. Quad 26 / 25 to role
196
+ 1). (3) **Pre-seed `Countries`** or the sync auto-creates malformed rows from the NetSuite string.
197
+ Also added the per-client backfill-window override, a **cursor-rewind recovery** procedure, and
198
+ **corrected** this doc's claim that a throwing record self-heals — it does not, the cursor advances
199
+ past it. Flagged the unsuffixed shared `CLEAN NETSUITE CLINET.SQL` as an open hazard. (snaredla)
111
200
  - 2026-06-16 — Documented the onboarding process after adding Quad (TRUE-79575): wrapper +
112
201
  schedule + the required 12-key Parameters seed; captured the `isParentCustomer` NetSuite
113
202
  probe and the local Client_/Logs_ schema requirement. (dfranks)
@@ -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)_ — 17 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 21 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 22 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) — 12 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -2,9 +2,10 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
+ | [Elite — NetSuite → TOGa Supply inbound sync (TRUE-80499 onboarding)](features/netsuite-togasupply-sync.md) | 1.0 | Elite is the 18th client on the shared NetSuite → TOGa Supply importer ([engine](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)). | worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/PurchaseOrder.php, _underscore/Model/Elite/ItemReceipt.php, dbchanges2/Client_Elite/_modules.txt, test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php, test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php |
5
6
  | [Elite SalesOrder → NetSuite Push (postPost/postPut interceptors → worker2)](features/salesorder-netsuite-push.md) | 2.0 | Elite orders created in Toga are pushed into NetSuite **event-driven**, not on a cron. | _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql |
6
7
  | [Elite — Sales Order stage change posts a reply on the TOGa Desk (1.0) ticket](features/salesorder-status-togadesk-reply.md) | 2.0 | When an Elite sales order's **stage** changes, a reply is posted on the originating **TOGa Desk (1.0)** ticket so the requester sees progress where they raised | worker2/Worker/Sync/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrder.php |
7
8
  | [Elite — supply2 frontend scope (Inventory + Service Requests, both built)](features/supply2-scope.md) | 2.0 | Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). | toga2-supply/ELITE-CLIENT-TASK-NOTES.md, toga2-supply/src/pages/Orders/view/OrderView/viewModel/FIELDS/ELITE/BASEFIELDS.json, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/hooks/useFetchData.tsx, toga2-supply/src/components/ui/Toaster.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/viewModel/index.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/package.json |
8
9
  | [Elite — stale TableView config (11 dead Core.RecordFields across 9 views)](features/supply2-tableview-config-drift.md) | 2.0 | `Client_Elite`'s `TableViewJoins` predate **two** platform bridge-table migrations and still reference **11 deleted `Core.RecordFields` ids (211, 321, 932, 358, | dbchanges2/Client_Elite/, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
9
10
  | [Elite — raising a Service Request from a TOGa Desk ticket (App_Api_ServiceRequest)](features/togadesk-service-request-intake.md) | 1.0 | An Elite agent raises a **Service Request** from a TOGa Desk (1.0) ticket via a modal. | library/app/api/servicerequest.php, library/app/model/togadesk/ticket.php, togadesk/desk/includes/classes/class.ticket.php, togadesk/desk/template/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/actions/tickets/serviceRequest.php, dbchanges2/Client_Elite/2026-08-11a - EliteServiceRequestTicketUnique.sql, test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php |
10
- | [Elite](profile.md) | 2.0 | Elite is a managed-services client that uses **Freshservice** as their helpdesk platform. | worker2/Worker/Elite.php, worker2/Worker/Sync/ServiceRequest.php, library/app/api/toga2.php, library/app/api/servicerequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, togadesk/desk/includes/classes/class.ticket.php |
11
+ | [Elite](profile.md) | 2.0 | Elite is a managed-services client that uses **Freshservice** as their helpdesk platform. | worker2/Worker/Elite.php, worker2/Worker/Sync/ServiceRequest.php, library/app/api/toga2.php, library/app/api/servicerequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, togadesk/desk/includes/classes/class.ticket.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php |
@@ -0,0 +1,141 @@
1
+ ---
2
+ title: "Elite — NetSuite → TOGa Supply inbound sync (TRUE-80499 onboarding)"
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: elite
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-08-12
10
+ owners: ["snaredla"]
11
+ files:
12
+ - worker/crons/toga2/netsuite/sync_togasupply_elite.php
13
+ - worker/crons/toga2/netsuite/common_sync_togasupply.php
14
+ - worker/schedules/cron.worker.sync.json
15
+ - _underscore/Model/Elite/SalesOrder.php
16
+ - _underscore/Model/Elite/PurchaseOrder.php
17
+ - _underscore/Model/Elite/ItemReceipt.php
18
+ - dbchanges2/Client_Elite/_modules.txt
19
+ - test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php
20
+ - test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php
21
+ related:
22
+ - ../profile.md
23
+ - ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
24
+ - ../../../1.0/apps/worker/workflows/onboarding-client-to-netsuite-togasupply-sync.md
25
+ - ./salesorder-netsuite-push.md
26
+ - ./supply2-scope.md
27
+ - ../../growrk/features/units-netsuite-custom-fields.md
28
+ ---
29
+
30
+ ## Summary
31
+
32
+ Elite is the 18th client on the shared NetSuite → TOGa Supply importer
33
+ ([engine](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)). This doc records
34
+ what is **Elite-specific**; everything about how the engine works, and the two provisioning gaps this
35
+ onboarding discovered, lives in the engine and
36
+ [onboarding workflow](../../../1.0/apps/worker/workflows/onboarding-client-to-netsuite-togasupply-sync.md)
37
+ docs.
38
+
39
+ **Direction matters:** this is the **inbound** half (NetSuite → TOGa). The **outbound** push of Elite
40
+ orders *into* NetSuite is a separate, event-driven path — see
41
+ [salesorder-netsuite-push](./salesorder-netsuite-push.md). Nothing in this cron ever writes to
42
+ NetSuite.
43
+
44
+ **Backfill result (2026-08, April 2024 → Aug 2026, ~90 minutes):** 257 SalesOrders (0 missing a
45
+ NetSuite id), 234 PurchaseOrders, 201 ItemFulfillments, 29 ItemReceipts, 2 InventoryAdjustments,
46
+ 153 Items, 229 Units, 1042 Contacts, 806 Addresses.
47
+
48
+ ## What was built
49
+
50
+ | Piece | Detail |
51
+ |---|---|
52
+ | **Wrapper** | `worker/crons/toga2/netsuite/sync_togasupply_elite.php` — `isParentCustomer` **false**, NetSuite customer internal id **36443**, end-user-customer custom field **3149** |
53
+ | **Schedule** | `cron.worker.sync.json`, `*/5 * * * *`, `active: 1` |
54
+ | **Client models** | 18 new `_underscore/Model/Elite/*.php` trait models (see below) |
55
+ | **Schema** | `dbchanges2/Client_Elite/_modules.txt` opts into `_modules/netsuite` |
56
+ | **Warehouse locations** | `2026-08-05b` (NetSuite **196** "Elite"/Chicago) and `2026-08-06d` (NetSuite **5** "New York") — both confirmed to carry real Elite inventory-adjustment activity before seeding |
57
+ | **Vendor + ACL fixes** | `2026-08-06e - EliteAgilantVendor.sql`, `2026-08-06a - EliteNetsuiteCustomFieldAcl.sql` — the two module gaps, [written up in the workflow](../../../1.0/apps/worker/workflows/onboarding-client-to-netsuite-togasupply-sync.md) |
58
+
59
+ ### The 18 trait models are pure convention — and fail SILENTLY
60
+
61
+ Each is only `class _Model_Elite_X extends _Model_Client_X { use _Trait_Netsuite_X; }`:
62
+ Country, Customer, InventoryAdjustment, Invoice, Item, ItemFulfillment, ItemFulfillmentItemUnit,
63
+ ItemFulfillmentStage, ItemReceipt, Location, PaymentTerm, PurchaseOrder, SalesOrder, SalesOrderItem,
64
+ SalesOrderStage, ShippingMethod, Subsidiary, Vendor.
65
+
66
+ Resolution is **pure class-name string substitution off the JWT client slug** —
67
+ `api2/Component/Api/V2/V2.php:2878-2886` reads `jwtPayload.id.client.slug` and does
68
+ `str_replace('_Model_Client_', '_Model_Elite_', …)` then `class_exists()`; the same block repeats at
69
+ :3148, :3744 and :4177 for nested/sort paths. **If `class_exists` fails, it falls back to the base
70
+ `_Model_Client_X` with no error, no log, and a 200 response** — the `c_netsuite*` fields simply are
71
+ not there. So a typo'd filename or class name presents as "the sync ran and imported nothing useful",
72
+ never as a failure.
73
+
74
+ The mirror-image trap is the one that bit GroWrk: once the model **does** resolve, every `c_` field
75
+ the trait declares must exist as a column, a `CustomRecordFields` row **and** an ACL grant, or reads
76
+ 500/403 — see [GroWrk NetSuite-module custom fields](../../growrk/features/units-netsuite-custom-fields.md).
77
+
78
+ ### Elite-only wrapper deviations
79
+
80
+ - **`MAX_TIME_WINDOW_TO_FETCH_FROM_NETSUITE_SECONDS = 2592000` (30 days)**, vs. the 5-day default the
81
+ other 17 wrappers inherit — Elite is backfilling from 2024 and 5-day windows on a 5-minute cron were
82
+ too slow. This is only possible because the shared engine now `define()`s the cap instead of
83
+ `const`-ing it. **Reduce it to the default once the backfill is caught up.**
84
+ - **`IS_ENABLED_INTEGRATION_INVOICES = false` — TEMPORARY.** The invoice section scans the whole
85
+ NetSuite transaction table per window, kept running out of time, and **starved the item-receipt and
86
+ item-fulfillment sections that follow it** — which are what `_qtyOnHand` derives from. Re-enable
87
+ once inventory has caught up. (Note the invoice section is also capped at `now − 86400s` by design,
88
+ so it always trails.)
89
+ - **`MIN_DATETIME_TO_CHECK_FOR_NETSUITE_DATA = '2018-01-01'`**, with the real start controlled by the
90
+ seeded `Parameters` cursors.
91
+
92
+ ## Gotchas / known issues
93
+
94
+ - **⚠ Two files named `sync_togasupply_elite.php`, both scheduled.**
95
+ `worker/crons/toga2/netsuite/sync_togasupply_elite.php` (`*/5`) is **this** supply importer;
96
+ `worker/crons/toga2/elite/sync_togasupply_elite.php` (`*/2`) is the older
97
+ `syncWithToga`/`syncWithTogadesk` desk bridge with its own parameter keys. They coexist harmlessly
98
+ but **confirm the folder before editing** — this is the generic name collision the engine doc warns
99
+ about, now real for Elite.
100
+ - **⚠ `Client_Elite` has TWO active `Apis` rows** (Agilant `id 1`, Elite `id 2`). `getClientContext()`
101
+ does `LIMIT 1` with **no `ORDER BY`**, so a worker can authenticate as the wrong client. Not yet
102
+ fixed; be explicit about which api uuid/secret you pass.
103
+ - **⚠ `Client_Elite.Countries` needs a cleanup migration.** Three rows were auto-created by the sync
104
+ from the NetSuite strings before `Countries` was pre-seeded, with malformed codes/names:
105
+ `_unitedKingdom` → `_united Kingdom` / `_i`, `_canada` → `_canada` / `_C`, `_australia` →
106
+ `_australia` / `_A`. Fix `code`/`name` to `GB`/`CA`/`AU` **without touching `c_netsuiteCountry`** —
107
+ that string is the sync's match key and must keep NetSuite's exact value (leading underscore
108
+ included). Mechanics in the
109
+ [engine doc](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
110
+ This matters for Elite specifically because **Elite ships internationally** (see the shipping
111
+ decision on the [profile](../profile.md)).
112
+ - **⚠ The four migrations named above (`2026-08-05b`, `2026-08-06a`, `2026-08-06d`, `2026-08-06e`) are
113
+ NOT in the `dbchanges2` checkout** — absent from `_main`'s working tree and from
114
+ `origin/TRUE-80499` as of 2026-08-12. They may be uncommitted on another machine/branch. Exactly the
115
+ GroWrk situation ("production is ahead of the repo"): until they are committed, the next
116
+ environment or client provisioned from `dbchanges2` re-creates this drift. **Do not re-write them
117
+ from memory — find the branch.**
118
+ - **`Units.locationId` is NULL for all 229 Elite Units.** The sync never populates it; transaction
119
+ location lives on `InventoryAdjustmentItems.locationId` (populated: 6 rows at NetSuite 196, 1 at 5).
120
+ Any Elite inventory table view must read location from the transaction items.
121
+ - **Sales order 169441 was missing from the first backfill** (NetSuite id 4624062, 2024-04-15,
122
+ customer PO `BEGGS/04152024/…`) because its window ran ~15 minutes before the vendor fix. It is the
123
+ worked example for the engine's cursor-advances-past-a-failed-record behaviour — recovered by
124
+ rewinding `NETSUITE_LAST_SYNC_DATETIME_SALES_ORDERS`.
125
+
126
+ ## Change history
127
+
128
+ - 2026-08-12 — TRUE-80499: **onboarded Elite to the NetSuite → TOGa Supply importer.** Built the
129
+ wrapper (customer 36443, `isParentCustomer` false, 30-day window, invoices temporarily disabled),
130
+ the schedule entry, 18 `_Model_Elite_*` netsuite trait models, the `_modules/netsuite` opt-in, and
131
+ the two warehouse Locations (NetSuite 196 Chicago, 5 New York). Fixed the two module gaps that
132
+ blocked it (missing shared "Agilant" `Vendors` row → 400 `EV-12` on every customer-PO order, 191
133
+ Sentry events/day, PurchaseOrders 0 → 234; and 27 custom fields with 0 ACL grants → 403 `EV-2`) —
134
+ both generalised into the
135
+ [onboarding workflow](../../../1.0/apps/worker/workflows/onboarding-client-to-netsuite-togasupply-sync.md).
136
+ Full backfill April 2024 → Aug 2026 completed in ~90 minutes (257 SalesOrders / 234 PurchaseOrders /
137
+ 201 ItemFulfillments / 29 ItemReceipts / 153 Items / 229 Units / 1042 Contacts / 806 Addresses).
138
+ Recorded the silent `class_exists` fallback in client-model resolution, the duplicate
139
+ `sync_togasupply_elite.php` filename across two integrations, the two active `Apis` rows vs.
140
+ `getClientContext()`'s unordered `LIMIT 1`, the `Countries` cleanup still owed, and that the four
141
+ dbchanges2 migrations are not in the repo checkout. (snaredla)
@@ -154,6 +154,16 @@ wrong-country address.
154
154
  select2 state list kept emptying the dropdown — the file's own comment records this. The
155
155
  server-side pair check is the guard; the modal stays as-is.
156
156
 
157
+ ### …and that is the ONLY address rule Elite wants (confirmed 2026-08-12)
158
+
159
+ Elite confirmed (via Bala): **no address validation — Elite ships internationally.** So the modal
160
+ must **not** enforce US state/postal formats and **must not** copy Prudential's `_unitedStates`
161
+ hardcoding. The country/state **pair** check above stays, because it is a correctness guard (a 2.0
162
+ `Addresses` row derives its country from its state), not a US-format rule — and the country must still
163
+ resolve to a real `Countries` row for the downstream NetSuite push. Elite also **dictates the shipping
164
+ method on the Sales Order: FedEx only.** Both rules and their reference orders are on the
165
+ [Elite profile](../profile.md#shipping-rules-confirmed-with-elite-by-bala-2026-08-12).
166
+
157
167
  ## Test harness
158
168
 
159
169
  `test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php` (PHP **7.2**)
@@ -204,6 +214,12 @@ them, so it proves everything *after* the permission check, not the check itself
204
214
 
205
215
  ## Change history
206
216
 
217
+ - 2026-08-12 (shipping rules) — Recorded Elite's confirmed shipping decisions (via Bala): the
218
+ **shipping method is dictated on the Sales Order and is FedEx only**, and there is deliberately
219
+ **no address validation** because Elite ships internationally — so the modal must not enforce US
220
+ state/postal formats and must not reuse Prudential's `_unitedStates` hardcoding. The country/state
221
+ pair check stays (correctness, not US-format), and the country must still resolve to a `Countries`
222
+ row for the NetSuite push. (snaredla)
207
223
  - 2026-08-12 (later pass) — Recorded the **button gate** itself
208
224
  (`isEliteServiceRequestTicket()`: client 163 + department 299 + custom field 88 = `Service
209
225
  Request`; the button **replaces** "New Part Request" on those tickets, and the 2.0 uuid comes from
@@ -2,6 +2,7 @@
2
2
  title: Elite
3
3
  framework: "2.0"
4
4
  apps:
5
+ - worker
5
6
  - worker2
6
7
  - library
7
8
  - toga2-supply
@@ -24,7 +25,9 @@ files:
24
25
  - _underscore/Model/Elite/SalesOrder.php
25
26
  - _underscore/Model/Elite/ServiceRequest.php
26
27
  - togadesk/desk/includes/classes/class.ticket.php
28
+ - worker/crons/toga2/netsuite/sync_togasupply_elite.php
27
29
  related:
30
+ - features/netsuite-togasupply-sync.md
28
31
  - 2.0/apps/worker2/features/elite-freshservice-sync.md
29
32
  - 1.0/apps/library/features/elite-freshservice-sync.md
30
33
  - features/supply2-scope.md
@@ -49,6 +52,27 @@ on beta as of 2026-08-10; see [supply2-scope](features/supply2-scope.md). The ba
49
52
  dbchanges2 **PR #454** (TRUE-80499, snaredla24, 2026-08-05) added the **netsuite** module to
50
53
  `Client_Elite/_modules.txt` — the supply-chain schema layer is arriving.
51
54
 
55
+ **Elite is live on the inbound NetSuite → TOGa Supply sync as of 2026-08-12 (TRUE-80499).** A
56
+ `*/5` worker cron backfilled April 2024 → Aug 2026 in ~90 minutes (257 sales orders, 234 purchase
57
+ orders, 201 item fulfillments, 153 items, 229 units) — this is what adds **`worker`** (1.0) to
58
+ Elite's app scope. Elite-specific config, the still-owed `Countries` cleanup, and the two active
59
+ `Apis` rows hazard are in [netsuite-togasupply-sync](features/netsuite-togasupply-sync.md).
60
+
61
+ ## Shipping rules (confirmed with Elite by Bala, 2026-08-12)
62
+
63
+ Two client rules that constrain every order-creation surface (the desk Service Request modal, the
64
+ supply2 order forms, and the NetSuite push):
65
+
66
+ - **Elite dictates the shipping method on the Sales Order, and it is FedEx only** — the shipping-method
67
+ field must accept FedEx carrier values only.
68
+ - **No address validation.** Elite ships **internationally**, so **do not port Prudential's
69
+ `_unitedStates` hardcoding** and the SR modal must **not** enforce US state/postal formats.
70
+ Reference sales orders: **285264, 285379, 285346**.
71
+
72
+ The country still has to resolve to a real `Countries` row for the NetSuite push to succeed — "no
73
+ address validation" means no US-format enforcement, not "any string goes." Full reasoning is recorded
74
+ in `test/@srija/DOCS/ELITE/ELITE_SR_PLAN_BALA.md` and `ELITE_SR_PLAN_COMBINED.md`.
75
+
52
76
  Elite's service-request data model: `ServiceRequests` is the record the `service-requests` table
53
77
  view is built on (joining Tickets / Customers / ServiceRequestTypes / SalesOrders, with
54
78
  `SalesOrders.serviceRequestId = ServiceRequests.id`). **That relationship is 1:N, not 1:1**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.563",
3
+ "version": "1.0.564",
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",