toga-ai 1.0.562 → 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.
@@ -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 |
@@ -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 vendor targets PHP >= 8.5. Local PHP 8.1 requires a
143
- temporary bypass in `vendor/composer/platform_check.php` — revert before committing.
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)
@@ -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)_ — 18 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
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) — 13 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
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) — 49 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
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,10 +2,8 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
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
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
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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.562",
3
+ "version": "1.0.563",
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",
@@ -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)