toga-ai 1.0.561 → 1.0.562

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ files:
17
17
  - library/app/api/carrier/fedex.php
18
18
  related:
19
19
  - ../architecture.md
20
- - ./service-request-toga2-provisioning.md
20
+ - ../../../clients/elite/features/togadesk-service-request-intake.md
21
21
  ---
22
22
 
23
23
  ## Summary
@@ -23,7 +23,7 @@ related:
23
23
  - ../../worker/features/netsuite-togasupply-per-client-sync.md
24
24
  - ../architecture.md
25
25
  - ./error-capture-1-0.md
26
- - ./service-request-toga2-provisioning.md
26
+ - ../../../clients/elite/features/togadesk-service-request-intake.md
27
27
  ---
28
28
 
29
29
  ## Summary
@@ -91,7 +91,8 @@ belong to the NetSuite importer — documented in the per-client-sync doc, not h
91
91
  > `->data->{resource}` sees an empty result and concludes **"the record does not exist"** — which,
92
92
  > for a duplicate check or a contact lookup, is exactly the wrong conclusion (it creates a second
93
93
  > record). Check `empty($response->isSuccess)` *first*, and keep "unknown" distinct from "absent".
94
- > See [App_Api_ServiceRequest](./service-request-toga2-provisioning.md) for the three bugs this
94
+ > See [Elite Service Request intake](../../../clients/elite/features/togadesk-service-request-intake.md)
95
+ > for the bugs this
95
96
  > caused and the `?bool` tri-state fix.
96
97
 
97
98
  > **`authenticate()` is also 1.0's ambient-client choke point for error reporting** (added
@@ -391,7 +392,8 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
391
392
  returns — including GETs — and a `$throwOnError = false` failure must never be read as "the record
392
393
  does not exist". Also recorded that naming `fields` on `GET /states` **suppresses** the nested
393
394
  country a plain request returns. Surfaced building
394
- [App_Api_ServiceRequest](./service-request-toga2-provisioning.md) (TRUE-80497). (snaredla)
395
+ [App_Api_ServiceRequest](../../../clients/elite/features/togadesk-service-request-intake.md)
396
+ (TRUE-80497). (snaredla)
395
397
  - 2026-08-10 — TRUE-79401 re-synced with `_production` (`c64164fe`, pushed); the sole conflict was a
396
398
  positional const-block collision, resolved by keeping both sides. Patch intact; deploy still
397
399
  unverified. `STARTECH_TOGADESK_CLIENTS` (credential registry, add a row per client onboard) is
