toga-ai 1.0.561 → 1.0.563
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/library/features/app-class-placement-base-contracts.md +1 -1
- package/knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md +5 -3
- package/knowledge/1.0/apps/togadesk/workflows/standalone-test-scripts.md +1 -1
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/api-payload-interceptors.md +10 -2
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +1 -1
- package/knowledge/2.0/apps/worker2/features/elite-freshservice-sync.md +18 -2
- package/knowledge/2.0/apps/worker2/features/service-request-sales-order-generation.md +44 -0
- package/knowledge/INDEX.md +3 -3
- package/knowledge/clients/elite/INDEX.md +2 -4
- package/knowledge/clients/elite/features/salesorder-netsuite-push.md +63 -9
- package/knowledge/clients/elite/features/togadesk-service-request-intake.md +86 -1
- package/knowledge/clients/elite/profile.md +10 -8
- 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 |
|
|
@@ -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
|
-
-
|
|
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 [
|
|
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](
|
|
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/
|
|
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-
|
|
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
|
-
[
|
|
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,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)
|
|
@@ -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
|
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)_ —
|
|
7
|
+
- **library** (Library) _(framework core)_ — 17 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
8
|
- **worker** (Worker) — 21 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,9 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
-
| [Elite
|
|
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 |
|
|
5
|
+
| [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
6
|
| [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
7
|
| [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
8
|
| [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 |
|
|
9
|
+
| [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
10
|
| [Elite](profile.md) | 2.0 | Elite is a managed-services client that uses **Freshservice** as their helpdesk platform. | worker2/Worker/Elite.php, worker2/Worker/Sync/ServiceRequest.php, library/app/api/toga2.php, library/app/api/servicerequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, togadesk/desk/includes/classes/class.ticket.php |
|
|
@@ -6,16 +6,20 @@ project: _Underscore
|
|
|
6
6
|
client: elite
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
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/
|
|
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.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`
|
|
58
|
-
|
|
59
|
-
|
|
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/
|
|
34
|
-
- features/
|
|
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
|
-
[
|
|
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-
|
|
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.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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,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)
|