toga-ai 1.0.297 → 1.0.299
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/togadesk/INDEX.md +1 -1
- package/knowledge/1.0/apps/togadesk/features/email-to-ticket-intake.md +10 -2
- package/knowledge/1.0/apps/togadesk/features/ticket-lifecycle.md +48 -2
- package/knowledge/clients/nycdoe/features/servicenow-integration.md +58 -3
- package/package.json +1 -1
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
| [Ticket Email Notifications (notifications table)](features/notifications.md) | Which TOGa Desk emails fire for a given client is driven **entirely by data**, not code: the `TOGaDeskSupport.notifications` table holds one row per `(clientid, | desk/includes/classes/class.notification.php, crons/tickets.php, crons/tickets_prod.php |
|
|
9
9
|
| [REST API (RPC-over-POST) & API-Key Auth](features/rest-api.md) | TOGa Desk exposes a programmatic API at `desk/api/`. | desk/api/index.php, desk/api/resources/tickets.php, desk/api/resources/assets.php, desk/api/resources/authenticate.php, desk/includes/classes/class.apikey.php, desk/includes/functions.php |
|
|
10
10
|
| [SMB Contract Editing & the clientMspId Corruption Trap](features/smb-contract-editing.md) | The SMB contracts page (`/desk/?route=toga/smbcontracts&togaClientId=<id>`) edits `TOGA_*.SMBContracts` rows via a modal. | desk/template/modals/toga/smbcontracts/smbContract.php, desk/includes/controllers/modals/toga/smbcontracts/smbContract.php, desk/includes/controllers/actions/toga/smbcontracts/smbContract.php, desk/includes/controllers/actions/toga/smbcontracts/edit.php |
|
|
11
|
-
| [Ticket Lifecycle (class.ticket.php)](features/ticket-lifecycle.md) | All TOGa Desk ticket creation and reply handling funnels through `Ticket` in `desk/includes/classes/class.ticket.php`. | desk/includes/classes/class.ticket.php, desk/includes/controllers/actions.php, desk/api/resources/tickets.php, crons/tickets.php, desk/includes/controllers/actions/tickets/merge.php |
|
|
11
|
+
| [Ticket Lifecycle (class.ticket.php)](features/ticket-lifecycle.md) | All TOGa Desk ticket creation and reply handling funnels through `Ticket` in `desk/includes/classes/class.ticket.php`. | desk/includes/classes/class.ticket.php, desk/includes/controllers/actions.php, desk/includes/controllers/actions/tickets/addReply.php, desk/api/resources/tickets.php, desk/api/resources/ticket_replies.php, crons/tickets.php, crons/pipe.php, desk/includes/controllers/actions/tickets/merge.php |
|
|
12
12
|
| [Standalone PHP Test Script Bootstrap (TOGa Desk)](workflows/standalone-test-scripts.md) | How to write a standalone CLI PHP script that bootstraps the TOGa Desk framework for read-only testing of desk classes (e.g. | |
|
|
@@ -6,8 +6,8 @@ project: TOGa Desk
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-07-09
|
|
10
|
+
owners: ["jcardinal", "dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- crons/tickets.php
|
|
13
13
|
- crons/tickets_prod.php
|
|
@@ -61,6 +61,14 @@ Writes `tickets`, `tickets_replies`, attachment `files`; reads config from the `
|
|
|
61
61
|
- **Auto-reply filters** are partly hardcoded (e.g. ignore `TechHub@compass-usa.com`
|
|
62
62
|
"Automatic reply:" messages).
|
|
63
63
|
- Attachments are deleted locally immediately after the S3 upload with **no retry** if S3 fails.
|
|
64
|
+
- **`adminid` clobber on reply routing:** after `emailToTicket()` sets `$data['adminid']` from
|
|
65
|
+
the FROM address (~line 1407), an asset lookup (~line 1437-1442) overwrites it with the
|
|
66
|
+
replying user's asset's assigned admin. On a matched-ticket reply this makes a user's email
|
|
67
|
+
look like a staff reply in `addReply()` (`isAdminReply=true`), so `Awaiting User` etc. never
|
|
68
|
+
transition. The asset-derived adminid is for NEW-ticket auto-assign in `add()`, not reply
|
|
69
|
+
classification. See the ticket-lifecycle doc.
|
|
64
70
|
|
|
65
71
|
## Change history
|
|
72
|
+
- 2026-07-09 — documented the `emailToTicket()` adminid-overwrite reply misclassification
|
|
73
|
+
(TRUE-80114 planning investigation) (dfranks)
|
|
66
74
|
- 2026-06-15 — documented from a source read of `crons/tickets*.php` and `emailToTicket()` (jcardinal)
|
|
@@ -6,13 +6,16 @@ project: TOGa Desk
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["mhammontree"]
|
|
9
|
+
updated: 2026-07-09
|
|
10
|
+
owners: ["mhammontree", "dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- desk/includes/classes/class.ticket.php
|
|
13
13
|
- desk/includes/controllers/actions.php
|
|
14
|
+
- desk/includes/controllers/actions/tickets/addReply.php
|
|
14
15
|
- desk/api/resources/tickets.php
|
|
16
|
+
- desk/api/resources/ticket_replies.php
|
|
15
17
|
- crons/tickets.php
|
|
18
|
+
- crons/pipe.php
|
|
16
19
|
- desk/includes/controllers/actions/tickets/merge.php
|
|
17
20
|
related:
|
|
18
21
|
- 1.0/apps/togadesk/features/smb-contract-editing.md
|
|
@@ -40,6 +43,26 @@ Creation paths — all funnel through `Ticket::add($data)`:
|
|
|
40
43
|
- Ticket merge (`actions/tickets/merge.php`) inserts a master ticket directly — it copies
|
|
41
44
|
fields from the child, including `customerid` (since June 2026).
|
|
42
45
|
|
|
46
|
+
### Reply entry points (FOUR doors — only two transition status)
|
|
47
|
+
There are **four** inbound reply paths, but the status-transition switch lives ONLY inside
|
|
48
|
+
`Ticket::addReply()`. Any door that does not route through `addReply()` will not change status:
|
|
49
|
+
- **IMAP-poll cron** — `crons/tickets.php` → `emailToTicket()` → `addReply()`. ✅ transitions.
|
|
50
|
+
- **Mail pipe** — `crons/pipe.php:113` → `emailToTicket()` (legacy 6-arg call) → `addReply()`.
|
|
51
|
+
✅ transitions.
|
|
52
|
+
- **Desk REST API** — `desk/api/resources/ticket_replies.php` `'add'` → `addReply($data)`
|
|
53
|
+
directly. ✅ transitions.
|
|
54
|
+
- **togaview customer portal** — the portal reply form (`action="/ticket"` + `newReplyBtn`,
|
|
55
|
+
from the generic `common/togaview/ticket.php` and client variants e.g.
|
|
56
|
+
`common/newcenturyholdingsllc/ticket.php`, `common/towfoundation/ticket.php`) POSTs to
|
|
57
|
+
`togaview/mvc/ticket/post.php` `newReplyBtn` handler (~line 388). This handler inserts the
|
|
58
|
+
reply row **directly** via `App_Model_TogaDesk_TicketReply` and **bypasses the `Ticket` class
|
|
59
|
+
entirely** — it never updates `tickets.status`, `tickets_replies.newStatus`, or
|
|
60
|
+
`tickets_history`. ❌ **no status transition.** This is the gap behind "a reply from outside
|
|
61
|
+
Desk doesn't move the ticket."
|
|
62
|
+
|
|
63
|
+
The in-Desk agent reply path is `desk/includes/controllers/actions/tickets/addReply.php` →
|
|
64
|
+
`Ticket::addReply($_POST)`.
|
|
65
|
+
|
|
43
66
|
## How it works
|
|
44
67
|
|
|
45
68
|
### `Ticket::deriveCustomerId(int, string): ?int` (added June 2026)
|
|
@@ -64,6 +87,11 @@ means the reply lands but the ticket status silently never changes (no history e
|
|
|
64
87
|
re-entry). That was the On Hold SLA bug; `case 'On Hold'` (admin ⇒ stays On Hold, client ⇒
|
|
65
88
|
Open) was added June 2026.
|
|
66
89
|
|
|
90
|
+
- **`origin` is the reliable "reply came from outside Desk" signal.** The in-app agent reply
|
|
91
|
+
path (`addReply.php` → `addReply($_POST)`) carries **no `origin` key**, whereas
|
|
92
|
+
`emailToTicket()` sets `tickets.origin = 'EMAIL'` and togaview sets `'TOGAVIEW'` on new
|
|
93
|
+
tickets. Absence of `origin` on a reply therefore identifies an in-app agent reply; presence
|
|
94
|
+
identifies an external reply. This is the safe hook for any "external reply → status" logic.
|
|
67
95
|
- Special case: an admin replying to their OWN ticket is treated as a user reply.
|
|
68
96
|
- Auto-assign on first staff reply requires the role perm `allowAutoAssign` via
|
|
69
97
|
profiles/profile_departments.
|
|
@@ -88,11 +116,29 @@ backfill (e.g. craftex:
|
|
|
88
116
|
column (close time would have to come from `tickets_replies.newStatus='Closed'` or
|
|
89
117
|
`tickets_history`). Implementing it is a product decision: what happens to a lapsed reply
|
|
90
118
|
(attach-but-closed / new ticket / bounce)?
|
|
119
|
+
- **`emailToTicket()` clobbers `adminid`, misclassifying user email replies as staff replies.**
|
|
120
|
+
After matching the sender and setting `$data['adminid']` from the FROM address (~line 1407),
|
|
121
|
+
an asset lookup (~line 1437-1442) **overwrites** `$data['adminid']` with the replying user's
|
|
122
|
+
asset's assigned admin. When the matched ticket goes to `addReply()`, the user's reply is
|
|
123
|
+
read as an admin/staff reply (`isAdminReply=true`), so states like `Awaiting User` stay put
|
|
124
|
+
instead of transitioning to `Open`. The asset-derived adminid is meant for auto-assigning
|
|
125
|
+
NEW tickets in `add()`, not for classifying replies — `adminid` serves two conflicting
|
|
126
|
+
purposes here.
|
|
127
|
+
- **Cross-app DB coupling:** togadesk and togaview share the `TOGaDeskSupport` database
|
|
128
|
+
(`tickets`, `tickets_replies`, `tickets_history`, `people`), so a reply/status behavior
|
|
129
|
+
change on either side is visible to both immediately. togaview cannot load Desk's
|
|
130
|
+
`class.ticket.php`, so its writes go through the `App_Model_TogaDesk_*` models
|
|
131
|
+
(`TicketReply`, `TicketsHistory`, `Ticket`) — any shared status logic must account for that
|
|
132
|
+
the portal path never instantiates `Ticket`.
|
|
91
133
|
- Legacy `togadesk/includes/class.ticket.php` has active email-reopen logic; the live
|
|
92
134
|
`desk/includes` version has it commented out — email replies never set status to "Reopened"
|
|
93
135
|
via `emailToTicket()`.
|
|
94
136
|
|
|
95
137
|
## Change history
|
|
138
|
+
- 2026-07-09 — mapped the four reply entry points (togaview portal bypasses `addReply()` so
|
|
139
|
+
never transitions status); documented `emailToTicket()` adminid-overwrite reply
|
|
140
|
+
misclassification, the `origin`-absence agent-reply signal, and togadesk↔togaview shared-DB
|
|
141
|
+
coupling — from TRUE-80114 planning source investigation (dfranks)
|
|
96
142
|
- 2026-06-12 — documented from craftex MSP portal debugging session (mhammontree)
|
|
97
143
|
- 2026-06 — added `deriveCustomerId()`; added `On Hold` case to addReply() switch; merge
|
|
98
144
|
copies `customerid` (mhammontree)
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: nycdoe
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [mhammontree]
|
|
9
|
+
updated: 2026-07-09
|
|
10
|
+
owners: [mhammontree, sking]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/sync/nycdoe/import_asn.php
|
|
13
13
|
- worker/crons/sync/nycdoe/import_inc.php
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
- worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php
|
|
18
18
|
- worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php
|
|
19
19
|
- worker/crons/sync/nycdoe/3_create_installation_ticket.php
|
|
20
|
+
- worker/crons/sync/nycdoe/test_multi_po_receipt_resolution.php
|
|
20
21
|
- worker/crons/sync/nycdoe/send_ticket_updates.php
|
|
21
22
|
- worker/crons/sync/nycdoe/send_request_item_updates.php
|
|
22
23
|
- worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php
|
|
@@ -34,6 +35,7 @@ files:
|
|
|
34
35
|
- library/app/asnprocessor/lexmark.php
|
|
35
36
|
- library/app/asnprocessor/acer.php
|
|
36
37
|
- library/app/edi.php
|
|
38
|
+
- library/app/netsuite.php
|
|
37
39
|
related:
|
|
38
40
|
- ../profile.md
|
|
39
41
|
- hold-status-sync.md
|
|
@@ -131,12 +133,27 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
|
|
|
131
133
|
`qtyOrder` is immutable** — there is no update path to a sent SO line; growth must be a
|
|
132
134
|
new item row → new SO line.
|
|
133
135
|
- **Stage 4:** reconciles the NetSuite **PO** (`netSuiteInternalPurchaseOrderId` — not the
|
|
134
|
-
SO id) and sends serial inventory assignments.
|
|
136
|
+
SO id) and sends serial inventory assignments. It is already multi-PO aware
|
|
137
|
+
(`listPurchaseOrdersCreatedFromSalesOrder`, groups items by PO, stamps each item's
|
|
138
|
+
`netSuiteInternalPurchaseOrderId`) **but only stamps items where the PO is NULL and never
|
|
139
|
+
re-stamps** — so an item stamped to PO-A but later *received* on PO-B is never corrected.
|
|
140
|
+
**TOGA does NOT create NetSuite POs anywhere in the DOE flow.** Stage 3 creates only the SO
|
|
141
|
+
(and, when needed, a special-order inventory Item); POs are generated inside NetSuite by
|
|
142
|
+
procurement/special-order. A single SO can therefore be fulfilled across **multiple POs**
|
|
143
|
+
(back-orders, multi-vendor, re-cut lines) — this is a legitimate NetSuite outcome, so the
|
|
144
|
+
durable fix lives downstream (resolve receipts SO-wide), not in preventing PO splits.
|
|
135
145
|
- **Stage 5:** for units `WHERE togadeskRepairOrderId IS NULL`, reads serials from NetSuite
|
|
136
146
|
item receipts (so TOGa Desk RO serials are **driven by NetSuite**, not the ASN unit),
|
|
137
147
|
creates/reuses TOGa Desk manufacturers/models/assets/serial-numbers/MSO and creates **a
|
|
138
148
|
new repair_orders row unconditionally per loop iteration** — one RO per Unit (a 50-cable
|
|
139
149
|
line = 1 unit = 1 RO). Existing-serial lookup is scoped **per item row** (`$asnItemId`).
|
|
150
|
+
Item receipts are now resolved **Sales-Order-wide** via
|
|
151
|
+
`App_NetSuite::listItemReceiptsCreatedFromSalesOrder($soId)` (fans out across ALL POs from
|
|
152
|
+
`listPurchaseOrdersCreatedFromSalesOrder`, dedupes receipts by `internalId`), cached once
|
|
153
|
+
per ASN, with `getItemReceipt` cached per receipt id. The inner item query also requires
|
|
154
|
+
`netSuiteInternalSalesOrderId IS NOT NULL` so a null-SO first row cannot poison the per-ASN
|
|
155
|
+
receipt cache. Self-healing: units previously stuck under an unstamped PO clear on
|
|
156
|
+
subsequent 5-min cron runs.
|
|
140
157
|
- **"#/N received" UI** (`togadesk/desk/template/pages/managedserviceorders/view.php`):
|
|
141
158
|
denominator = `SUM(qtyOrder)` per part (committed + unsent); numerator = Units with
|
|
142
159
|
`togadeskRepairOrderId IS NOT NULL`.
|
|
@@ -218,6 +235,36 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
|
|
|
218
235
|
- **Stage 6 is not currently running** (`4_` paused with `active: 0`, `5_` unscheduled).
|
|
219
236
|
- TRUE-77354: Stage 5 lookups changed `LIKE` → `=` to stop `_`/`%` wildcard matches in
|
|
220
237
|
serials.
|
|
238
|
+
- **Multi-PO-per-SO silently drops install tickets (FIXED 2026-07-09).** Stage 5 used to
|
|
239
|
+
fetch item receipts only for the single stamped `netSuiteInternalPurchaseOrderId`. When a
|
|
240
|
+
NetSuite SO is fulfilled across more than one PO, units received under a PO *other* than the
|
|
241
|
+
stamped one never got a TogaDesk repair order, and the every-5-min cron silently re-queried
|
|
242
|
+
the wrong PO forever. No error was raised — the symptom was serials never syncing. Fixed by
|
|
243
|
+
resolving receipts SO-wide (see Stage 5). Before, no `App_NetSuite` method listed receipts
|
|
244
|
+
by SO — only by PO (`listItemReceiptsCreateFromPurchaseOrder`) or location
|
|
245
|
+
(`listAllItemReceiptsForLocation`); the new `listItemReceiptsCreatedFromSalesOrder`
|
|
246
|
+
(`library/app/netsuite.php`, after `listItemReceiptsCreateFromPurchaseOrder`) fills that gap.
|
|
247
|
+
Concrete case: customer PO `S202643596` (ASN 26339, SO internalId 6971858) split across PO
|
|
248
|
+
164785 (internalId 6971859 → `38S0500` synced, 15 ROs) and PO 165751 (`50M7280` + `A3L980`
|
|
249
|
+
NOT synced). PRs: library#831 (merge first) then worker#1670.
|
|
250
|
+
|
|
251
|
+
## Debugging & DB topology (DOE 1.0 sync)
|
|
252
|
+
|
|
253
|
+
Reference for future DOE investigations — the 1.0 worker cannot run on a dev/Windows box;
|
|
254
|
+
use the toga DB MCP + `Logs.API` instead of running prod code locally.
|
|
255
|
+
|
|
256
|
+
- **`db_core` alias → legacy (V1) environment, schema `Core`** — holds the FLAT tables
|
|
257
|
+
`AdvanceShippingNotices` / `AdvanceShippingNoticeItems` / `AdvanceShippingNoticeUnits`
|
|
258
|
+
(columns incl. `togadeskRepairOrderId`, `serialNumber`, `netSuiteInternalSalesOrderId`,
|
|
259
|
+
`netSuiteInternalPurchaseOrderId`, `customerPurchaseOrder`). The 1.0 cron reads **legacy/Core**.
|
|
260
|
+
- **Do NOT confuse with prod (V2) `Client_Nycdoe`** — a DIFFERENT, normalized schema
|
|
261
|
+
(`AdvanceShippingNoticeItemUnits` with a `unitId` FK). The 1.0 cron does not read V2.
|
|
262
|
+
- **NetSuite SOAP request/response payloads** are logged in legacy `Logs.API` — filter
|
|
263
|
+
`sourceJob LIKE '%3_create_installation_ticket%'`; `requestPayload` contains the PO
|
|
264
|
+
internalId. (The multi-PO case above was verified this way: the receipt search for PO
|
|
265
|
+
6971859 returned `totalRecords=1`, only `38S0500`.)
|
|
266
|
+
- **`Bridge_NetSuite`** (legacy) has `SalesOrders` / `SalesOrderItems` / `SerialNumbers` but
|
|
267
|
+
**no `PurchaseOrders` table** — POs are pulled live from the NetSuite API.
|
|
221
268
|
|
|
222
269
|
## Operating rules when changing this integration
|
|
223
270
|
|
|
@@ -231,6 +278,14 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
|
|
|
231
278
|
of the consumer query; `php -l` every touched file.
|
|
232
279
|
|
|
233
280
|
## Change history
|
|
281
|
+
- 2026-07-09 — Fixed silent install-ticket drop when a NetSuite SO is fulfilled across
|
|
282
|
+
multiple POs: Stage 5 now resolves item receipts Sales-Order-wide via new helper
|
|
283
|
+
`App_NetSuite::listItemReceiptsCreatedFromSalesOrder` (fans across all POs from the SO,
|
|
284
|
+
dedupes by internalId, cached per ASN), and its item query requires
|
|
285
|
+
`netSuiteInternalSalesOrderId IS NOT NULL`. Root cause: TOGA doesn't create DOE POs, so
|
|
286
|
+
multi-PO-per-SO is legitimate and units received on an unstamped PO were never ticketed.
|
|
287
|
+
Added multi-PO notes to Stage 4/5, a gotcha, and a DB-topology/debugging section.
|
|
288
|
+
PRs library#831 + worker#1670. (sking)
|
|
234
289
|
- 2026-06-29 — Outbound status sync now keeps DOE holds authoritative (TRUE-79922). Hold
|
|
235
290
|
detection moved to the SNOW native `state` field (not `u_status_task`); Assigned-force
|
|
236
291
|
blocks guarded against local `HOLD_*` to stop the `state:3`↔`state:2` self-clobber; RITM
|
package/package.json
CHANGED