toga-ai 1.0.281 → 1.0.282

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.
@@ -57,6 +57,28 @@ the originating page. Status changes are audited through `RepairHistory` (`repai
57
57
  onsite/install started → review → closed; depot repairs add parts/tracking steps. Bulk actions
58
58
  exist for closing installs and editing onsite tickets.
59
59
 
60
+ ### Status is a denormalized cache (`repair_orders.status`)
61
+ This is **shared, client-agnostic** behavior — every repair-order client is subject to it.
62
+ - Status transitions are **recorded on `repair_order_notes.newstatus`**, not written to
63
+ `repair_orders.status` directly. `Repair::addNotes`, `Repair::changeStatus`, and
64
+ `Repair::reworkRepair` (`class.repair.php`) each only create a note — none writes the
65
+ status column itself.
66
+ - **`repair_orders.status` is a denormalized CACHE**, reconciled from the latest note **only
67
+ when** `App_Model_TogaDesk_RepairOrder::updateStatus()` runs (fired by the central actions
68
+ `scheduleDispatch`, `technicianOnsite`, `technicianEnRoute`, `awaitingDispatch`,
69
+ `reworkAwaitingScheduling`, and the part flows). `updateStatus()` writes via a raw `UPDATE`
70
+ with no history row (see gotchas).
71
+ - **The LIVE status the UI shows is DERIVED, not the cache:** the grid/detail SELECTs use
72
+ `qqStatus() AS status` (`repairorder.php` ~L356), recomputing status from the latest note on
73
+ read. So the UI can show a status the cache column does not currently hold.
74
+ - **Consequence:** any consumer that reads the cached `repair_orders.status` (e.g. the worker
75
+ sync crons) diverges from the derived UI status whenever a note changed status but no
76
+ `updateStatus()` ran afterward — the consumer then acts on a **stale** status. This is
77
+ exactly how a DOE hold set via a comment failed to sync (see
78
+ [NYCDOE Hold-Status Sync](../../../../clients/nycdoe/features/hold-status-sync.md)).
79
+ - `updateStatus()`'s notification block is **enqueue-only** — it inserts dedup'd
80
+ `repair_order_notifications` rows; it sends no inline email.
81
+
60
82
  ## UPS shipment / carrier-account billing (Prepare Shipment flow)
61
83
  The "PREPARE SHIPMENT" button opens modal `desk/template/modals/central/addTracking.php`,
62
84
  whose carrier/account dropdown is built from the constant
@@ -126,6 +148,12 @@ always confirm the client arm before changing shared central logic.
126
148
  account-location map noted under *UPS shipment / carrier-account billing*.
127
149
 
128
150
  ## Change history
151
+ - 2026-07-07 — TRUE-80060 (togadesk portion): documented the durable, client-agnostic
152
+ status-cache model — status transitions land on `repair_order_notes.newstatus`;
153
+ `repair_orders.status` is a denormalized cache reconciled only by `updateStatus()`; the UI
154
+ shows the derived `qqStatus() AS status`, so cache-reading consumers (e.g. worker crons) can
155
+ act on a stale status when a note changed status without a following `updateStatus()`. Noted
156
+ `updateStatus()`'s notification block is enqueue-only. (mhammontree)
129
157
  - 2026-07-07 — TRUE-80060: documented the shared `qqStatus()`/`updateStatus()` status-recompute
130
158
  hold gates (in `library/app/model/togadesk/repairorder.php`) — silent raw-`UPDATE` overwrite,
131
159
  hold states must be gated or dropped, the six-hold-variant collapse to base `STATUS_HOLD`, and
@@ -2,6 +2,6 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
- | [NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)](features/hold-status-sync.md) | 1.0 | DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and **stay held** — holds are SLA-bearing in both systems. | worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/send_request_item_updates.php, library/app/model/togadesk/repairorder.php, library/app/api/nycdoev2.php |
5
+ | [NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)](features/hold-status-sync.md) | 1.0 | DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and **stay held** — holds are SLA-bearing in both systems. | worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/send_request_item_updates.php, library/app/model/togadesk/repairorder.php, library/app/api/nycdoev2.php, togadesk/desk/includes/classes/class.repair.php |
6
6
  | [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/asnprocessor/manufacturer.php, library/app/asnprocessor/apple.php, library/app/asnprocessor/lenovo.php, library/app/asnprocessor/lexmark.php, library/app/asnprocessor/acer.php, library/app/edi.php |
7
7
  | [New York City Department of Education](profile.md) | 1.0 | NYC DOE (New York City Department of Education) is a TOGA client whose entire integration runs in the **1.0 worker tier** (~30 cron scripts under `worker/crons/ | |
@@ -14,6 +14,7 @@ files:
14
14
  - worker/crons/sync/nycdoe/send_request_item_updates.php
15
15
  - library/app/model/togadesk/repairorder.php
16
16
  - library/app/api/nycdoev2.php
17
+ - togadesk/desk/includes/classes/class.repair.php
17
18
  related:
18
19
  - servicenow-integration.md
19
20
  - ../profile.md
@@ -25,14 +26,14 @@ related:
25
26
  DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and
26
27
  **stay held** — holds are SLA-bearing in both systems. This doc covers the bidirectional
27
28
  hold-status sync for DOE Incidents (INC) and Request Items (RITM), the authoritative
28
- business rule that governs it, and the two independent defects that let holds silently
29
- revert. It is the status-sync companion to the broader
29
+ business rule that governs it, and the three independent defects that let holds silently
30
+ revert or never reach ServiceNow. It is the status-sync companion to the broader
30
31
  [ServiceNow / ASN integration](servicenow-integration.md) doc; the underlying crons and
31
32
  `App_Api_NYCDOEV2` plumbing are documented there.
32
33
 
33
- > **Two distinct hold-revert paths — different repos, mechanisms, and revert targets.**
34
- > A held DOE order could be knocked out of hold by *either* of two unrelated code paths;
35
- > both had to be closed:
34
+ > **Three distinct hold-failure paths — different repos, mechanisms, and symptoms.**
35
+ > A held DOE order could fail to persist/propagate via *any* of three unrelated code paths;
36
+ > all three had to be closed (see *Coordinated three-repo fix & deploy ordering* below):
36
37
  > - **TRUE-79922 (worker crons):** the SNOW-side `state` 3⇄2 oscillation — an outbound
37
38
  > self-clobber pushing `state:2` over a held `state:3`, plus an inbound wrong-field read
38
39
  > — reverted the order to **"In Progress"**. Fixed; see *Change history* + the sections
@@ -43,6 +44,14 @@ revert. It is the status-sync companion to the broader
43
44
  > *Local status recompute* below. This defect is in a **shared, client-agnostic** model
44
45
  > method — surfaced by DOE but affecting every TogaDesk repair-order client; the durable
45
46
  > mechanism lives in [Field-Service Dispatch](../../../1.0/apps/togadesk/features/field-service-dispatch.md).
47
+ > - **TRUE-80060 (`togadesk` desk app):** a hold set **via a note/comment**
48
+ > (`Repair::addNotes`) never reached ServiceNow at all. `addNotes` recorded the new status
49
+ > only on `repair_order_notes.newstatus` — never writing the `repair_orders.status` cache
50
+ > nor calling `updateStatus()` — and the TRUE-75199 real-time-sync optimization suppressed
51
+ > `repair_orders.dtUpdated`. The worker reads the **cached** `repair_orders.status` and
52
+ > selects orders by `dtUpdated >= dtSynced`, so an on-hold-with-comment order left a stale
53
+ > non-hold cache **and** an un-bumped `dtUpdated` — the worker never selected it and never
54
+ > pushed `state:3`. Fixed; see *Hold set via a note/comment* below.
46
55
 
47
56
  > DOE "tickets" are **`repair_orders`** rows (`App_Model_TogaDesk_RepairOrder`,
48
57
  > `db_togadesk` / legacy `TOGaDeskSupport`, **clientid = 16**) — NOT the generic `tickets`
@@ -117,6 +126,41 @@ revert. It is the status-sync companion to the broader
117
126
  - The revert is **silent** — `updateStatus()` writes via a raw `UPDATE` with **no**
118
127
  `repair_order_history` / `repair_order_notes` entry.
119
128
 
129
+ ### Hold set via a note/comment (`addNotes`) — TRUE-80060 (togadesk)
130
+ - In TOGaDesk a status change carried on a comment goes through `Repair::addNotes`
131
+ (`togadesk/desk/includes/classes/class.repair.php`), which records the new status on
132
+ `repair_order_notes.newstatus`. Because `repair_orders.status` is a denormalized cache
133
+ reconciled only by `updateStatus()` (see
134
+ [Field-Service Dispatch](../../../1.0/apps/togadesk/features/field-service-dispatch.md)),
135
+ a hold set this way did **not** update the cache the worker reads.
136
+ - Compounding it, the **TRUE-75199** real-time comment-sync optimization suppressed
137
+ `repair_orders.dtUpdated`, so the order also failed the worker's `dtUpdated >= dtSynced`
138
+ selection — the hold never even entered the outbound cron.
139
+ - **Fix (scoped to `clientid == 16`):** (a) suppress `dtUpdated` only for a **pure comment**
140
+ (`newstatus == oldStatus`); keep it bumped when the status changed, so the worker selects
141
+ the order — the worker's work-note query filters `referenceId IS NULL`, so a
142
+ real-time-reconciled comment is not re-sent; (b) after a **status-changing** note, call
143
+ `App_Model_TogaDesk_RepairOrder::updateStatus()` to reconcile `repair_orders.status` from
144
+ the latest note. This reconcile depends on the library TRUE-80060 `qqStatus()` fix — see
145
+ deploy ordering below.
146
+
147
+ ## Coordinated three-repo fix & deploy ordering (TRUE-79922 / TRUE-80060)
148
+ Making DOE holds persist took a change in **three repos**, with a **deploy-order dependency**:
149
+ - **worker — TRUE-79922:** stop the SNOW `state` 3⇄2 oscillation (outbound self-clobber +
150
+ inbound wrong-field read). *(failure path #1 above)*
151
+ - **library — TRUE-80060:** `qqStatus()` recognizes all six `HOLD_*` on recompute so
152
+ `updateStatus()` can't demote a hold. *(failure path #2 above)*
153
+ - **togadesk — TRUE-80060:** `addNotes` reconciles the cache + keeps `dtUpdated` so a
154
+ note-set hold reaches the worker. *(failure path #3 above)*
155
+
156
+ > **DEPLOY ORDER: `library` must ship WITH or BEFORE `togadesk`.** The togadesk `addNotes`
157
+ > change calls `updateStatus()`, which relies on the library `qqStatus()` fix. Ship togadesk
158
+ > first and `updateStatus()` recomputes a base hold **out of** hold — reintroducing failure
159
+ > path #2 on every note-set hold.
160
+
161
+ The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets`) is
162
+ **intentionally kept** as a failover for SNOW API unreliability — not removed by this work.
163
+
120
164
  ## Reference facts (DOE sync debugging)
121
165
 
122
166
  - **SNOW request/response logs:** logged to the legacy `Logs` schema, **`API` table**
@@ -146,21 +190,42 @@ revert. It is the status-sync companion to the broader
146
190
  - **Diagnosing which writer dropped a hold:** `repair_order_history`
147
191
  (`repairid, userid, dtStamp, note`) records *user-driven* status changes. A hold reverting
148
192
  with **no** history row between the hold and the revert = a raw-`UPDATE` writer
149
- (`updateStatus`/`qqStatus`), **not** the note/reply path (`class.repair.php::addNotes` syncs
150
- replies to SNOW but never writes `repair_orders.status`) and **not** the worker inbound cron
151
- (`process_tickets.php` contains no reference to `ORDER_ASSIGNED_AWAITING_SCHEDULING`). This
152
- is how TRUE-80060 was pinned to `qqStatus()` rather than the SNOW round-trip.
193
+ (`updateStatus`/`qqStatus`), **not** the worker inbound cron (`process_tickets.php` contains
194
+ no reference to `ORDER_ASSIGNED_AWAITING_SCHEDULING`). Note that **since TRUE-80060** a
195
+ *status-changing* `class.repair.php::addNotes` now **calls** `updateStatus()` (so it too can
196
+ move the cache via that raw `UPDATE`); a **pure comment** still writes nothing to
197
+ `repair_orders.status`. This history-gap technique is how TRUE-80060 was pinned to
198
+ `qqStatus()` rather than the SNOW round-trip.
153
199
  - **KNOWN REMAINING GAP (needs a follow-up ticket):** both `qqStatus()` hold gates live inside
154
200
  the `type IN (ONSITE_REPAIR, ONSITE_SERVICE)` branch. The `DEPOT_REPAIR` branch
155
201
  (`repairorder.php` from ~L799) has **no hold gate at all**, so a `DEPOT_REPAIR` order set to
156
202
  any hold status is still silently recomputed out of hold by `updateStatus()` — the same
157
203
  class of bug, untouched by TRUE-80060.
204
+ - **KNOWN REMAINING GAP (needs a follow-up ticket) — inbound note reconciliation is paged too
205
+ small.** `process_tickets.php`'s inbound comment reconciliation fetches only `page_size=50,
206
+ page_number=1` of the SNOW journal, then **deletes any local note not in that set** (the
207
+ remainder-delete ~L1326–1330). An order with **>50** comments/work_notes can have local
208
+ notes silently deleted. It is also inconsistent with `addNotes`, which fetches
209
+ `page_size=100`.
210
+ - **KNOWN REMAINING GAP — `buildDoubleFieldArray` collapses unsynced notes.** In
211
+ `process_tickets`, `buildDoubleFieldArray` keys on `IFNULL(referenceId,0)`, so **all**
212
+ NULL-`referenceId` notes collapse to key `'0'`, making the remainder-delete handle multiple
213
+ unsynced notes inconsistently.
158
214
  - `qqStatus()` collapses all recognized hold sub-statuses to base `HOLD` in
159
215
  `repair_orders.status`; the specific hold **sub-reason persists only in
160
216
  `repair_order_notes.newstatus`**. Any status badge / report / filter reading
161
217
  `repair_orders.status` directly cannot distinguish the six hold variants after a recompute.
162
218
 
163
219
  ## Change history
220
+ - 2026-07-07 — TRUE-80060 (togadesk portion): closed the **third** DOE hold-failure path — a
221
+ hold set via a note/comment (`Repair::addNotes`) never reaching SNOW. `addNotes` recorded
222
+ status only on `repair_order_notes.newstatus` (never the `repair_orders.status` cache the
223
+ worker reads), and the TRUE-75199 optimization suppressed `dtUpdated`, so the worker's
224
+ `dtUpdated >= dtSynced` selection skipped it. Fix (clientid 16): suppress `dtUpdated` only
225
+ for a pure comment; call `updateStatus()` after a status-changing note. Documented the
226
+ coordinated three-repo fix + `library`-before-`togadesk` deploy order, and two new
227
+ `process_tickets` reconciliation gaps (page_size=50 remainder-delete; `IFNULL(referenceId,0)`
228
+ collapse). php -l clean; php-reviewer 0 blocking. (mhammontree)
164
229
  - 2026-07-07 — TRUE-80060: closed the **second** DOE hold-revert path — the local
165
230
  `qqStatus()` recompute (in the shared model `library/app/model/togadesk/repairorder.php`)
166
231
  silently demoting held orders to `ORDER_ASSIGNED_AWAITING_SCHEDULING` whenever
@@ -4,11 +4,12 @@ framework: "1.0"
4
4
  apps:
5
5
  - worker
6
6
  - library
7
+ - togadesk
7
8
  project: Worker
8
9
  client: nycdoe
9
10
  type: profile
10
11
  status: active
11
- updated: 2026-06-10
12
+ updated: 2026-07-07
12
13
  owners: [mhammontree]
13
14
  files: []
14
15
  related:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.281",
3
+ "version": "1.0.282",
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",