@@ -52,7 +52,7 @@ web stack. Example harness: `C:\WWW\test\@Mark\TOGaDeskSupport\test_derive_custo
52
52
  harness then tests everything *after* the permission gate — it does **not** test the gate itself,
53
53
  so a permission regression will pass. Hit while harnessing
54
54
  `Ticket::createEliteServiceRequest()` (see the
55
- [Elite desk Service Request feature](../../../clients/elite/features/desk-service-request-creation.md)).
55
+ [Elite desk Service Request feature](../../../clients/elite/features/togadesk-service-request-intake.md)).
56
56
  - **Lint/run with the right PHP.** Desk + library code targets **7.2**, and the default CLI on a dev
57
57
  box is 8.x — see the
58
58
  [7.2 verification procedure](../../test/features/static-no-db-regression-harness.md).
@@ -3,7 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
6
- | [API Payload Interceptors (metadata-registered prePost/postPut model hooks)](features/api-payload-interceptors.md) | A `_Model`'s `prePost()` / `postPost()` / `prePut()` / `postPut()` hooks are **not** called by the model. | api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderItem.php, dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql |
6
+ | [API Payload Interceptors (metadata-registered prePost/postPut model hooks)](features/api-payload-interceptors.md) | A `_Model`'s `prePost()` / `postPost()` / `prePut()` / `postPut()` hooks are **not** called by the model. | api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderItem.php, dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql |
7
7
  | [Multi-Client (Cross-Client) Data Retrieval](features/cross-client-data-retrieval.md) | A single authenticated V2 GET listing can return records across **many** clients (designed for 1000+) that the caller is entitled to, honoring **each target cli | api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Cache/Table.php, _underscore/Model/Cache/Tables/Client.php, _underscore/Model/Core/Record.php, worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Cache/2026-06-30a - MultiClientCacheTables.sql, dbchanges2/Core/2026-06-30b - CacheClusterRegistrationAndRecordTtl.sql, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
8
8
  | [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
9
9
  | [ENVIRONMENT (not the EB environment name) decides the _underscore branch and Config file](features/environment-variable-drives-underscore-branch.md) | An api2 Elastic Beanstalk instance decides **which `_underscore` branch it clones** and **which `Config/<env>.ini` it loads** from the EB environment property * | api2/.ebextensions/git.php, api2/.ebextensions/php_include_underscore.config, api2/.ebextensions/git.sandbox-dev.json, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, api2/Component/Api/V2/V2.php |
@@ -6,16 +6,18 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-11
10
- owners: ["mhammontree", "dfranks", "bala"]
9
+ updated: 2026-08-12
10
+ owners: ["mhammontree", "dfranks", "bala", "snaredla"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  - api2/Controller/Index.php
14
14
  - _underscore/Model/Client/ItemFulfillment.php
15
15
  - _underscore/Model/Elite/SalesOrder.php
16
+ - _underscore/Model/Elite/ServiceRequest.php
16
17
  - _underscore/Model/Compass/SalesOrder.php
17
18
  - _underscore/Model/Compass/SalesOrderItem.php
18
19
  - dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql
20
+ - dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql
19
21
  related:
20
22
  - ./record-scripts.md
21
23
  - ../architecture.md
@@ -333,6 +335,12 @@ and the failing environment**. It is a small table, and the drift is usually exa
333
335
  change that switches on a *code* path; if the PHP defining the method is not confirmed deployed to
334
336
  that environment, the row takes the endpoint down. Insert inactive, verify the deploy, then flip
335
337
  `isActive = 1` — and remember every environment activates independently.
338
+ - **⚠ A post interceptor's `$payload` is the record, not a route-keyed envelope** — and it is
339
+ by-reference, so pull the record into a local instead of reassigning `$payload`.
340
+ - **⚠ Client behaviour never goes on `_Model_Client_X`.** Every tenant with a row for that record
341
+ inherits it through `parent::` calls. Put it on `_Model_<Client>_X`.
342
+ - **A client's rows may be in `Core` (with `clientId`), not in `Client_<X>`** — check both before
343
+ concluding a client has no interceptors.
336
344
  - **The method name is derived, so a typo'd enum silently misses.** A row with
337
345
  `prePostProcessing = 'PRE'`, `httpMethod = 'PUT'` resolves to `prePut`, not `prePost`; the engine
338
346
  will simply find no method and move on.
@@ -235,7 +235,7 @@ queue failure cannot fail the originating write, so the user's record saves, not
235
235
  happens, and no error surfaces. Worked example: `GenerateSalesOrder` →
236
236
  `GenerateSalesOrderFromServiceRequest` spans `worker2/Worker/Sync/ServiceRequest.php` and the
237
237
  dispatch string in `_underscore/Model/Elite/ServiceRequest.php:44` — see the
238
- [Elite Service Request pipeline](../../../clients/elite/features/service-request-to-salesorder-pipeline.md).
238
+ [Service Request → Sales Order generation](./service-request-sales-order-generation.md).
239
239
 
240
240
  Rules: rename **both sides in one PR and one deploy**; grep every repo for the old string (including
241
241
  `Core.CronJobs.action` and migrations) before merging; and treat it exactly like the
@@ -139,6 +139,41 @@ have it. See the `c_`-column convention in `2.0/standards/framework-rules.md`.
139
139
  - **Placement:** cross-system sync workers live under `worker2/Worker/Sync/`. This worker does
140
140
  **not** belong in `Worker/Netsuite/` (which keeps the NetSuite push,
141
141
  `Worker/Netsuite/SalesOrder.php`) and not in `Worker/Client/`.
142
+ - **⚠ Renaming this action is a TWO-REPO, single-deploy change.** The method name here and the
143
+ `runTask(action: 'Sync/ServiceRequest/GenerateSalesOrderFromServiceRequest', …)` **string** in
144
+ `_underscore/Model/Elite/ServiceRequest.php:44` must land and deploy **together** — the rename
145
+ from `GenerateSalesOrder` on 2026-08-12 spans both. Ship either alone and the dispatcher targets a
146
+ name that no longer exists; because `postPost` wraps `runTask` in try/catch + `error_log`, Sales
147
+ Order generation then stops **silently**. Same exposure for
148
+ `Sync/SalesOrderStatus/PostSalesOrderStatusToTogadesk1`. See
149
+ [creating worker actions](./creating-worker-actions.md).
150
+ - **⚠ The copied client-context helper has already DRIFTED.** `getClientContextFromCore()` here,
151
+ the same private in `Worker/Sync/SalesOrderStatus.php`, and `getClientContext()` in
152
+ `Worker/Netsuite/SalesOrder.php` are three copies — and only **this** one has `ORDER BY id ASC` on
153
+ the `Apis` credential lookup, so the other two pick a **non-deterministic** API credential row
154
+ (`Worker/Netsuite/SalesOrder.php` documents the flaw in a comment). Add the `ORDER BY` to all
155
+ three if you touch any, and prefer extraction.
156
+ - **The NetSuite actions in this chain are `Netsuite/SalesOrder/CreateNetSuite` and
157
+ `Netsuite/SalesOrder/UpdateNetSuite`.** There is **no** `Netsuite/SalesOrder/Push` — do not write
158
+ that string into a migration, a test or a doc.
159
+
160
+ ## Verifying the whole chain — `test_elite_chain_toga2.php`
161
+
162
+ `test/@srija/Elite Testing/Service Requests/test_elite_chain_toga2.php` (PHP 8.1) exists because the
163
+ interceptor layer is a **single point of silent failure**: the Service Request is created, nothing
164
+ downstream happens, and no error is raised anywhere. It asserts, in order:
165
+
166
+ 1. **every queued action string resolves to a real `class::method`** (the rename hazard above);
167
+ 2. the `_underscore` models queue **those same strings**;
168
+ 3. `ApiPayloadInterceptors` rows exist for **record 35 (service-requests)** and **record 14
169
+ (sales-orders)** — and, critically, that each row's **derived** method name
170
+ (`strtolower(prePostProcessing) . ucfirst(strtolower(httpMethod))`) actually **exists on the
171
+ model**, because a registered row whose method is missing **500s every request on that route**;
172
+ 4. Sales Order / Purchase Order / NetSuite / desk-reply state after a run;
173
+ 5. a **repeat** status push, proving the dedupe guard holds.
174
+
175
+ Those three structural checks (string resolves → producer matches → interceptor row + derived
176
+ method exist) are reusable for any client on this chain.
142
177
 
143
178
  ## Known limitations — business blockers, not code defects
144
179
 
@@ -153,6 +188,15 @@ Recorded so nobody re-debugs them as bugs. All four were confirmed against Elite
153
188
 
154
189
  ## Change history
155
190
 
191
+ - 2026-08-12 (later pass) — Recorded three deployment/verification facts: the action rename
192
+ `GenerateSalesOrder` → `GenerateSalesOrderFromServiceRequest` is a **two-repo, single-deploy**
193
+ change (the `runTask` string lives in `_underscore/Model/Elite/ServiceRequest.php:44`, and a
194
+ half-shipped rename stops Sales Order generation silently); the copied client-context helper has
195
+ **drifted** (`ORDER BY id ASC` on the `Apis` lookup exists only in this file, so the other two
196
+ copies pick a non-deterministic credential); and the chain is verified by
197
+ `test_elite_chain_toga2.php`, which checks that every queued action string resolves, producers
198
+ match, and each interceptor row's **derived** method name exists. Also noted the real NetSuite
199
+ action names (no `Netsuite/SalesOrder/Push`). (snaredla)
156
200
  - 2026-08-12 — TRUE-80497/80498: built `_Worker_Sync_ServiceRequest` — a tenant-agnostic
157
201
  `GenerateSalesOrderFromServiceRequest(clientIdentifier, serviceRequestId)` generalizing the
158
202
  Prudential 1.0 cron, with early return when a sales order already exists (idempotent under
@@ -3,10 +3,10 @@
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
5
  | [Elite — \"New Service Request\" button on TOGa Desk 1.0 tickets](features/desk-service-request-creation.md) | 1.0 | TRUE-80497. | togadesk/desk/includes/controllers/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/actions/tickets/serviceRequest.php, togadesk/desk/template/modals/tickets/serviceRequest.php, togadesk/desk/includes/classes/class.ticket.php, library/app/model/togadesk/ticket.php, library/app/api/servicerequest.php, dbchanges2/Client_Elite/2026-08-11a - EliteServiceRequestTicketUnique.sql, test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php |
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, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Client_Elite/2026-08-10 - SalesOrderNetsuiteInterceptors.sql |
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
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
9
  | [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
10
  | [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
- | [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, 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 |
11
+ | [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
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 |
@@ -6,16 +6,20 @@ project: _Underscore
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-10
10
- owners: ["bala"]
9
+ updated: 2026-08-12
10
+ owners: ["bala", "snaredla"]
11
11
  files:
12
12
  - _underscore/Model/Elite/SalesOrder.php
13
+ - _underscore/Model/Elite/ServiceRequest.php
13
14
  - worker2/Worker/Netsuite/SalesOrder.php
14
- - dbchanges2/Client_Elite/2026-08-10 - SalesOrderNetsuiteInterceptors.sql
15
+ - dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql
15
16
  related:
16
17
  - ../../../2.0/apps/worker2/features/netsuite-salesorder-outbound-push.md
18
+ - ../../../2.0/apps/worker2/features/service-request-sales-order-generation.md
17
19
  - ../../../2.0/apps/api2/features/api-payload-interceptors.md
18
20
  - ../../../2.0/apps/_underscore/features/netsuite-rest-client.md
21
+ - ./salesorder-status-togadesk-reply.md
22
+ - ./togadesk-service-request-intake.md
19
23
  - ../profile.md
20
24
  ---
21
25
 
@@ -48,15 +52,51 @@ different shape — a CashSale built inline in `postPost`).
48
52
  5. `runTask` is wrapped in `try/catch` + `error_log`, so a broken queue can never fail the sales
49
53
  order POST itself.
50
54
 
55
+ ## `postPut` also drives the desk reply (2026-08-12)
56
+
57
+ `postPut` now branches on **what was actually submitted** (`$api->httpPayload` holds only the fields
58
+ sent):
59
+
60
+ - **stage changed** (`salesOrderStageId` / `salesOrderStage`) → queues
61
+ `Sync/SalesOrderStatus/PostSalesOrderStatusToTogadesk1`, **first**, because it does not need the
62
+ NetSuite read-back. See [Sales Order stage → desk reply](./salesorder-status-togadesk-reply.md).
63
+ - **shipping method changed** → the existing `Netsuite/SalesOrder/UpdateNetSuite` path, which also
64
+ bails when the order has no `c_netsuiteInternalSalesOrderId` (never sent, so nothing to update).
65
+
66
+ Neither branch replaces the by-reference `$payload`; the record is read into a local, so a
67
+ multi-record response is not truncated.
68
+
51
69
  ## Interceptor registration is per-environment DATA — and prod has none
52
70
 
53
71
  `Client_Elite.ApiPayloadInterceptors` is **EMPTY in production**. Without rows, `postPost`/`postPut`
54
- **never run**: the order saves, nothing else happens, and there is no error anywhere. The
55
- migration `dbchanges2/Client_Elite/2026-08-10 - SalesOrderNetsuiteInterceptors.sql` registers
56
- `POST`/`POST` and `POST`/`PUT` for **`recordId 14` (sales-orders)**, guarded with
57
- `WHERE NOT EXISTS` so a replay cannot create duplicates — **a duplicate row would queue the same
58
- NetSuite job twice for one order.** (Check the file carries dbchanges2's mandatory
59
- `YYYY-MM-DD<letter>` suffix before it merges.)
72
+ **never run**: the order saves, nothing else happens, and there is no error anywhere.
73
+
74
+ > **⚠ CORRECTION (2026-08-12): the registration migration moved.** It is no longer
75
+ > `dbchanges2/Client_Elite/2026-08-10 - SalesOrderNetsuiteInterceptors.sql` (that file no longer
76
+ > exists). The rows now land in **`Core.ApiPayloadInterceptors`** via
77
+ > **`dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql`**,
78
+ > using the *one-client / all-APIs* form: `clientId 41` (Elite), **`apiId NULL`**, `isActive 1`,
79
+ > `minDepth 1`. Three rows: **record 35 (`service-requests`) POST/POST**, **record 14
80
+ > (`sales-orders`) POST/POST**, and **record 14 POST/PUT**.
81
+
82
+ Every row is guarded with `WHERE NOT EXISTS` on `(clientId, recordId, prePostProcessing,
83
+ httpMethod)` so a replay cannot create duplicates — **a duplicate row would queue the same NetSuite
84
+ job twice for one order.**
85
+
86
+ **The record 14 rows fire for EVERY Elite sales order**, not only ones that came from a service
87
+ request — the migration says so explicitly. Any guard that should apply only to service-request
88
+ orders has to live in the PHP.
89
+
90
+ ## Decision — the push lives on Elite models, never on `_Model_Client_SalesOrder`
91
+
92
+ `_Model_Compass_SalesOrder` and `_Model_Quad_SalesOrder` both call `parent::postPost()`, and
93
+ Compass has an **active `recordId 14` interceptor row**. So a NetSuite push placed on the shared
94
+ `_Model_Client_SalesOrder` would have pushed **Compass and Quad** orders into NetSuite as a side
95
+ effect. It is on `_Model_Elite_SalesOrder` / `_Model_Elite_ServiceRequest` for that reason.
96
+
97
+ **Decision: no traits for per-client interceptor behaviour.** A small interceptor is added per
98
+ client as each one needs it. A shared trait `use`d by several client models re-creates the same
99
+ blast radius the client-model split exists to prevent, and hides it one level further down.
60
100
 
61
101
  `recordId 14` and the `Apis` ids (**1 = Agilant, 2 = Elite**) happen to match between dev-sandbox
62
102
  and production — **verify them per environment rather than assuming.**
@@ -107,6 +147,20 @@ precedent**. Treat the first prod Elite order as a real first run.
107
147
 
108
148
  ## Change history
109
149
 
150
+ - 2026-08-12 — TRUE-80498/80501: **corrected the registration migration** — the
151
+ `Client_Elite/2026-08-10 - SalesOrderNetsuiteInterceptors.sql` file no longer exists; the rows now
152
+ land in **`Core.ApiPayloadInterceptors`** via
153
+ `Core/2026-08-06a - Service request and sales order payload interceptors.sql` in the
154
+ one-client/all-APIs form (`clientId 41`, `apiId NULL`, `minDepth 1`), covering **record 35
155
+ service-requests POST/POST** as well as record 14 POST/POST and POST/PUT, and noting that the
156
+ record 14 rows fire for **every** Elite sales order. Extended `postPut` to queue
157
+ `Sync/SalesOrderStatus/PostSalesOrderStatusToTogadesk1` on a **stage** change (ahead of the
158
+ NetSuite read-back it does not need), leaving the shipping-method branch as-is. Recorded the
159
+ **decision** that the push lives on `_Model_Elite_*` and never on the shared
160
+ `_Model_Client_SalesOrder` — Compass and Quad both call `parent::postPost()` and Compass has an
161
+ active record 14 row, so the shared model would have pushed their orders to NetSuite — and the
162
+ companion decision to add **a small interceptor per client rather than a shared trait**.
163
+ (snaredla)
110
164
  - 2026-08-10 — **Built the Elite push trigger**: `_Model_Elite_SalesOrder::postPost` queues
111
165
  `Netsuite/SalesOrder/CreateNetSuite`, `postPut` queues `UpdateNetSuite` on a shipping-method
112
166
  change; both call `parent::postPost()` first, bail when the payload carries
@@ -10,17 +10,21 @@ updated: 2026-08-12
10
10
  owners: ["snaredla"]
11
11
  files:
12
12
  - library/app/api/servicerequest.php
13
+ - library/app/model/togadesk/ticket.php
13
14
  - togadesk/desk/includes/classes/class.ticket.php
14
15
  - togadesk/desk/template/modals/tickets/serviceRequest.php
15
16
  - togadesk/desk/includes/controllers/modals/tickets/serviceRequest.php
16
17
  - togadesk/desk/includes/controllers/actions/tickets/serviceRequest.php
17
18
  - dbchanges2/Client_Elite/2026-08-11a - EliteServiceRequestTicketUnique.sql
19
+ - test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php
18
20
  related:
19
21
  - ../profile.md
20
22
  - ./salesorder-netsuite-push.md
21
23
  - ../../../2.0/apps/worker2/features/service-request-sales-order-generation.md
22
24
  - ../../../1.0/apps/library/features/toga2-api-client-and-bridge.md
23
25
  - ../../../1.0/apps/togadesk/features/ticket-lifecycle.md
26
+ - ../../../1.0/apps/togadesk/workflows/standalone-test-scripts.md
27
+ - ../../../1.0/apps/library/features/app-class-placement-base-contracts.md
24
28
  ---
25
29
 
26
30
  ## Summary
@@ -37,6 +41,26 @@ only the **desk gate** (is this an Elite service-request ticket?) and the **tick
37
41
  reporting**. Commit `e2b03fa7` moved ~235 lines out of `class.ticket.php` to get there — record the
38
42
  library file as the home; `class.ticket.php` is now the caller, not the implementation.
39
43
 
44
+ ## The button and its gate — three conditions, one helper
45
+
46
+ On a qualifying ticket the **"New Service Request"** button **replaces** the existing **"New Part
47
+ Request"** button; it is not an extra button, and every other Elite department keeps the parts flow.
48
+
49
+ `App_Model_TogaDesk_Ticket::isEliteServiceRequestTicket(int $clientId, int $departmentId, ?string $ticketTypeValue): bool`
50
+ (`library/app/model/togadesk/ticket.php`) is the single source of truth — `true` only when **all
51
+ three** hold:
52
+
53
+ | Condition | Constant |
54
+ |---|---|
55
+ | `tickets.clientid` = 163 (Elite) | `TOGADESK_CLIENT_ID__ELITE` |
56
+ | `tickets.departmentid` = 299 (Elite Helpdesk - Contact Center) | `TOGADESK_DEPARTMENT_ID__ELITE_CONTACT_CENTER` |
57
+ | custom field 88 "Ticket Type" = `Service Request` | `TOGADESK_CUSTOM_FIELD_ID__ELITE_TICKET_TYPE` + `ELITE_TICKET_TYPE__SERVICE_REQUEST` |
58
+
59
+ The template, the modal controller **and** `Ticket::createEliteServiceRequest()` all call the helper
60
+ rather than repeating the three ids — re-checked server-side, because a hidden button is not a
61
+ security control. The ticket's 2.0 uuid comes from **`tickets.referenceId`**; without one the modal
62
+ cannot proceed.
63
+
40
64
  ## How it works
41
65
 
42
66
  1. The modal (`desk/template/modals/tickets/serviceRequest.php`) collects the requested-for person,
@@ -51,7 +75,23 @@ library file as the home; `class.ticket.php` is now the caller, not the implemen
51
75
  5. `App_Api_ServiceRequest::createServiceRequestInToga2(...)` posts the **parent and its units in
52
76
  one request**. Related records resolve by **natural key** (type name, part number, email) —
53
77
  only the customer is referenced by uuid.
54
- 6. The desk writes a `Service Request created <number>` history entry with the unit count.
78
+ 6. The desk writes a `Service Request created <number>` history entry with the unit count, and adds
79
+ a reply authored by `ELITE_DEFAULT_AGENT = 35876` (Agilant Helpdesk / serviceops@togatech.com —
80
+ the author of the other machine-written replies on Elite tickets). The reply passes the ticket's
81
+ **current** status as `newStatus`, so raising a Service Request does not silently move the ticket
82
+ to *In Progress*.
83
+
84
+ ## Statuscodes: 1705 = success, 11 = failure
85
+
86
+ `Ticket::ELITE_SERVICE_REQUEST_STATUS__SUCCESS = 1705` is Elite's own success message row;
87
+ `ELITE_SERVICE_REQUEST_STATUS__FAILURE = 11` is the generic "cannot add item" failure, with the
88
+ detail written to the ticket history. **Two paths return 1705 without creating anything, and both
89
+ are deliberate:**
90
+
91
+ - the **duplicate guard** — the desired end state (one Service Request on this ticket) already
92
+ holds, so a second submit is duplicate-safe SUCCESS, not an error;
93
+ - a **create that succeeded but whose reply write failed** — `addReply()` is wrapped in try/catch
94
+ and the exception is swallowed, so a reply failure can never report a successful create as failed.
55
95
 
56
96
  ## The duplicate guard must filter IN the query
57
97
 
@@ -114,8 +154,43 @@ wrong-country address.
114
154
  select2 state list kept emptying the dropdown — the file's own comment records this. The
115
155
  server-side pair check is the guard; the modal stays as-is.
116
156
 
157
+ ## Test harness
158
+
159
+ `test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php` (PHP **7.2**)
160
+ exercises the desk half from the CLI — a path that had **never been executed** before 2026-08-12:
161
+ the button gate, `referenceId` resolution, the six modal lookups, the duplicate guard, and under
162
+ `--run` a real create with history/reply verification.
163
+
164
+ **It cannot test the permission gate.** `createEliteServiceRequest()` calls `isOwner()`, which reads
165
+ the `$isAdmin` / `$liu` globals and **redirects + exits** when they are absent; the harness seeds
166
+ them, so it proves everything *after* the permission check, not the check itself. See
167
+ [standalone test-script bootstrap](../../../1.0/apps/togadesk/workflows/standalone-test-scripts.md)
168
+ — and note that verifying 7.2 compatibility needs a real 7.2 binary, since the default CLI is 8.x
169
+ ([procedure](../../../1.0/apps/test/features/static-no-db-regression-harness.md)).
170
+
117
171
  ## Gotchas / known issues
118
172
 
173
+ - **⚠ The endpoint is hardcoded to PRODUCTION, in TWO constants that must stay in sync.**
174
+ `Ticket::ELITE_SERVICE_REQUEST_CREATE_ENDPOINT` (`class.ticket.php`) and
175
+ `ELITE_SERVICE_REQUEST_LOOKUP_ENDPOINT` (the modal controller) are both
176
+ `https://api.togahub.com/v2`. They **must** match: the modal reads customer / state / item uuids
177
+ from the lookup endpoint and the create posts those same uuids, and **a uuid from one environment
178
+ does not exist in another**. Consequence, deliberate but sharp: **every environment
179
+ (alpha/beta/stage) writes Service Requests into production.** Change one constant and you must
180
+ change the other in the same commit.
181
+ - **⚠ A failed contact lookup used to create a DUPLICATE contact in 2.0.**
182
+ `findContactUuidByEmailAddress()` returned `null` on an API failure, which the caller read as "no
183
+ such contact" and created one — for someone 2.0 already held. Same root cause as the duplicate-SR
184
+ bug above: with throwing off, a failure arrives as a *response*. `empty($response->isSuccess)` is
185
+ a safe check on **anything** `App_Api_Toga2::send()` returns, GETs included — see
186
+ [the transport doc](../../../1.0/apps/library/features/toga2-api-client-and-bridge.md).
187
+ - **`States.countryId` has `isIdentifier = 0`**, so a nested country cannot disambiguate a state
188
+ either — the state **uuid** is the only country-safe handle when posting.
189
+ - **A 2.0 list route returns an array; a single-record response returns a bare object.** The modal
190
+ controller normalises this — new lookups must do the same or they break on the one-result case.
191
+ - **`App_Api_ServiceRequest` extends nothing, deliberately.** In `library/app/`, the folder is a
192
+ behavioural contract and `App_Api` is the fulfilment-vendor base — see
193
+ [where a new App_ class goes](../../../1.0/apps/library/features/app-class-placement-base-contracts.md).
119
194
  - **⚠ Do not re-implement this in `class.ticket.php`.** After `e2b03fa7` that file holds the desk
