toga-ai 1.0.398 → 1.0.400
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: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/AssortmentTranslation.php
|
|
@@ -25,9 +25,15 @@ Serves Assortment (product-grouping) **names** in multiple languages by adding a
|
|
|
25
25
|
English stays in `Assortments.name`; the sidecar holds only non-English overlays. Because the
|
|
26
26
|
translation layer is fully metadata-driven (see
|
|
27
27
|
[Language Translation Layer](../../api2/features/language-translation-layer.md)), wiring this up
|
|
28
|
-
required
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
required only a new table, model, Core record registration, ACL grant, and a single field link.
|
|
29
|
+
Compass Canada's French (fr-CA) assortment names are seeded as the first consumer.
|
|
30
|
+
|
|
31
|
+
> **Correction (2026-07-21):** the original "no api2 code change needed" claim was only true for the
|
|
32
|
+
> **FK/top-level** read paths. Assortment names are surfaced on the storefront **category browse** via a
|
|
33
|
+
> `join=` request (`Assortments.name`), and the joined-select-fields read path did **not** consult the
|
|
34
|
+
> sidecar — it copied the raw SQL column straight into the response, so the seeded French names stayed
|
|
35
|
+
> English there. That required a real api2 change (the 5th serialization site) — see the
|
|
36
|
+
> [Language Translation Layer](../../api2/features/language-translation-layer.md) 2026-07-21 change history.
|
|
31
37
|
|
|
32
38
|
## Key files / entry points
|
|
33
39
|
|
|
@@ -51,8 +57,10 @@ The mechanism itself is the data-driven translation layer documented in
|
|
|
51
57
|
4. **The field link (what makes it translate automatically).** The translatable `Assortments.name`
|
|
52
58
|
RecordField gets its `Core.RecordFields.translationRecordFieldId` set to the sidecar's `name`
|
|
53
59
|
RecordField. The api2 V2 layer reads `translationRecordFieldId` for every field and automatically
|
|
54
|
-
returns the sidecar value for the caller's resolved language (falling back to English when absent)
|
|
55
|
-
|
|
60
|
+
returns the sidecar value for the caller's resolved language (falling back to English when absent).
|
|
61
|
+
This works with no api2 code change for FK/top-level reads; the storefront's `join=` (joined-select)
|
|
62
|
+
read path additionally required the 5th-serialization-site fix landed 2026-07-21 (see the correction
|
|
63
|
+
note above and the Language Translation Layer doc).
|
|
56
64
|
5. **ACL.** A full ACL chain grants the **Base** role read/write on the new record. Because the
|
|
57
65
|
record's `aclDatabase = 'CLIENT'`, the grant lives in each client DB — see
|
|
58
66
|
[ACL Permission Chain](./acl-permission-chain.md).
|
|
@@ -75,6 +83,11 @@ The mechanism itself is the data-driven translation layer documented in
|
|
|
75
83
|
|
|
76
84
|
## Change history
|
|
77
85
|
|
|
86
|
+
- 2026-07-21 — Correction: the "no api2 code change needed" claim held only for FK/top-level reads. The
|
|
87
|
+
storefront category browse surfaces `Assortments.name` via a `join=` request, and the joined-select read
|
|
88
|
+
path in api2 did not consult the sidecar (raw column copied through), so seeded fr-CA names stayed English
|
|
89
|
+
there. Fixed by the 5th-serialization-site change in the Language Translation Layer (V2.php); no change to
|
|
90
|
+
this sidecar/model/metadata was required. (bala)
|
|
78
91
|
- 2026-06-26 — Initial build: `AssortmentTranslations` sidecar table + `_Model_Client_AssortmentTranslation`
|
|
79
92
|
model + `Core.Records` 332 registration + `translationRecordFieldId` link on `Assortments.name` + Base-role
|
|
80
93
|
ACL chain. Reuses the existing metadata-driven translation layer, so no api2 code change was needed.
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: ["jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -110,8 +110,11 @@ RecordField. `buildLookups`/RecordFields-load resolves this into
|
|
|
110
110
|
`lookupTranslatableFieldByRecordFieldId[sourceRecordFieldId] => {sidecarRecordId, sidecarField}`.
|
|
111
111
|
|
|
112
112
|
**Read path (PHP `_Model::load()`, NOT a SQL JOIN — deliberate choice).** At every response
|
|
113
|
-
serialization site (
|
|
114
|
-
|
|
113
|
+
serialization site `getTranslatedFieldValue($record, $field, $sourceModel, $defaultValue)` is called.
|
|
114
|
+
There are now **five** serialization sites, all of which must route through the helper: (1) top-level
|
|
115
|
+
full-model fields, (2) custom-field options, (3) foreign-key child objects, (4) inherent-child arrays
|
|
116
|
+
(both the specific-fields and the all-fields/expand-everything branches), and (5) **joined-select
|
|
117
|
+
fields** (fields requested from a `join=` table, e.g. `Assortments.name`). When a non-base
|
|
115
118
|
language is active and the field is translatable, it loads the sidecar row for that source row +
|
|
116
119
|
`languageId` (cached per row so multiple fields = one load) and returns the sidecar value if non-null;
|
|
117
120
|
otherwise it returns the English default and queues a fallback warning. The warning now carries the
|
|
@@ -119,6 +122,21 @@ missing record's `uuid` in its `identifiers` and dedupes per-field-**and**-per-r
|
|
|
119
122
|
(key `recordFieldId:uuid`) rather than once-per-field — so every record lacking a translation for a
|
|
120
123
|
field is reported individually.
|
|
121
124
|
|
|
125
|
+
**Joined-select fields (site 5) — how the source row is reached.** A `join=` field's value arrives as
|
|
126
|
+
a flat SQL column (`$row->{Alias_field}`), so the helper needs the joined row's identity to load its
|
|
127
|
+
sidecar. In the LIST/GET branch of `processRoutePairs` (joined-select SQL build ~L3700-3800,
|
|
128
|
+
serialization ~L4038-4060): when `shouldTranslate()` is true and a selected joined field is translatable
|
|
129
|
+
(`lookupRecordFieldByRecordIdAndField` + `lookupTranslatableFieldByRecordFieldId`), the SQL build also
|
|
130
|
+
SELECTs that joined table's primary key (`` `Alias`.`id` AS `Alias_id` ``), tracked in
|
|
131
|
+
`$joinedTableRecordsByAlias` and `$joinedTablePrimaryKeyAliasByAlias`; the hidden PK column is only
|
|
132
|
+
appended when not already requested (`in_array` guard). At serialization the joined source model is
|
|
133
|
+
instantiated by that PK (`new $joinedSourceModelName((int)$row->$alias)`) and each joined value routes
|
|
134
|
+
through `getTranslatedFieldValue()`. English and API-credential/public traffic is byte-identical (all
|
|
135
|
+
new logic gated behind `shouldTranslate()`, which is false for en / API / public — no extra SQL column,
|
|
136
|
+
no extra model load, output unchanged). This is why a metadata-only sidecar (e.g. `AssortmentTranslations`)
|
|
137
|
+
still needed an api2 code change to translate on the `join=` read path even though FK/top-level reads
|
|
138
|
+
worked with no code change.
|
|
139
|
+
|
|
122
140
|
**Write path.** On create and update, `extractTranslationWrites()` pulls translatable fields out of
|
|
123
141
|
the write set for a non-base language (so the English source is never overwritten), and after the
|
|
124
142
|
source row saves, `saveTranslationWrites()` upserts them into the sidecar for the current language.
|
|
@@ -191,6 +209,25 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
|
|
|
191
209
|
(matching `ItemTranslations`) while the source fields are `utf8mb4_0900_ai_ci`. Ad-hoc queries that
|
|
192
210
|
compare a sidecar string against a source string must add `COLLATE utf8mb4_bin` or MySQL throws a
|
|
193
211
|
collation-mismatch error.
|
|
212
|
+
- **GROUP BY gotcha on translatable joined fields (site 5).** The sibling joined-field code pushes each
|
|
213
|
+
selected joined field into `$groupBy` under aggregates; the hidden PK column added for translation must
|
|
214
|
+
**also** be pushed to `$groupBy` when `!empty($aggregateFunctionFields)`, or a non-English list request
|
|
215
|
+
combining a translatable `join=` with an aggregate/`group=` throws MySQL 1055 under `ONLY_FULL_GROUP_BY`.
|
|
216
|
+
Mirror the sibling `$groupBy[]` push for the PK column.
|
|
217
|
+
- **DISTINCT granularity (site 5).** The list path always runs `SELECT DISTINCT`, so adding the hidden PK
|
|
218
|
+
to the SELECT can in theory change DISTINCT granularity. Only reachable with distinct + join + a
|
|
219
|
+
translatable joined field whose rows share values but differ by PK; verified harmless for the real
|
|
220
|
+
storefront query (a `WHERE` pinning one assortment yields the same 4 rows). Worth checking if a
|
|
221
|
+
translatable join is added to a query that relies on DISTINCT collapsing duplicate rows.
|
|
222
|
+
- **Per-row cost on translatable-join lists (site 5).** `getTranslatedFieldValue` dereferences the source
|
|
223
|
+
model key via `_Model __get`, forcing a full row load, so ~2 queries per row (source + sidecar) for
|
|
224
|
+
non-English translatable-join lists — consistent with the feature's per-row-load tradeoff. Candidate
|
|
225
|
+
future optimization: let the helper accept a known id instead of a live `_Model`.
|
|
226
|
+
- **Pre-existing (not introduced by site 5): `join=`/`ojoin=` identifiers are unvalidated.** Table/alias/
|
|
227
|
+
ON-clause identifiers flow from the query string into SQL with no validation/escaping (`V2.php` ~L3416-3463,
|
|
228
|
+
`parseOptionsJoin` ~L7874-7920). The new PK-select line reuses the same already-tainted `$tableAlias` as the
|
|
229
|
+
adjacent pre-existing lines, so it adds no new injection class — but the underlying tainted-identifier path
|
|
230
|
+
is a standing concern flagged for separate follow-up.
|
|
194
231
|
- Reading a possibly-absent magic field off a generic `_Model` needs `array_key_exists(...)` +
|
|
195
232
|
`__get` — `isset()`/`?? null` always read false/null. See
|
|
196
233
|
[_Model magic-field access](../../_underscore/features/model-magic-field-access.md); this was the
|
|
@@ -202,6 +239,20 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
|
|
|
202
239
|
|
|
203
240
|
## Change history
|
|
204
241
|
|
|
242
|
+
- 2026-07-21 — Wired translation into the **joined-select-fields** path (the 5th and final read-path
|
|
243
|
+
serialization site) in `V2.php` `processRoutePairs` (joined-select SQL build ~L3700-3800, serialization
|
|
244
|
+
~L4038-4060). `join=` fields (e.g. `Assortments.name`) previously copied the raw SQL column straight into
|
|
245
|
+
the response and never consulted the sidecar, so Compass Canada's already-seeded French assortment/category
|
|
246
|
+
names stayed English on the storefront category browse. Fix (gated behind `shouldTranslate()`, so en/API/
|
|
247
|
+
public output is byte-identical): SELECT the joined table's PK as a hidden `` `Alias`.`id` AS `Alias_id` ``
|
|
248
|
+
(tracked in `$joinedTableRecordsByAlias`/`$joinedTablePrimaryKeyAliasByAlias`, `in_array`-guarded), then at
|
|
249
|
+
serialization instantiate the joined source model by that PK and route the value through
|
|
250
|
+
`getTranslatedFieldValue()`. Also fixed a GROUP BY 1055 bug (the hidden PK must be pushed to `$groupBy` when
|
|
251
|
+
aggregates are present). Noted the DISTINCT-granularity edge, the ~2-queries-per-row cost, and that the
|
|
252
|
+
pre-existing unvalidated `join=` identifier path is unchanged (no new injection class). No toga2-commerce or
|
|
253
|
+
DB/metadata change needed — sidecar rows, the `Assortments.name` `translationRecordFieldId` link, and the
|
|
254
|
+
fr-CA storefront request all already existed. cso: SAFE TO SHIP; php-reviewer: safe after the GROUP BY fix
|
|
255
|
+
(applied), remaining items non-blocking follow-ups. (bala)
|
|
205
256
|
- 2026-07-13 — Extended the translation layer to item **feature** text: three new sidecar tables
|
|
206
257
|
(`FeatureTranslations`, `ItemCategoryFeatureGroupTranslations`, `ItemFeatureTranslations`; Records
|
|
207
258
|
343/344/345, RecordFields 2442–2457) with `translationRecordFieldId` wired to the source fields,
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: nycdoe
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
10
|
-
owners: [mhammontree]
|
|
9
|
+
updated: 2026-07-20
|
|
10
|
+
owners: [mhammontree, sking]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/sync/nycdoe/send_ticket_updates.php
|
|
13
13
|
- worker/crons/sync/nycdoe/process_tickets.php
|
|
@@ -76,29 +76,55 @@ 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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
### Status fields (ServiceNow side) — CORRECTED 2026-07-20
|
|
80
|
+
> **The earlier "native `state` is authoritative; never use `u_status_task`" model was WRONG**
|
|
81
|
+
> (see Change history 2026-07-20). Both fields exist and both matter, with distinct roles:
|
|
82
|
+
- **`u_status_task` (custom) is the CLIENT-FACING, authoritative incident status** — the field
|
|
83
|
+
DOE's own ServiceNow views/reporting read (values `Open`, `In Progress`, `On Hold`, …). **A hold
|
|
84
|
+
MUST be written here or the client never sees it.** It is **writable from any active state**; a
|
|
85
|
+
controlled test PATCH of `{u_status_task:'On Hold'}` returned success and PERSISTED for days
|
|
86
|
+
(INC2192842 held 3+ days). **The TOGaDesk inbound sync keys off `u_status_task`**
|
|
87
|
+
(`process_tickets.php:206` reads `$data->u_status_task`), and the RITM cron checks
|
|
88
|
+
`u_status_task == 'On Hold'` (`send_request_item_updates.php:224`) — so `u_status_task` **is**
|
|
89
|
+
the correct field for hold detection.
|
|
90
|
+
- The native **`state`** field is ServiceNow's INTERNAL, SLA-bearing state machine. Setting
|
|
91
|
+
`state=3` (On Hold) pauses the SNOW SLA clock but is **invisible to the DOE client**. ServiceNow
|
|
92
|
+
only permits the native On Hold transition **FROM In Progress (`state=2`)** — pushing `state=3`
|
|
93
|
+
from any other state (Assigned, or already Resolved/Closed/Canceled) is silently rejected (HTTP
|
|
94
|
+
200, empty body — see the empty-body gotcha). `hold_reason=10` is not a valid choice on this
|
|
95
|
+
instance and is stored empty/dropped, but native On Hold still sticks without it.
|
|
96
|
+
- Numeric `state` codes seen on the wire: INC `On Hold`=**3**, `In Progress`=**2**;
|
|
97
|
+
RITM `On Hold`=**8**, the scheduled-update state=**"-14"**.
|
|
98
|
+
|
|
99
|
+
### Outbound (TOGaDesk → ServiceNow) — INC hold push (worker #1676 + #1677)
|
|
100
|
+
When a DOE repair order is on hold, `send_ticket_updates.php` pushes:
|
|
101
|
+
- **Always set `u_status_task='On Hold'`** — the client-facing field, writable from any active
|
|
102
|
+
state; on release set it back to `'In Progress'`. This is what actually surfaces the hold to the
|
|
103
|
+
client. Until the local status stops reverting (the library `qqStatus()` fix below) the outbound
|
|
104
|
+
never sees `orderIsOnHold=true` and never pushes `u_status_task` at all — hence the deploy
|
|
105
|
+
dependency.
|
|
106
|
+
- **Set native `state=3` (On Hold) only when the incident is currently `In Progress`** — an SLA
|
|
107
|
+
pause. Because ServiceNow rejects the transition from any other state (empty-body 200), the cron
|
|
108
|
+
**defers without advancing `dtSynced`** when the incident is neither On Hold nor In Progress, and
|
|
109
|
+
re-pushes only once it reaches In Progress.
|
|
110
|
+
- **Closed-incident reconcile:** if the incident is already `Resolved`/`Closed`/`Canceled` on the
|
|
111
|
+
SNOW side it can no longer be held — the cron **advances `dtSynced`** to accept the remote
|
|
112
|
+
closure instead of retrying forever. (A canceled incident, INC2190846, had generated **2,392**
|
|
113
|
+
failed empty-body PATCHes before this.)
|
|
114
|
+
- The "Assigned → In Progress" auto-start block is still guarded so it never pushes `state:2` over
|
|
115
|
+
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`, plus `SN_TASK_STATUS_ON_HOLD` /
|
|
117
|
+
`SN_TASK_STATUS_IN_PROGRESS` for the `u_status_task` writes (#1677).
|
|
118
|
+
- `send_request_item_updates.php` (RITM) pushes `state:"8"` (On Hold) and carries a defensive
|
|
119
|
+
`HOLD_*` guard on the scheduled-update path so it can't push `state:"-14"` while held; its
|
|
120
|
+
reverse-hold logic keys off `u_status_task == 'On Hold'`.
|
|
95
121
|
|
|
96
122
|
### Inbound (ServiceNow → TOGaDesk)
|
|
97
|
-
- `process_tickets.php`
|
|
98
|
-
(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
123
|
+
- `process_tickets.php` reads the client-facing **`u_status_task`** field
|
|
124
|
+
(`$status = $data->u_status_task`, L206) — **NOT** native `state` — and maps `On Hold` to the
|
|
125
|
+
bare **`STATUS_HOLD`** on the local repair order (this is exactly why the library `qqStatus()`
|
|
126
|
+
fix below had to recognize bare `HOLD`). Both inbound "Assigned-force" `patchINC(state:2)` blocks
|
|
127
|
+
are guarded against a local hold.
|
|
102
128
|
|
|
103
129
|
### RITM "ETA-in-hold"
|
|
104
130
|
- `send_request_item_updates.php` syncs `scheduled_date` (ETA) to ServiceNow **inside the
|
|
@@ -182,9 +208,19 @@ The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets
|
|
|
182
208
|
auto-start fought the hold push — `state:3` then `state:2` pushed seconds apart, every run,
|
|
183
209
|
so SNOW never stayed On Hold. Any future auto-start/auto-status block on these crons MUST
|
|
184
210
|
short-circuit when the local RO is in a `HOLD_*` status.
|
|
185
|
-
-
|
|
186
|
-
native `state`
|
|
187
|
-
|
|
211
|
+
- **⚠ SUPERSEDED (see Change history 2026-07-20):** TRUE-79922 originally concluded that reading
|
|
212
|
+
`u_status_task` (instead of native `state`) was a bug and that hold detection should be on native
|
|
213
|
+
`state` only. That was **backwards.** `u_status_task` is the client-facing authoritative field
|
|
214
|
+
and **is** the correct hold-detection field — the live inbound cron reads it at
|
|
215
|
+
`process_tickets.php:206`. Native `state` is the internal SLA state, pushed only as a secondary
|
|
216
|
+
pause when the incident is In Progress.
|
|
217
|
+
- **The empty-body 200 (nycd3 `data/tasks` PATCH):** `App_Api_NYCDOEV2::patchINC`/`patchRITM`
|
|
218
|
+
(both hit `/api/nycd3/data/tasks/{number}`) return **HTTP 200 with an EMPTY body (no `result`
|
|
219
|
+
object)** when a requested native `state` transition is illegal — e.g. pushing `state=3` from a
|
|
220
|
+
non-In-Progress state. This is silent: no exception, no error field. A canceled incident
|
|
221
|
+
(INC2190846) generated **2,392** such failed PATCHes before the closed-incident reconcile was
|
|
222
|
+
added. Gate native `state=3` on `snState == 'In Progress'`, and reconcile
|
|
223
|
+
Resolved/Closed/Canceled by advancing `dtSynced`.
|
|
188
224
|
- Holds are **never auto-released** — if a SNOW user manually changes status, the outbound
|
|
189
225
|
cron correctly re-asserts On Hold; that is intended, not a bug. Clear the hold in TOGaDesk.
|
|
190
226
|
- **Diagnosing which writer dropped a hold:** `repair_order_history`
|
|
@@ -217,6 +253,25 @@ The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets
|
|
|
217
253
|
`repair_orders.status` directly cannot distinguish the six hold variants after a recompute.
|
|
218
254
|
|
|
219
255
|
## Change history
|
|
256
|
+
- 2026-07-20 — **Corrected the authoritative-field model and shipped the INC hold push.**
|
|
257
|
+
Controlled test PATCHes proved the earlier "native `state` is authoritative; never use
|
|
258
|
+
`u_status_task`" conclusion (TRUE-79922) **backwards**: the custom **`u_status_task`** is the
|
|
259
|
+
client-facing authoritative incident status (a `{u_status_task:'On Hold'}` PATCH persisted 3+
|
|
260
|
+
days, INC2192842), the inbound cron reads it (`process_tickets.php:206`), and the RITM cron keys
|
|
261
|
+
off it (L224). Native `state` is the internal SLA state and only accepts On Hold **from In
|
|
262
|
+
Progress** — any other transition returns a silent HTTP-200 empty body (a canceled incident,
|
|
263
|
+
INC2190846, had racked up 2,392 failed PATCHes). Fix: outbound `send_ticket_updates.php` now
|
|
264
|
+
always writes `u_status_task='On Hold'` (client-facing; `'In Progress'` on release), sets native
|
|
265
|
+
`state=3` only when In Progress (SLA pause), and reconciles already-closed incidents by advancing
|
|
266
|
+
`dtSynced` instead of retrying forever; added `SN_STATE_*` / `SN_HOLD_REASON_DEFAULT` /
|
|
267
|
+
`SN_TASK_STATUS_*` constants. worker PRs **#1676** (merged — retry-storm/closed reconcile + native
|
|
268
|
+
state gate) and **#1677** (open — `u_status_task` write). (sking)
|
|
269
|
+
- 2026-07-20 — library **#845**: `App_Model_TogaDesk_RepairOrder::qqStatus()` now recognizes all
|
|
270
|
+
six `HOLD_*` constants in both "on hold" IN-clauses. The inbound sync writes SNOW On Hold as the
|
|
271
|
+
bare `STATUS_HOLD` ('HOLD'), which the prior 2-of-6 match dropped, silently recomputing the order
|
|
272
|
+
to `ORDER_ASSIGNED_AWAITING_SCHEDULING` (the local "hold revert"). Additive only. Must ship with/
|
|
273
|
+
before worker #1677 — until the local status stops reverting, the outbound never sees
|
|
274
|
+
`orderIsOnHold=true`. (sking)
|
|
220
275
|
- 2026-07-07 — TRUE-80060 (togadesk portion): closed the **third** DOE hold-failure path — a
|
|
221
276
|
hold set via a note/comment (`Repair::addNotes`) never reaching SNOW. `addNotes` recorded
|
|
222
277
|
status only on `repair_order_notes.newstatus` (never the `repair_orders.status` cache the
|
package/package.json
CHANGED