toga-ai 1.0.562 → 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.
- package/knowledge/1.0/apps/library/INDEX.md +0 -1
- package/knowledge/1.0/apps/worker/INDEX.md +2 -2
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +140 -3
- package/knowledge/1.0/apps/worker/workflows/onboarding-client-to-netsuite-togasupply-sync.md +95 -6
- package/knowledge/2.0/apps/worker2/features/elite-freshservice-sync.md +18 -2
- package/knowledge/INDEX.md +4 -4
- package/knowledge/clients/elite/INDEX.md +2 -3
- package/knowledge/clients/elite/features/netsuite-togasupply-sync.md +141 -0
- package/knowledge/clients/elite/features/togadesk-service-request-intake.md +16 -0
- package/knowledge/clients/elite/profile.md +24 -0
- package/package.json +1 -1
- package/knowledge/1.0/apps/library/features/service-request-toga2-provisioning.md +0 -135
- package/knowledge/clients/elite/features/desk-service-request-creation.md +0 -136
- package/knowledge/clients/elite/features/service-request-to-salesorder-pipeline.md +0 -109
|
@@ -16,6 +16,5 @@
|
|
|
16
16
|
| [NetSuite SuiteQL/REST API Reference](features/netsuite-suiteql-api-reference.md) | General working reference for the Agilant NetSuite integration: how to authenticate, how SuiteQL behaves, and the confirmed schema of the tables/columns/codes w | library/app/api/netsuite/rest.php, library/ssl/netsuite_ec_key.pem, test/@dave/Junk Drawer/nsq.php |
|
|
17
17
|
| [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php |
|
|
18
18
|
| [NetSuite Sync Alert Monitor (App_SystemMonitor_NetSuiteIntegration)](features/netsuite-sync-alert-monitor.md) | `App_SystemMonitor_NetSuiteIntegration` (`library/app/systemmonitor/netsuiteintegration.php`, title **"NetSuite Sync Alert"**) is a 1.0 system monitor that watc | library/app/systemmonitor/netsuiteintegration.php, worker/crons/infrastructure/system_monitors.php |
|
|
19
|
-
| [App_Api_ServiceRequest — provisioning a TOGa 2.0 Service Request from 1.0](features/service-request-toga2-provisioning.md) | `App_Api_ServiceRequest` (`library/app/api/servicerequest.php`) is the shared 1.0-side class that turns a posted form into a **TOGa 2.0 Service Request** — it v | library/app/api/servicerequest.php, library/app/api/toga2.php, togadesk/desk/includes/classes/class.ticket.php |
|
|
20
19
|
| [Startech PC Matic B2B Sync (library)](features/startech-pcmaticb2b-sync.md) | `library/app/api/toga2.php` handles bidirectional ticket sync for PC Matic B2B between TOGaDesk 1.0 and TOGA 2.0. | library/app/api/toga2.php, library/app/api/startechticket.php, worker/crons/toga2/startech/common_import_supporting_records.php |
|
|
21
20
|
| [App_Api_Toga2 — TOGa2 API Client & 1.0↔2.0 Sync Bridge](features/toga2-api-client-and-bridge.md) | `App_Api_Toga2` (`library/app/api/toga2.php`, ~8400 lines) is the **1.0-side client for the TOGa 2 (`_underscore`/api2) public API** *and* the home of the cross | library/app/api/toga2.php, worker/crons/toga2/aig/sync_togasupply_aig.php, worker/crons/toga2/wje/sync_togasupply_wje.php, test/@Mark/AIG/test_multi_email.php |
|
|
@@ -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-
|
|
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
|
|
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).
|
package/knowledge/1.0/apps/worker/workflows/onboarding-client-to-netsuite-togasupply-sync.md
CHANGED
|
@@ -6,14 +6,19 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
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
|
-
|
|
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
|
-
-
|
|
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)
|
|
@@ -139,9 +139,25 @@ To test worker2 Elite code locally against production TOGA 2 and Freshservice:
|
|
|
139
139
|
require '_underscore.php';
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
4. **PHP version** — worker2
|
|
143
|
-
|
|
142
|
+
4. **PHP version** — worker2's Composer install targets **PHP >= 8.5**, so on a local **8.1**
|
|
143
|
+
machine every script dies in `vendor/composer/platform_check.php` before any of your code runs.
|
|
144
|
+
**The flag-based escapes do not work here** — three were tried (the `--ignore-platform-req`
|
|
145
|
+
family and friends) and none of them stopped the runtime check, because the check is a
|
|
146
|
+
*generated file* that `autoload.php` requires unconditionally; the flags only affect
|
|
147
|
+
dependency *resolution* at install time.
|
|
148
|
+
The one thing that works is **temporarily neutralizing `vendor/composer/platform_check.php`**
|
|
149
|
+
and restoring it immediately afterwards. Two properties make that safe: the file is
|
|
150
|
+
**gitignored** (so it can never be committed by accident), and it is regenerated by any
|
|
151
|
+
`composer install`/`dump-autoload`. Still, wrap it so the restore is guaranteed even if the
|
|
152
|
+
script fails — a half-bypassed vendor tree is a confusing thing to come back to. Never leave it
|
|
153
|
+
neutralized, and never "fix" this by lowering the platform requirement.
|
|
144
154
|
|
|
145
155
|
## Change history
|
|
156
|
+
- 2026-08-12 — Expanded the local-PHP-version note: on a PHP 8.1 machine the **flag-based
|
|
157
|
+
`--ignore-platform-req` escapes do not help**, because `platform_check.php` is a generated file
|
|
158
|
+
required unconditionally by `autoload.php` and the flags only affect install-time resolution.
|
|
159
|
+
The working approach is to temporarily neutralize `vendor/composer/platform_check.php` with a
|
|
160
|
+
**guaranteed restore** — safe because the file is gitignored and regenerated by any
|
|
161
|
+
`composer install`. (snaredla)
|
|
146
162
|
- 2026-06-11 — Redacted Elite credential constants from the doc; secret scanner added to `validate`. (snaredla)
|
|
147
163
|
- 2026-06-10 — Documented `_Worker_Elite` Freshservice → TOGA 2 webhook sync (conversation/attachment handling, TOGA 2 API gotchas, local dev setup). (snaredla)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -4,11 +4,11 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
4
4
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
|
-
- **library** (Library) _(framework core)_ —
|
|
8
|
-
- **worker** (Worker) —
|
|
7
|
+
- **library** (Library) _(framework core)_ — 17 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/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
|
-
- **togadesk** (TOGa Desk) —
|
|
11
|
+
- **togadesk** (TOGa Desk) — 12 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
|
|
12
12
|
- **togaview** (TOGa View) — 7 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
|
|
13
13
|
- **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
|
|
14
14
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
21
|
- **_underscore** (_Underscore) _(framework core)_ — 54 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
|
-
- **worker2** (Worker) —
|
|
22
|
+
- **worker2** (Worker) — 48 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 22 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 6 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
-
| [Elite —
|
|
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 |
|
|
6
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 |
|
|
7
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 |
|
|
8
|
-
| [Elite — Service Request → Sales Order → status back to TOGa Desk 1.0](features/service-request-to-salesorder-pipeline.md) | 2.0 | Once a Service Request exists in 2.0 (raised from [TOGa Desk](./desk-service-request-creation.md) or in supply2), an interceptor-driven chain turns it into a Sa | worker2/Worker/Sync/ServiceRequest.php, worker2/Worker/Sync/SalesOrderStatus.php, _underscore/Model/Elite/ServiceRequest.php, _underscore/Model/Elite/SalesOrder.php, test/@srija/Elite Testing/Service Requests/test_elite_chain_toga2.php |
|
|
9
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 |
|
|
10
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 |
|
|
11
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 |
|
|
12
|
-
| [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,135 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: App_Api_ServiceRequest — provisioning a TOGa 2.0 Service Request from 1.0
|
|
3
|
-
framework: "1.0"
|
|
4
|
-
repo: library
|
|
5
|
-
project: Library
|
|
6
|
-
client: shared
|
|
7
|
-
type: feature
|
|
8
|
-
status: active
|
|
9
|
-
updated: 2026-08-12
|
|
10
|
-
owners: [snaredla]
|
|
11
|
-
files:
|
|
12
|
-
- library/app/api/servicerequest.php
|
|
13
|
-
- library/app/api/toga2.php
|
|
14
|
-
- togadesk/desk/includes/classes/class.ticket.php
|
|
15
|
-
related:
|
|
16
|
-
- ./toga2-api-client-and-bridge.md
|
|
17
|
-
- ./app-class-placement-base-contracts.md
|
|
18
|
-
- ../../../clients/elite/features/desk-service-request-creation.md
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## Summary
|
|
22
|
-
|
|
23
|
-
`App_Api_ServiceRequest` (`library/app/api/servicerequest.php`) is the shared 1.0-side class that
|
|
24
|
-
turns a posted form into a **TOGa 2.0 Service Request** — it validates and normalises the posted
|
|
25
|
-
values, resolves the related 2.0 records it needs, refuses a duplicate, and POSTs the Service
|
|
26
|
-
Request with its nested units in a single request. All transport goes through
|
|
27
|
-
[`App_Api_Toga2::send()`](./toga2-api-client-and-bridge.md); this class holds no 1.0 table and does
|
|
28
|
-
no SQL.
|
|
29
|
-
|
|
30
|
-
First consumer is the Elite "New Service Request" button in TOGa Desk (see
|
|
31
|
-
[Elite desk Service Request creation](../../../clients/elite/features/desk-service-request-creation.md)),
|
|
32
|
-
but the class itself is client-agnostic — the caller passes the API endpoint in.
|
|
33
|
-
|
|
34
|
-
## Key files / entry points
|
|
35
|
-
|
|
36
|
-
`abstract class App_Api_ServiceRequest` — public:
|
|
37
|
-
|
|
38
|
-
- `buildFromPostedValues(array $postedValues, string $ticketUuid, ?string $overrideWithApiEndpoint = null): array`
|
|
39
|
-
— validates + normalises the modal post into the 2.0 payload array (throws on a validation
|
|
40
|
-
failure; the caller converts that into the UI message).
|
|
41
|
-
- `searchTicketFromToga2(string $ticketUuid, ?string $overrideWithApiEndpoint = null)` — the
|
|
42
|
-
**duplicate guard**: does a Service Request already exist for this ticket?
|
|
43
|
-
- `createServiceRequestInToga2(array $serviceRequest, ?string $overrideWithApiEndpoint = null)` —
|
|
44
|
-
the POST (parent + nested units).
|
|
45
|
-
- `describeApiFailureMessage($response): string` — turns a 2.0 failure response into an
|
|
46
|
-
agent-readable message.
|
|
47
|
-
|
|
48
|
-
Private: `isStateInCountry()`, `findContactUuidByEmailAddress()`, `cleanPostedValue()`.
|
|
49
|
-
|
|
50
|
-
## How it works
|
|
51
|
-
|
|
52
|
-
1. **Build** — `buildFromPostedValues()` trims/length-caps every posted value
|
|
53
|
-
(`cleanPostedValue()`), resolves the requester contact by email, checks the selected state
|
|
54
|
-
really belongs to the selected country, and assembles the parent payload plus one nested
|
|
55
|
-
`serviceRequestUnits` entry per line.
|
|
56
|
-
2. **Duplicate guard** — `searchTicketFromToga2()` looks the ticket uuid up in 2.0 before creating
|
|
57
|
-
anything.
|
|
58
|
-
3. **Create** — `createServiceRequestInToga2()` POSTs **parent + nested `serviceRequestUnits` in
|
|
59
|
-
ONE request**. Related records inside that payload resolve by **natural key** — the request type
|
|
60
|
-
by *name*, the item by *partNumber* — while **customer, state and ticket are matched by uuid**.
|
|
61
|
-
4. The caller (TOGa Desk) maps the outcome to its own status code and writes the ticket
|
|
62
|
-
history/reply.
|
|
63
|
-
|
|
64
|
-
## The failure-check contract — `empty($response->isSuccess)` is always safe
|
|
65
|
-
|
|
66
|
-
This is the load-bearing fact behind three bugs fixed on 2026-08-12, and it is not obvious from the
|
|
67
|
-
call site.
|
|
68
|
-
|
|
69
|
-
`App_Api_Toga2::send()` (`library/app/api/toga2.php`, ~L432–450) **always returns a response object
|
|
70
|
-
that carries `isSuccess`**:
|
|
71
|
-
|
|
72
|
-
- it returns early **only** when `isSuccess` is true;
|
|
73
|
-
- when `isSuccess` is false and `$throwExceptionOnApiError = false`, it falls through the retry loop
|
|
74
|
-
and ends at `return $response` — **a failure arrives as a return value, not an exception**;
|
|
75
|
-
- if the response is not an object, or lacks the `isSuccess` property at all, it **throws**.
|
|
76
|
-
|
|
77
|
-
Therefore anything `send()` hands back can be checked with `empty($response->isSuccess)` — including
|
|
78
|
-
GETs. There is no "successful response without the property" case to defend against.
|
|
79
|
-
|
|
80
|
-
**The bug class this prevents:** every helper here calls `send()` with
|
|
81
|
-
`$throwExceptionOnApiError = false`, so a network blip, an auth failure or a 500 comes back as a
|
|
82
|
-
*response*. Code that reads straight past it to `$response->data->{resource}` sees "no records" and
|
|
83
|
-
concludes **"the thing does not exist"** — the most dangerous possible misreading for a lookup.
|
|
84
|
-
|
|
85
|
-
## Fixed 2026-08-12 — three silent failures, one root cause
|
|
86
|
-
|
|
87
|
-
| Lookup | Old behavior on API failure | Consequence |
|
|
88
|
-
|---|---|---|
|
|
89
|
-
| duplicate Service Request | returned `null` | read as "no duplicate exists" → **a second Service Request on a ticket that already had one** |
|
|
90
|
-
| contact by email | returned `null` | read as "no contact exists" → **a duplicate contact created in 2.0** for someone 2.0 already held |
|
|
91
|
-
| state-belongs-to-country | returned `false` | a blip told the agent *"the selected state does not belong to the selected country"* — a **false accusation of bad input** |
|
|
92
|
-
|
|
93
|
-
`isStateInCountry()` now returns **`?bool` as a tri-state**: `true` confirmed, `false` a genuine
|
|
94
|
-
mismatch, **`null` unconfirmed** (exception or empty result). The submission is refused in both the
|
|
95
|
-
`false` and `null` cases — the difference is the **message**: only `false` blames the input, `null`
|
|
96
|
-
says the check could not be completed. Do not collapse this back into a `bool`.
|
|
97
|
-
|
|
98
|
-
## 2.0 API rules this class had to learn the hard way
|
|
99
|
-
|
|
100
|
-
- **Naming `fields` on `GET /states` SUPPRESSES the nested country.** A plain request returns the
|
|
101
|
-
nested `country`; adding a `fields` list drops it. If a nested relation vanishes, suspect the
|
|
102
|
-
field list before suspecting the data.
|
|
103
|
-
- **Post a state by UUID, never by code.** `WA` and `NT` each belong to **two** countries, and the
|
|
104
|
-
API matches on code alone. `States.countryId` has `isIdentifier = 0`, so the nested country cannot
|
|
105
|
-
disambiguate it either — the uuid is the only unambiguous handle.
|
|
106
|
-
- **Natural-key resolution inside a nested write:** type by name, item by partNumber. Only
|
|
107
|
-
customer / state / ticket go by uuid.
|
|
108
|
-
|
|
109
|
-
## Gotchas / known issues
|
|
110
|
-
|
|
111
|
-
- **A uuid is environment-scoped.** Any flow that reads uuids from one 2.0 environment and posts
|
|
112
|
-
them to another will fail or, worse, resolve to a different record. Read and write against the
|
|
113
|
-
**same** endpoint — see the hardcoded-endpoint gotcha in the
|
|
114
|
-
[Elite desk feature](../../../clients/elite/features/desk-service-request-creation.md).
|
|
115
|
-
- **This class extends nothing, deliberately.** Extending `App_Api` silently inherits the 1.0
|
|
116
|
-
fulfilment-vendor contract. See
|
|
117
|
-
[where a new App_ class goes](./app-class-placement-base-contracts.md).
|
|
118
|
-
- **Credentials.** Elite's `CLIENT_UUID_ELITE` / `API_UUID_ELITE` / `API_SECRET_ELITE` are class
|
|
119
|
-
constants on `App_Api_Toga2` (`library/app/api/toga2.php`). Read them from source; never
|
|
120
|
-
reproduce the values. An API secret living in a git-tracked constant is a standing concern — see
|
|
121
|
-
the credentials gotcha on [the transport doc](./toga2-api-client-and-bridge.md).
|
|
122
|
-
|
|
123
|
-
## Change history
|
|
124
|
-
|
|
125
|
-
- 2026-08-12 — TRUE-80497: created `App_Api_ServiceRequest` as the shared 1.0→2.0 Service Request
|
|
126
|
-
provisioner. Recorded the `send()` failure contract (**`empty($response->isSuccess)` is always a
|
|
127
|
-
safe check, including on GETs**) and fixed **three** silent failures that all came from reading
|
|
128
|
-
past a failure response returned under `$throwExceptionOnApiError = false`: the duplicate-SR
|
|
129
|
-
lookup and the contact lookup both reported "does not exist" (allowing a second SR and a
|
|
130
|
-
duplicate contact), and the state/country check reported a genuine mismatch for an exception or
|
|
131
|
-
empty result. `isStateInCountry()` now returns `?bool` — `null` = unconfirmed, with its own
|
|
132
|
-
message; both `null` and `false` still refuse. Also recorded the 2.0 rules: `fields` on
|
|
133
|
-
`GET /states` suppresses the nested country; states must be posted by uuid (`WA`/`NT` are
|
|
134
|
-
ambiguous across countries and `States.countryId` has `isIdentifier = 0`); parent + nested
|
|
135
|
-
`serviceRequestUnits` post in one request with type/item resolved by natural key. (snaredla)
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Elite — \"New Service Request\" button on TOGa Desk 1.0 tickets"
|
|
3
|
-
framework: "1.0"
|
|
4
|
-
repo: togadesk
|
|
5
|
-
project: TOGa Desk
|
|
6
|
-
client: elite
|
|
7
|
-
type: client-feature
|
|
8
|
-
status: active
|
|
9
|
-
updated: 2026-08-12
|
|
10
|
-
owners: [snaredla]
|
|
11
|
-
files:
|
|
12
|
-
- togadesk/desk/includes/controllers/modals/tickets/serviceRequest.php
|
|
13
|
-
- togadesk/desk/includes/controllers/actions/tickets/serviceRequest.php
|
|
14
|
-
- togadesk/desk/template/modals/tickets/serviceRequest.php
|
|
15
|
-
- togadesk/desk/includes/classes/class.ticket.php
|
|
16
|
-
- library/app/model/togadesk/ticket.php
|
|
17
|
-
- library/app/api/servicerequest.php
|
|
18
|
-
- dbchanges2/Client_Elite/2026-08-11a - EliteServiceRequestTicketUnique.sql
|
|
19
|
-
- test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php
|
|
20
|
-
related:
|
|
21
|
-
- ../../../1.0/apps/library/features/service-request-toga2-provisioning.md
|
|
22
|
-
- ../../../1.0/apps/togadesk/features/ticket-lifecycle.md
|
|
23
|
-
- ../../../1.0/apps/togadesk/workflows/standalone-test-scripts.md
|
|
24
|
-
- ./service-request-to-salesorder-pipeline.md
|
|
25
|
-
- ../profile.md
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Summary
|
|
29
|
-
|
|
30
|
-
TRUE-80497. Elite agents used to raise Service Requests **by hand, outside the desk**. A ticket that
|
|
31
|
-
qualifies now shows a **"New Service Request"** button that opens a modal and creates the Service
|
|
32
|
-
Request directly in **TOGa 2.0**, writing the resulting number back to the ticket history and as a
|
|
33
|
-
reply.
|
|
34
|
-
|
|
35
|
-
On a qualifying ticket the button **replaces** the existing **"New Part Request"** button — it is
|
|
36
|
-
not an extra button. Every other Elite department keeps the parts flow untouched.
|
|
37
|
-
|
|
38
|
-
## The gate — three conditions, one helper
|
|
39
|
-
|
|
40
|
-
`App_Model_TogaDesk_Ticket::isEliteServiceRequestTicket(int $clientId, int $departmentId, ?string $ticketTypeValue): bool`
|
|
41
|
-
(`library/app/model/togadesk/ticket.php`) is the single source of truth. It is `true` only when
|
|
42
|
-
**all three** hold:
|
|
43
|
-
|
|
44
|
-
| Condition | Constant |
|
|
45
|
-
|---|---|
|
|
46
|
-
| `tickets.clientid` = 163 (Elite) | `TOGADESK_CLIENT_ID__ELITE` |
|
|
47
|
-
| `tickets.departmentid` = 299 (Elite Helpdesk – Contact Center) | `TOGADESK_DEPARTMENT_ID__ELITE_CONTACT_CENTER` |
|
|
48
|
-
| custom field 88 "Ticket Type" = `Service Request` | `TOGADESK_CUSTOM_FIELD_ID__ELITE_TICKET_TYPE` + `ELITE_TICKET_TYPE__SERVICE_REQUEST` |
|
|
49
|
-
|
|
50
|
-
The template, the modal controller **and** `Ticket::createEliteServiceRequest()` all call the same
|
|
51
|
-
helper rather than repeating the three ids — re-checked server-side because the button being hidden
|
|
52
|
-
is not a security control.
|
|
53
|
-
|
|
54
|
-
## How it works
|
|
55
|
-
|
|
56
|
-
1. **Modal controller** (`controllers/modals/tickets/serviceRequest.php`) re-checks the gate,
|
|
57
|
-
resolves the ticket's 2.0 uuid from `tickets.referenceId`, then runs its lookups against 2.0
|
|
58
|
-
(customer/contact, states, countries, request types, items, existing Service Request) and
|
|
59
|
-
renders `template/modals/tickets/serviceRequest.php`.
|
|
60
|
-
2. **Action controller** (`controllers/actions/tickets/serviceRequest.php`) is a thin shim: it calls
|
|
61
|
-
`Ticket::createEliteServiceRequest($_POST)` and reroutes on the returned statuscode.
|
|
62
|
-
3. **`Ticket::createEliteServiceRequest()`** (`class.ticket.php`) owns the desk half only — gate,
|
|
63
|
-
ticket lookup, history and reply. Validation, duplicate detection and the API calls belong to
|
|
64
|
-
[`App_Api_ServiceRequest`](../../../1.0/apps/library/features/service-request-toga2-provisioning.md).
|
|
65
|
-
4. On success it writes a `TicketHistory` line (`Service Request created <number> (<type>, N units)`)
|
|
66
|
-
and adds a reply authored by `ELITE_DEFAULT_AGENT = 35876` (Agilant Helpdesk /
|
|
67
|
-
serviceops@togatech.com — the same author as the other machine-written replies on Elite tickets).
|
|
68
|
-
The reply passes the ticket's **current** status as `newStatus`, so creating a Service Request
|
|
69
|
-
does not silently move the ticket to *In Progress*.
|
|
70
|
-
|
|
71
|
-
## Statuscodes: 1705 = success, 11 = failure
|
|
72
|
-
|
|
73
|
-
`ELITE_SERVICE_REQUEST_STATUS__SUCCESS = 1705` is Elite's own success message row;
|
|
74
|
-
`ELITE_SERVICE_REQUEST_STATUS__FAILURE = 11` is the generic "cannot add item" failure, with the
|
|
75
|
-
detail written to the ticket history.
|
|
76
|
-
|
|
77
|
-
**Two paths return 1705 without creating anything, and both are correct:**
|
|
78
|
-
|
|
79
|
-
- the **duplicate guard** — a Service Request already exists for this ticket, so the desired end
|
|
80
|
-
state is already true. A second submit is a **duplicate-safe SUCCESS**, not an error.
|
|
81
|
-
- a **create that succeeded but whose reply write failed** — the `addReply()` call is wrapped in
|
|
82
|
-
try/catch and the exception is deliberately swallowed, so a reply failure can never report a
|
|
83
|
-
successful create as a failure.
|
|
84
|
-
|
|
85
|
-
## One Service Request per ticket (Elite only)
|
|
86
|
-
|
|
87
|
-
Decided 2026-08-11. The application-level duplicate guard is not sufficient on its own — two
|
|
88
|
-
simultaneous submissions can both pass it — so the rule is enforced in the database by a **UNIQUE
|
|
89
|
-
index on `ServiceRequests.ticketId`**:
|
|
90
|
-
`dbchanges2/Client_Elite/2026-08-11a - EliteServiceRequestTicketUnique.sql`.
|
|
91
|
-
|
|
92
|
-
- Applied to **dev-sandbox**; **NOT yet applied to production**.
|
|
93
|
-
- **Elite-only.** Other clients may legitimately raise more than one Service Request per ticket —
|
|
94
|
-
do not promote this index to a shared/Core migration.
|
|
95
|
-
|
|
96
|
-
## Gotchas / known issues
|
|
97
|
-
|
|
98
|
-
- **⚠ The endpoint is hardcoded to PRODUCTION, in TWO constants that must stay in sync.**
|
|
99
|
-
`Ticket::ELITE_SERVICE_REQUEST_CREATE_ENDPOINT` (`class.ticket.php`) and
|
|
100
|
-
`ELITE_SERVICE_REQUEST_LOOKUP_ENDPOINT` (the modal controller) are both
|
|
101
|
-
`https://api.togahub.com/v2`. They **must** match: the modal reads customer / state / item uuids
|
|
102
|
-
from the lookup endpoint and the create posts those same uuids, and **a uuid from one environment
|
|
103
|
-
does not exist in another**. The consequence is deliberate but sharp — **every environment
|
|
104
|
-
(alpha/beta/stage) writes Service Requests into production.** Change one constant and you must
|
|
105
|
-
change the other in the same commit.
|
|
106
|
-
- **A 2.0 list route returns an array; a single-record response returns a bare object.** The modal
|
|
107
|
-
controller normalises this (wraps the object) — new lookups must do the same or they break on the
|
|
108
|
-
one-result case.
|
|
109
|
-
- **`tickets.referenceId` is the bridge to 2.0.** No `referenceId` ⇒ no 2.0 ticket uuid ⇒ the modal
|
|
110
|
-
cannot proceed.
|
|
111
|
-
- **The desk half had never been executed before 2026-08-12.** See the harness below.
|
|
112
|
-
|
|
113
|
-
## Test harness
|
|
114
|
-
|
|
115
|
-
`test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php` (PHP **7.2**)
|
|
116
|
-
exercises the desk half from the CLI: the button gate, `referenceId` resolution, the six modal
|
|
117
|
-
lookups, the duplicate guard, and — under `--run` — a real create with history/reply verification.
|
|
118
|
-
|
|
119
|
-
**It cannot test the permission gate.** `createEliteServiceRequest()` calls `isOwner()`, which reads
|
|
120
|
-
the `$isAdmin` / `$liu` globals and **redirects + exits** when they are absent; the harness seeds
|
|
121
|
-
them, so everything it proves is *after* the permission check. See
|
|
122
|
-
[standalone test-script bootstrap](../../../1.0/apps/togadesk/workflows/standalone-test-scripts.md).
|
|
123
|
-
|
|
124
|
-
## Change history
|
|
125
|
-
|
|
126
|
-
- 2026-08-12 — TRUE-80497: built the Elite "New Service Request" button, modal, action and
|
|
127
|
-
`Ticket::createEliteServiceRequest()`, gated by
|
|
128
|
-
`App_Model_TogaDesk_Ticket::isEliteServiceRequestTicket()` (client 163 + department 299 + custom
|
|
129
|
-
field 88 = `Service Request`) and **replacing** the "New Part Request" button on those tickets.
|
|
130
|
-
Recorded the statuscode semantics (1705 success / 11 failure, with the duplicate guard and a
|
|
131
|
-
swallowed reply failure both returning 1705), the **one-Service-Request-per-ticket** rule enforced
|
|
132
|
-
by a UNIQUE index on `ServiceRequests.ticketId`
|
|
133
|
-
(`dbchanges2/Client_Elite/2026-08-11a`, dev-sandbox only — **not yet in production**), and the
|
|
134
|
-
⚠ hardcoded production endpoint pair that makes every environment write to production. Added the
|
|
135
|
-
PHP 7.2 desk harness (`test/@srija/…/test_elite_desk_service_request.php`) for a path that had
|
|
136
|
-
never been executed. (snaredla)
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Elite — Service Request → Sales Order → status back to TOGa Desk 1.0"
|
|
3
|
-
framework: "2.0"
|
|
4
|
-
repo: worker2
|
|
5
|
-
project: Worker
|
|
6
|
-
client: elite
|
|
7
|
-
type: client-feature
|
|
8
|
-
status: active
|
|
9
|
-
updated: 2026-08-12
|
|
10
|
-
owners: [snaredla]
|
|
11
|
-
files:
|
|
12
|
-
- worker2/Worker/Sync/ServiceRequest.php
|
|
13
|
-
- worker2/Worker/Sync/SalesOrderStatus.php
|
|
14
|
-
- _underscore/Model/Elite/ServiceRequest.php
|
|
15
|
-
- _underscore/Model/Elite/SalesOrder.php
|
|
16
|
-
- test/@srija/Elite Testing/Service Requests/test_elite_chain_toga2.php
|
|
17
|
-
related:
|
|
18
|
-
- ./salesorder-netsuite-push.md
|
|
19
|
-
- ./desk-service-request-creation.md
|
|
20
|
-
- ../../../2.0/apps/api2/features/api-payload-interceptors.md
|
|
21
|
-
- ../../../2.0/apps/worker2/features/creating-worker-actions.md
|
|
22
|
-
- ../../../2.0/apps/worker2/features/netsuite-salesorder-outbound-push.md
|
|
23
|
-
- ../profile.md
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Summary
|
|
27
|
-
|
|
28
|
-
Once a Service Request exists in 2.0 (raised from
|
|
29
|
-
[TOGa Desk](./desk-service-request-creation.md) or in supply2), an interceptor-driven chain turns it
|
|
30
|
-
into a Sales Order and reports the order's status back to the 1.0 desk ticket:
|
|
31
|
-
|
|
32
|
-
1. `_Model_Elite_ServiceRequest::postPost` queues
|
|
33
|
-
**`Sync/ServiceRequest/GenerateSalesOrderFromServiceRequest`** (worker2).
|
|
34
|
-
2. That action generates the Sales Order (and, where vendors are resolvable, purchase orders),
|
|
35
|
-
moving the Service Request to the **Exception** stage when it cannot.
|
|
36
|
-
3. `_Model_Elite_SalesOrder` queues the NetSuite push — a separate feature, see
|
|
37
|
-
[SalesOrder → NetSuite push](./salesorder-netsuite-push.md).
|
|
38
|
-
4. **`Sync/SalesOrderStatus/PostSalesOrderStatusToTogadesk1`** writes the order status back as a
|
|
39
|
-
reply on the originating 1.0 desk ticket, with a dedupe guard so a repeated push does not
|
|
40
|
-
double-post.
|
|
41
|
-
|
|
42
|
-
Every hop is registered data (`ApiPayloadInterceptors`) plus a queued action **string** — which is
|
|
43
|
-
where the two durable hazards below come from.
|
|
44
|
-
|
|
45
|
-
## ⚠ Renaming a worker action is a TWO-REPO, single-deploy change
|
|
46
|
-
|
|
47
|
-
The 2026-08-12 rename `GenerateSalesOrder` → `GenerateSalesOrderFromServiceRequest` touches:
|
|
48
|
-
|
|
49
|
-
- the **method** — `worker2/Worker/Sync/ServiceRequest.php`
|
|
50
|
-
- the **dispatch string** — `\_Worker::runTask(action: 'Sync/ServiceRequest/GenerateSalesOrderFromServiceRequest', …)`
|
|
51
|
-
in `_underscore/Model/Elite/ServiceRequest.php:44`
|
|
52
|
-
|
|
53
|
-
**They must land and deploy together.** If either ships alone, `runTask` dispatches to a name that
|
|
54
|
-
no longer exists and **Sales Order generation stops silently** — `postPost` wraps `runTask` in
|
|
55
|
-
try/catch + `error_log`, so the Service Request saves and nothing else happens, with no user-visible
|
|
56
|
-
error. The same coupling applies to `Sync/SalesOrderStatus/PostSalesOrderStatusToTogadesk1`.
|
|
57
|
-
|
|
58
|
-
Treat an action name like a database column rename: see the multi-producer rename rule in
|
|
59
|
-
[creating worker actions](../../../2.0/apps/worker2/features/creating-worker-actions.md).
|
|
60
|
-
|
|
61
|
-
## The NetSuite actions in this chain
|
|
62
|
-
|
|
63
|
-
`Netsuite/SalesOrder/CreateNetSuite` (new order) and `Netsuite/SalesOrder/UpdateNetSuite` (changed
|
|
64
|
-
order) are the real queued actions. **There is no `Netsuite/SalesOrder/Push`** — do not write that
|
|
65
|
-
string into a migration, a test or a doc.
|
|
66
|
-
|
|
67
|
-
## Verifying the chain — `test_elite_chain_toga2.php`
|
|
68
|
-
|
|
69
|
-
`test/@srija/Elite Testing/Service Requests/test_elite_chain_toga2.php` (PHP 8.1) exists because the
|
|
70
|
-
interceptor layer is a **single point of silent failure**: the Service Request is created, nothing
|
|
71
|
-
downstream happens, and no error is raised anywhere. It asserts, in order:
|
|
72
|
-
|
|
73
|
-
1. **every queued action string resolves to a real `class::method`** (the rename hazard above);
|
|
74
|
-
2. the `_underscore` models queue **those same strings**;
|
|
75
|
-
3. `ApiPayloadInterceptors` rows exist for **record 35 (service-requests)** and **record 14
|
|
76
|
-
(sales-orders)**, and — critically — that each row's **derived** method name
|
|
77
|
-
(`strtolower(prePostProcessing) . ucfirst(strtolower(httpMethod))`) actually **exists on the
|
|
78
|
-
model**. A registered row whose derived method is missing **500s every request on that route**;
|
|
79
|
-
4. Sales Order / Purchase Order / NetSuite / desk-reply state after a run;
|
|
80
|
-
5. a **repeat** status push, to prove the dedupe guard holds.
|
|
81
|
-
|
|
82
|
-
Reusable beyond Elite — the same three checks (string resolves, producer matches, interceptor row +
|
|
83
|
-
derived method exist) catch the entire silent-failure class. Background on the mechanism:
|
|
84
|
-
[API payload interceptors](../../../2.0/apps/api2/features/api-payload-interceptors.md).
|
|
85
|
-
|
|
86
|
-
## Gotchas / known issues
|
|
87
|
-
|
|
88
|
-
- **Duplicated client-context helper, already drifted.** `getClientContextFromCore()` exists in both
|
|
89
|
-
`Worker/Sync/ServiceRequest.php` and `Worker/Sync/SalesOrderStatus.php`, and as
|
|
90
|
-
`getClientContext()` in `Worker/Netsuite/SalesOrder.php`. Only the **ServiceRequest** copy has
|
|
91
|
-
`ORDER BY id ASC` on the `Apis` credential query — the other two pick a **non-deterministic API
|
|
92
|
-
credential row** (Netsuite/SalesOrder.php even documents the flaw in a comment). **Not yet
|
|
93
|
-
extracted**; recorded as a known issue, see the
|
|
94
|
-
[outbound push doc](../../../2.0/apps/worker2/features/netsuite-salesorder-outbound-push.md).
|
|
95
|
-
- **The interceptor registration is per-environment DATA.** "It didn't fire in <env>" is a `SELECT`
|
|
96
|
-
on that client's `ApiPayloadInterceptors`, never a code question.
|
|
97
|
-
|
|
98
|
-
## Change history
|
|
99
|
-
|
|
100
|
-
- 2026-08-12 — TRUE-80497/80501: documented the Elite Service Request → Sales Order → desk-status
|
|
101
|
-
chain. Recorded the **two-repo rename constraint** (the worker method in
|
|
102
|
-
`worker2/Worker/Sync/ServiceRequest.php` and the `runTask` dispatch string in
|
|
103
|
-
`_underscore/Model/Elite/ServiceRequest.php:44` must deploy together or Sales Order generation
|
|
104
|
-
stops silently), that the real NetSuite actions are `CreateNetSuite`/`UpdateNetSuite` (**no
|
|
105
|
-
`Push`**), and the drifted duplicate client-context helper (`ORDER BY id ASC` present in only one
|
|
106
|
-
of three copies → non-deterministic API credential). Added the PHP 8.1 chain harness
|
|
107
|
-
`test_elite_chain_toga2.php`, which verifies action strings resolve, producers match, and each
|
|
108
|
-
interceptor row's **derived** method name exists (a missing one 500s every request on the route).
|
|
109
|
-
(snaredla)
|