120
195
  gate and history reporting only. New validation or payload work belongs in
121
196
  `library/app/api/servicerequest.php`.
@@ -129,6 +204,16 @@ server-side pair check is the guard; the modal stays as-is.
129
204
 
130
205
  ## Change history
131
206
 
207
+ - 2026-08-12 (later pass) — Recorded the **button gate** itself
208
+ (`isEliteServiceRequestTicket()`: client 163 + department 299 + custom field 88 = `Service
209
+ Request`; the button **replaces** "New Part Request" on those tickets, and the 2.0 uuid comes from
210
+ `tickets.referenceId`), the **statuscode semantics** (1705 success / 11 failure, with both the
211
+ duplicate guard and a swallowed reply failure returning 1705), the reply author
212
+ `ELITE_DEFAULT_AGENT = 35876` and its status-preserving `newStatus`, the ⚠ **hardcoded production
213
+ endpoint pair** that makes every environment write to production, the **duplicate-contact** bug
214
+ from reading past a failed lookup (`empty($response->isSuccess)` is the safe check on anything
215
+ `send()` returns), `States.countryId isIdentifier = 0`, and the PHP 7.2 desk harness plus its
216
+ `isOwner()` limitation. (snaredla)
132
217
  - 2026-08-12 — TRUE-80497: moved the Elite service-request intake out of
