toga-ai 1.0.625 → 1.0.626
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.
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: nycdoe
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-20
|
|
10
10
|
owners: [mhammontree, sking]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/sync/nycdoe/send_ticket_updates.php
|
|
@@ -76,48 +76,87 @@ revert or never reach ServiceNow. It is the status-sync companion to the broader
|
|
|
76
76
|
- `Hold` has **statusRank 6**, beating `Work in Progress` (rank 5) in the downgrade-guard
|
|
77
77
|
added by TRUE-76812 — so a downstream sync cannot silently demote a held order.
|
|
78
78
|
|
|
79
|
-
### Status fields (ServiceNow side) — CORRECTED 2026-
|
|
80
|
-
> **
|
|
81
|
-
>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- The native **`state`** field is
|
|
91
|
-
|
|
92
|
-
only permits the native On Hold transition
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
### Status fields (ServiceNow side) — CORRECTED 2026-08-20 (VENDOR-STATED, TRUE-81044)
|
|
80
|
+
> **This is the third answer to "which field is authoritative" and the first one that is not an
|
|
81
|
+
> inference.** TRUE-79922 concluded native `state` only; the 2026-07-20 pass reversed that to
|
|
82
|
+
> `u_status_task`; TRUE-81044 re-reverses it. Both earlier conclusions were **inferred from
|
|
83
|
+
> observed rejections**. This one is **stated directly by ServiceNow**, so prefer it.
|
|
84
|
+
|
|
85
|
+
**ServiceNow's stated rule:** to put a DOE Incident On Hold, the mandatory attribute set is
|
|
86
|
+
**`state` + `hold_reason` + `u_eta`, submitted TOGETHER in one payload.** A ServiceNow-side
|
|
87
|
+
**business rule derives the client-facing `u_status_task` FROM `state`** — so we must *not* write
|
|
88
|
+
`u_status_task` ourselves. Doing so was fighting their derivation rule.
|
|
89
|
+
|
|
90
|
+
- The native **`state`** field is what we drive. It is the SLA-bearing state machine *and*, via
|
|
91
|
+
their business rule, the source of the client-facing status. Push it with its two companions.
|
|
92
|
+
- ⚠ **SUPERSEDED:** the claim that ServiceNow "only permits the native On Hold transition FROM
|
|
93
|
+
In Progress (`state=2`)" was **inferred from empty-body 200 rejections and is now doubtful** —
|
|
94
|
+
an *incomplete mandatory attribute set* produces the identical silent rejection, and our
|
|
95
|
+
payload was in fact incomplete (see the empty-body gotcha). **The In-Progress gate has been
|
|
96
|
+
removed** (TRUE-81044). ServiceNow explicitly invited us to report back if holds still fail
|
|
97
|
+
from `Assigned`.
|
|
98
|
+
- **`hold_reason`** is mandatory. ⚠ **SUPERSEDED:** the claim that `hold_reason=10` "is not a valid
|
|
99
|
+
choice on this instance and is stored empty/dropped" was **WRONG** — code **10 ("Other") IS
|
|
100
|
+
valid**, but it additionally **requires the companion free-text field
|
|
101
|
+
`u_other_reason_on_hold`**. We were sending `10` with no companion — an incomplete mandatory set
|
|
102
|
+
— which is why it read as dropped.
|
|
103
|
+
- **`u_other_reason_on_hold`** (string) — required whenever `hold_reason=10`. Must never be empty.
|
|
104
|
+
- **`u_eta`** is mandatory on every hold push, **unconditionally** — not "only when it differs from
|
|
105
|
+
the remote value" (the old conditional silently dropped a mandatory attribute on any re-push with
|
|
106
|
+
an unchanged or null ETA).
|
|
107
|
+
- **`u_status_task` (custom) is the CLIENT-FACING status, but is now READ-ONLY to us** — DOE's own
|
|
108
|
+
ServiceNow views/reporting read it (`Open`, `In Progress`, `On Hold`, …) and their business rule
|
|
109
|
+
populates it from `state`. ⚠ **SUPERSEDED:** the 2026-07-20 instruction that "a hold MUST be
|
|
110
|
+
written here or the client never sees it" no longer holds — we removed both the hold and the
|
|
111
|
+
release writes. It remains the field **three code paths still READ** for hold detection (see the
|
|
112
|
+
gotcha on that outstanding dependency).
|
|
96
113
|
- Numeric `state` codes seen on the wire: INC `On Hold`=**3**, `In Progress`=**2**;
|
|
97
|
-
RITM `On Hold`=**8**, the scheduled-update state=**"-14"**.
|
|
114
|
+
RITM `On Hold`=**8**, the scheduled-update state=**"-14"**. `hold_reason` `Other`=**10**.
|
|
98
115
|
|
|
99
|
-
### Outbound (TOGaDesk → ServiceNow) — INC hold push (
|
|
100
|
-
When a DOE repair order is on hold, `send_ticket_updates.php` pushes
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
- **
|
|
116
|
+
### Outbound (TOGaDesk → ServiceNow) — INC hold push (TRUE-81044 current shape)
|
|
117
|
+
When a DOE repair order is on hold, `send_ticket_updates.php` pushes **the full mandatory set
|
|
118
|
+
together, unconditionally**:
|
|
119
|
+
- **`state=3` + `hold_reason=10` + `u_eta` + `u_other_reason_on_hold` in ONE payload, every run.**
|
|
120
|
+
No gate on the remote state, and no "only if the ETA changed" condition — either would drop a
|
|
121
|
+
mandatory attribute and earn a silent empty-body 200.
|
|
122
|
+
- **`u_other_reason_on_hold`** is populated from the analyst's hold note already being read from
|
|
123
|
+
`repair_order_notes` for `work_notes`, falling back to
|
|
124
|
+
`App_Model_TogaDesk_RepairOrder::statusDisplay($repairOrder->status)` so it is **never empty**.
|
|
125
|
+
- **`hold_reason` stays pinned to `SN_HOLD_REASON_DEFAULT` (10, "Other").** See the *decision* on
|
|
126
|
+
not mapping hold reasons, below.
|
|
127
|
+
- **No `u_status_task` write** on either the hold push or the hold release — ServiceNow's own
|
|
128
|
+
business rule derives it from `state`. The `SN_TASK_STATUS_ON_HOLD` / `SN_TASK_STATUS_IN_PROGRESS`
|
|
129
|
+
constants and the `$snTaskStatus` read were removed with it.
|
|
130
|
+
- **Idempotency + release both key off `state`, not `u_status_task`:** the "already held" guard is
|
|
131
|
+
`$snState != 'On Hold'` and the release trigger is `$snState == 'On Hold'`. **Rule: gate a push on
|
|
132
|
+
the field you actually write** — the old `$snTaskStatus` keys gated the push on a field we no
|
|
133
|
+
longer drive.
|
|
134
|
+
- **Closed-incident reconcile (unchanged, still correct):** if the incident is already
|
|
135
|
+
`Resolved`/`Closed`/`Canceled` on the
|
|
111
136
|
SNOW side it can no longer be held — the cron **advances `dtSynced`** to accept the remote
|
|
112
137
|
closure instead of retrying forever. (A canceled incident, INC2190846, had generated **2,392**
|
|
113
138
|
failed empty-body PATCHes before this.)
|
|
114
139
|
- The "Assigned → In Progress" auto-start block is still guarded so it never pushes `state:2` over
|
|
115
140
|
a held order (TRUE-79922). Named constants: `SN_STATE_IN_PROGRESS` / `SN_STATE_ON_HOLD` /
|
|
116
|
-
`SN_HOLD_REASON_DEFAULT` in `send_ticket_updates.php
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
`SN_HOLD_REASON_DEFAULT` in `send_ticket_updates.php`.
|
|
142
|
+
- The existing **empty-body patch-response check** (~L493) still **throws**, so any remaining
|
|
143
|
+
rejection surfaces loudly instead of silently. Keep it.
|
|
144
|
+
|
|
145
|
+
> **DECISION (TRUE-81044) — do NOT map TogaDesk hold reasons onto ServiceNow hold-reason codes.**
|
|
146
|
+
> `hold_reason` stays pinned to `10` ("Other") and the human-readable reason is carried entirely in
|
|
147
|
+
> `u_other_reason_on_hold` / `work_notes`, sourced from TogaDesk. Rationale: ServiceNow's own states
|
|
148
|
+
> are not reliable, so mapping our six `HOLD_*` constants onto their code set buys nothing and adds
|
|
149
|
+
> a second source of truth. **Do not re-propose a reason-code mapping in a future session.**
|
|
150
|
+
|
|
151
|
+
### RITM cron — deliberately unchanged by TRUE-81044
|
|
152
|
+
`send_request_item_updates.php` already pushes native `state:"8"` (On Hold) and **never wrote**
|
|
153
|
+
`u_status_task`, so TRUE-81044's ask was already satisfied there. It carries a defensive `HOLD_*`
|
|
154
|
+
guard on the scheduled-update path so it can't push `state:"-14"` while held; its reverse-hold logic
|
|
155
|
+
still **reads** `u_status_task == 'On Hold'`.
|
|
156
|
+
> **OPEN QUESTION:** ServiceNow's communication was scoped to **Incident** state. We did **not**
|
|
157
|
+
> extend `hold_reason` / `u_eta` / `u_other_reason_on_hold` to the RITM payload without
|
|
158
|
+
> confirmation. Whether the RITM On Hold transition carries the same mandatory-set requirement is
|
|
159
|
+
> unconfirmed — ask before changing the RITM payload.
|
|
121
160
|
|
|
122
161
|
### Inbound (ServiceNow → TOGaDesk)
|
|
123
162
|
- `process_tickets.php` reads the client-facing **`u_status_task`** field
|
|
@@ -125,6 +164,8 @@ When a DOE repair order is on hold, `send_ticket_updates.php` pushes:
|
|
|
125
164
|
bare **`STATUS_HOLD`** on the local repair order (this is exactly why the library `qqStatus()`
|
|
126
165
|
fix below had to recognize bare `HOLD`). Both inbound "Assigned-force" `patchINC(state:2)` blocks
|
|
127
166
|
are guarded against a local hold.
|
|
167
|
+
- **Since TRUE-81044 this read is a DEPENDENCY on ServiceNow's own business rule**, not on anything
|
|
168
|
+
we write — the outbound no longer sets `u_status_task`. See the outstanding-dependency gotcha.
|
|
128
169
|
|
|
129
170
|
### RITM "ETA-in-hold"
|
|
130
171
|
- `send_request_item_updates.php` syncs `scheduled_date` (ETA) to ServiceNow **inside the
|
|
@@ -208,19 +249,32 @@ The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets
|
|
|
208
249
|
auto-start fought the hold push — `state:3` then `state:2` pushed seconds apart, every run,
|
|
209
250
|
so SNOW never stayed On Hold. Any future auto-start/auto-status block on these crons MUST
|
|
210
251
|
short-circuit when the local RO is in a `HOLD_*` status.
|
|
211
|
-
- **⚠
|
|
212
|
-
|
|
213
|
-
`
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
-
|
|
218
|
-
(both hit `/api/nycd3/data/tasks/{number}`) return
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
252
|
+
- **⚠ The "which field is authoritative" question has flipped TWICE. Read this before changing it a
|
|
253
|
+
fourth time.** TRUE-79922 said native `state` only (inferred). 2026-07-20 reversed it to
|
|
254
|
+
`u_status_task` (inferred from test PATCHes). **TRUE-81044 re-reversed it on ServiceNow's own
|
|
255
|
+
written instruction:** we write `state` + `hold_reason` + `u_eta` (+ `u_other_reason_on_hold`) and
|
|
256
|
+
**never write `u_status_task`**, because their business rule derives it from `state`. Only the
|
|
257
|
+
current answer is vendor-stated; do not re-litigate it from observed behavior alone.
|
|
258
|
+
- **⚠ An empty-body 200 does NOT tell you WHY (nycd3 `data/tasks` PATCH).**
|
|
259
|
+
`App_Api_NYCDOEV2::patchINC`/`patchRITM` (both hit `/api/nycd3/data/tasks/{number}`) return
|
|
260
|
+
**HTTP 200 with an EMPTY body (no `result` object)** on rejection — no exception, no error field.
|
|
261
|
+
**Critically, this response is IDENTICAL for an illegal state transition and for an INCOMPLETE
|
|
262
|
+
mandatory attribute set.** The previously documented rule "On Hold is only permitted FROM In
|
|
263
|
+
Progress" was *inferred* from this response and is now doubtful: our payload was genuinely
|
|
264
|
+
incomplete in two ways (`u_eta` sent only when it differed from the remote value, so any re-push
|
|
265
|
+
with an unchanged or null ETA dropped a mandatory attribute; and `u_other_reason_on_hold` never
|
|
266
|
+
sent at all alongside `hold_reason=10`). **Diagnostic rule for this integration: never infer a
|
|
267
|
+
state-machine rule from an empty-body 200 until you have verified payload completeness first.**
|
|
268
|
+
The In-Progress gate was removed on that basis. Separately, a canceled incident (INC2190846)
|
|
269
|
+
generated **2,392** such failed PATCHes before the closed-incident `dtSynced` reconcile was added.
|
|
270
|
+
- **⚠ OUTSTANDING DEPENDENCY (open, NOT closed by TRUE-81044) — we stopped WRITING `u_status_task`
|
|
271
|
+
but three paths still READ it.** Hold detection and RITM hold release now depend on ServiceNow's
|
|
272
|
+
business rule actually populating that field from `state`. If it doesn't, these break **silently**:
|
|
273
|
+
- `worker/crons/sync/nycdoe/process_tickets.php:206` — inbound hold detection
|
|
274
|
+
- `worker/crons/sync/nycdoe/send_request_item_updates.php:224` — RITM reverse-hold
|
|
275
|
+
- `worker/crons/sync/nycdoe/send_request_item_updates.php:139` — RITM closed-state check
|
|
276
|
+
**Verification owed on the FIRST hold that syncs after deploy:** read the incident back and confirm
|
|
277
|
+
`u_status_task` returns `On Hold`, via legacy `Logs.API` filtered `endpoint LIKE '%nycd3%'`.
|
|
224
278
|
- Holds are **never auto-released** — if a SNOW user manually changes status, the outbound
|
|
225
279
|
cron correctly re-asserts On Hold; that is intended, not a bug. Clear the hold in TOGaDesk.
|
|
226
280
|
- **Diagnosing which writer dropped a hold:** `repair_order_history`
|
|
@@ -253,6 +307,26 @@ The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets
|
|
|
253
307
|
`repair_orders.status` directly cannot distinguish the six hold variants after a recompute.
|
|
254
308
|
|
|
255
309
|
## Change history
|
|
310
|
+
- 2026-08-20 — **TRUE-81044: re-corrected the field-authority model on ServiceNow's own written
|
|
311
|
+
instruction, and fixed the real root cause of the failing INC hold push.** ServiceNow stated the
|
|
312
|
+
mandatory attribute set for an Incident On Hold is **`state` + `hold_reason` + `u_eta` together**,
|
|
313
|
+
and that their business rule derives the client-facing `u_status_task` **from `state`** — so our
|
|
314
|
+
`u_status_task` writes were fighting it. `send_ticket_updates.php`: removed `u_status_task` from
|
|
315
|
+
both the hold push and the release (and the `SN_TASK_STATUS_*` constants / `$snTaskStatus` read);
|
|
316
|
+
now sends `state`+`hold_reason`+`u_eta` **unconditionally together** (the `In Progress` gate and
|
|
317
|
+
the "only if ETA differs" condition are **gone**); added `u_other_reason_on_hold`, required
|
|
318
|
+
alongside `hold_reason=10` ("Other" IS valid — the earlier "invalid/dropped" note was wrong) and
|
|
319
|
+
sourced from the analyst hold note with a `statusDisplay()` fallback so it is never empty;
|
|
320
|
+
re-keyed the idempotency guard and release trigger from `$snTaskStatus` to `$snState`. Root-cause
|
|
321
|
+
correction: the documented "On Hold only from In Progress" rule was **inferred** from empty-body
|
|
322
|
+
200s, which do not distinguish an illegal transition from an **incomplete mandatory set** — ours
|
|
323
|
+
was incomplete. Decision: **no** mapping of the six TogaDesk `HOLD_*` constants onto ServiceNow
|
|
324
|
+
hold-reason codes; reason text comes from TogaDesk only. RITM cron intentionally untouched (already
|
|
325
|
+
compliant); open question whether RITM needs the same mandatory set. New open item: three paths
|
|
326
|
+
still **read** `u_status_task` — verify it comes back `On Hold` on the first post-deploy hold.
|
|
327
|
+
worker-only, single file (commit `ead92ad1e`, branch `TRUE-81044`, 34/-43); `php -l` clean on PHP
|
|
328
|
+
7.2, so the `library`-before-`togadesk` deploy-ordering constraint does not apply here.
|
|
329
|
+
(mhammontree)
|
|
256
330
|
- 2026-07-20 — **Corrected the authoritative-field model and shipped the INC hold push.**
|
|
257
331
|
Controlled test PATCHes proved the earlier "native `state` is authoritative; never use
|
|
258
332
|
`u_status_task`" conclusion (TRUE-79922) **backwards**: the custom **`u_status_task`** is the
|
package/package.json
CHANGED