133
218
  `togadesk` `class.ticket.php` into `library/app/api/servicerequest.php`
134
219
  (`App_Api_ServiceRequest`, commit `e2b03fa7`, ~235 lines); `class.ticket.php` keeps only the desk
@@ -30,8 +30,9 @@ related:
30
30
  - features/supply2-scope.md
31
31
  - features/supply2-tableview-config-drift.md
32
32
  - features/salesorder-netsuite-push.md
33
- - features/desk-service-request-creation.md
34
- - features/service-request-to-salesorder-pipeline.md
33
+ - features/togadesk-service-request-intake.md
34
+ - features/salesorder-status-togadesk-reply.md
35
+ - 2.0/apps/worker2/features/service-request-sales-order-generation.md
35
36
  ---
36
37
 
37
38
  ## Summary
@@ -60,19 +61,20 @@ renders a designed "no order details" empty state (see supply2-scope).
60
61
  Center tickets (client 163 / department 299 / Ticket Type = *Service Request*) show a **New Service
61
62
  Request** button that replaces *New Part Request* and creates the Service Request in TOGa 2.0 via
62
63
  the shared `App_Api_ServiceRequest` — see
63
- [desk-service-request-creation](features/desk-service-request-creation.md). The 2.0 side then
64
+ [togadesk-service-request-intake](features/togadesk-service-request-intake.md). The 2.0 side then
64
65
  generates a Sales Order and posts the order status back onto the desk ticket —
65
- [service-request-to-salesorder-pipeline](features/service-request-to-salesorder-pipeline.md). This
66
+ [service-request-sales-order-generation](../../2.0/apps/worker2/features/service-request-sales-order-generation.md). This
66
67
  adds **`togadesk`** (and **`test`**, for the two CLI harnesses) to Elite's app scope. **One Service
67
68
  Request per ticket** is enforced by a UNIQUE index on `ServiceRequests.ticketId`
68
69
  (`dbchanges2/Client_Elite/2026-08-11a`) — applied on dev-sandbox, **not yet in production** —
69
70
  and the desk endpoints are currently **hardcoded to production**, so every environment writes there.
70
71
 
71
72
  **Elite orders are pushed to NetSuite event-driven (2026-08).** `_Model_Elite_SalesOrder`'s
72
- `postPost`/`postPut` interceptors queue the shared worker2 push. Elite's
73
- `ApiPayloadInterceptors` table is **empty in production**, so the registration migration is the
74
- go-live gate — see [salesorder-netsuite-push](features/salesorder-netsuite-push.md). This work adds
75
- **`_underscore`** and **`api2`** to Elite's app scope.
73
+ `postPost`/`postPut` interceptors queue the shared worker2 push. `Client_Elite.ApiPayloadInterceptors`
74
+ is **empty in production**; the registration rows now live in **`Core.ApiPayloadInterceptors`**
75
+ (`dbchanges2/Core/2026-08-06a`, `clientId 41` / `apiId NULL`, records 35 and 14) and landing them is
76
+ the go-live gate — see [salesorder-netsuite-push](features/salesorder-netsuite-push.md). This work
77
+ adds **`_underscore`** and **`api2`** to Elite's app scope.
76
78
 
77
79
  **Tenant/test-account notes.** `Core.Domains` rows for Elite already exist in **every**
78
80
  environment (dev `http://elite.togasupply`, beta, production, sandbox-dev) with no dbchanges2 Core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.561",
3
+ "version": "1.0.562",
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",