toga-ai 1.0.427 → 1.0.429

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-07-21
9
+ updated: 2026-07-23
10
10
  owners: ["jcardinal", "mhammontree", "tcox"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -56,6 +56,13 @@ writable). Without it the field is rejected on write **and** is unreadable:
56
56
  - **`AclFieldPermissions` is `UNIQUE (recordFieldId, roleId)`** (Client blank DDL) — guard inserts
57
57
  with `NOT EXISTS`. For a **read-only, server-written** field (one populated by a `postPost`
58
58
  interceptor rather than the API caller), grant `isWritable = 0`.
59
+ - **A read-only field only needs a grant on the role(s) that actually READ it — find that role by
60
+ mirroring a sibling field of the same kind the readers already consume**, rather than blanket-granting
61
+ every role. E.g. the standard FK `serviceAddressId` mirrored its sibling FK `saleItemId`, whose sole
62
+ reader in `Client_Rate` is **role 1** (the toga2-view service cards) — so a single role-1 read grant
63
+ sufficed, NOT a 1/2/4 fan-out (a cross-role probe had shown roles 1/2/4 all lacked the grant, but only
64
+ role 1 actually reads the field). Grant `isWritable = 0` for a server-written field; resolve the role
65
+ by name/subselect (ids differ per client DB). Beta-proven on Rate `Client_Rate` 2026-07-23.
59
66
 
60
67
  ### Standard field vs. custom (`c_`) field — TWO different permission tables
61
68
 
@@ -199,6 +206,11 @@ the field.
199
206
 
200
207
  ## Change history
201
208
 
209
+ - **2026-07-23** — TRUE-79533: noted that a **read-only field only needs a grant on the role(s) that
210
+ actually read it** — mirror a sibling field of the same kind the readers already consume rather than
211
+ granting every role. The standard FK `serviceAddressId` mirrored `saleItemId`'s reader, **role 1** in
212
+ `Client_Rate` (the toga2-view service cards), so a single role-1 read grant sufficed (not a 1/2/4
213
+ fan-out). (tcox)
202
214
  - **2026-07-23** — TRUE-79533: documented the **READ side** of `AclFieldPermissions` — a registered
203
215
  field with no grant returns **`EZ-2`** on read (the counterpart of the write code `EV-9`), and
204
216
  the **standard vs. custom** split: standard fields use `AclFieldPermissions` (keyed by
@@ -60,5 +60,3 @@ easy to misread as an API problem.
60
60
  values to `String` before `urlEncode` — a numeric value (first hit via `fetchAddressesByIds`)
61
61
  threw `input.replace is not a function` and silently aborted the request client-side. Added
62
62
  regression tests. (tcox)
63
- </content>
64
- </invoke>
@@ -46,4 +46,3 @@ the store when it changed.
46
46
  field (a different covered property can have a different contact number). Previously the field
47
47
  was disabled once the store held a phone, and the post-purchase save made the first purchase lock
48
48
  it for every later visit. `save-if-different` retained. (tcox)
49
- </content>
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: nycdoe
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-07-09
9
+ updated: 2026-07-23
10
10
  owners: [mhammontree, sking]
11
11
  files:
12
12
  - worker/crons/sync/nycdoe/import_asn.php
@@ -248,6 +248,45 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
248
248
  164785 (internalId 6971859 → `38S0500` synced, 15 ROs) and PO 165751 (`50M7280` + `A3L980`
249
249
  NOT synced). PRs: library#831 (merge first) then worker#1670.
250
250
 
251
+ - **⚠ REGRESSION of the 2026-07-09 multi-PO fix — a cross-wired SO/PO stamp silently drops
252
+ install tickets (diagnosed 2026-07-23; deferred to a one-off backfill, cron NOT changed).**
253
+ The 2026-07-09 fix (library#831 + worker#1670) made Stage 5 resolve item receipts **only**
254
+ Sales-Order-wide via `App_NetSuite::listItemReceiptsCreatedFromSalesOrder($soId)` (which fans
255
+ across the POs from `listPurchaseOrdersCreatedFromSalesOrder($soId)`). It **replaced** —
256
+ rather than augmented — the old by-stamped-PO lookup (`listItemReceiptsCreateFromPurchaseOrder`).
257
+ So when an ASN item's stamped `netSuiteInternalPurchaseOrderId` is **not a child of** its
258
+ stamped `netSuiteInternalSalesOrderId`, the SO-wide fan-out never inspects the stamped PO,
259
+ finds zero receipts, and the cron **silently creates no repair order and raises no error** —
260
+ the same "serials never sync, no error" symptom as the bug it replaced, in a distinct case.
261
+ - **Fingerprint:** an item stamped an SO whose only child PO has no receipt, while the stamped
262
+ PO (often an internalId *lower* than the SO's, i.e. created earlier) carries the real receipt
263
+ with the matching serials.
264
+ - **Root cause of the cross-wired stamp (SME Skyler King confirmed) = a warehouse
265
+ manual-entry error, not automation.** Confirmed case ASN **26215** (customer PO
266
+ `S202641365`): original SO `6901611` processed only `DOE-30HSS1BB00` (qty 1), PO `6901612`,
267
+ item receipt `6910404` (ticketed fine, RO 811791). Two monitors `DOE-62C5GAR1US` (serial
268
+ `VKWD9092`) and `DOE-64B6MAR1UZ` (serial `V600F9M6`) were **manually** added to SO `6901611`
269
+ with a **manually** created PO `6910401`, received on item receipt `6910410`. Separately,
270
+ when the real ASN arrived, the automation created a **duplicate** SO `6996287` (auto-PO
271
+ `6996288`, never used to receive — the serials were already on `6910410`). Net: ASN items
272
+ 53905/53914 ended stamped SO=`6996287` (the duplicate) but PO=`6910401` (the manual PO under
273
+ the *original* SO), so SO-wide resolution finds nothing.
274
+ - **SME-confirmed correct end-state:** the units belong to the **original** SO (`6901611`);
275
+ the automation-created duplicate SO `6996287` / PO `6996288` is a NetSuite cleanup item
276
+ (cancel). The owed installs (one repair order per received unit) are **independent** of the
277
+ NetSuite duplicate and are created **TOGa Desk-side only**. Warehouse process has since been
278
+ corrected by training.
279
+ - **Recovery (chosen fix — one-off, not a cron change):** a run-once backfill reuses
280
+ `doeCreateInstallationRepairOrder` verbatim from Stage 5 (`3_create_installation_ticket.php`)
281
+ but scoped to a single ASN (id via CLI arg), resolving receipts from the **UNION** of the
282
+ SO's child POs *and* the item's stamped PO. It is idempotent (only units with
283
+ `togadeskRepairOrderId IS NULL`), guards double dispatch (skips a serial that already has a
284
+ `repair_order` in `db_togadesk`), never adds new `AdvanceShippingNoticeUnits`, is
285
+ serialized-only, and makes no NetSuite/ServiceNow writes. Confirmed on ASN 26215 (created the
286
+ two owed ROs). A **permanent union fix in the cron** (resolve receipts from SO child POs plus
287
+ the stamped PO) was considered but **deferred** — the team chose the one-off because the
288
+ warehouse process error is now prevented by training.
289
+
251
290
  ## Debugging & DB topology (DOE 1.0 sync)
252
291
 
253
292
  Reference for future DOE investigations — the 1.0 worker cannot run on a dev/Windows box;
@@ -265,6 +304,14 @@ use the toga DB MCP + `Logs.API` instead of running prod code locally.
265
304
  6971859 returned `totalRecords=1`, only `38S0500`.)
266
305
  - **`Bridge_NetSuite`** (legacy) has `SalesOrders` / `SalesOrderItems` / `SerialNumbers` but
267
306
  **no `PurchaseOrders` table** — POs are pulled live from the NetSuite API.
307
+ - **Stuck-vs-received check, per ASN item:** compare
308
+ `listItemReceiptsCreatedFromSalesOrder($stampedSO)` against
309
+ `listItemReceiptsCreateFromPurchaseOrder($stampedPO)`, dumping each receipt line's `itemName`
310
+ plus its `inventoryAssignment` serials. If the SO-wide call returns 0 but the stamped PO
311
+ returns the receipt with the matching serials, it is the cross-wired-stamp regression above.
312
+ - **`App_Model_Core_AdvanceShippingNotice` does NOT map `dtCreatedInstallationTicket`** — its
313
+ `__get` throws "No field or quick query defined for key …". Read that column via **raw SQL**,
314
+ not the model.
268
315
 
269
316
  ## Operating rules when changing this integration
270
317
 
@@ -278,6 +325,17 @@ use the toga DB MCP + `Logs.API` instead of running prod code locally.
278
325
  of the consumer query; `php -l` every touched file.
279
326
 
280
327
  ## Change history
328
+ - 2026-07-23 — Diagnosed a regression of the 2026-07-09 multi-PO fix: Stage 5's SO-wide receipt
329
+ resolution *replaced* the by-stamped-PO lookup, so a cross-wired stamp (an item's stamped PO
330
+ not a child of its stamped SO) silently drops install tickets with no error. Root cause of the
331
+ cross-wire = a warehouse manual-entry error (a duplicate SO was auto-created while the real
332
+ receipt sat under a manually-created PO on the original SO); SME Skyler King confirmed the
333
+ correct end-state (units on the original SO, duplicate SO cancelled, owed installs are TOGa
334
+ Desk-side only). Recovered stuck ASN 26215 with a one-off union-receipt backfill (union of SO
335
+ child POs + the stamped PO; idempotent; no NetSuite/ServiceNow writes) that created the two
336
+ owed ROs; a permanent union fix in the cron was considered but deferred (warehouse process now
337
+ corrected by training). Added a regression gotcha + two debugging notes. No production code
338
+ changed. (mhammontree; SME sking)
281
339
  - 2026-07-09 — Fixed silent install-ticket drop when a NetSuite SO is fulfilled across
282
340
  multiple POs: Stage 5 now resolves item receipts Sales-Order-wide via new helper
283
341
  `App_NetSuite::listItemReceiptsCreatedFromSalesOrder` (fans across all POs from the SO,
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: rate
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-06-29
10
- owners: [mhammontree]
9
+ updated: 2026-07-23
10
+ owners: [mhammontree, tcox]
11
11
  files:
12
12
  - _underscore/Model/Rate/Entitlement.php
13
13
  - _underscore/ApiRequest.php
@@ -96,9 +96,21 @@ creation vs. cancellation. This is an accepted, documented limitation — not an
96
96
  address, so an entitlement with no address never even attempts contract creation (and
97
97
  never logs a `/contract` call).
98
98
  - **Auth-induced failures are blind to the monitor** — see the detection limitation above.
99
+ - **The AIG-success branch REASSIGNS `$payload->uuid`** (`$payload->uuid = _String::generateUuid()`,
100
+ repurposed as the AIG contract number) — and it does so **in the same `postPost`** that pins the WH
101
+ service address. On the pre-redesign build the pin step ran **after** this reassignment and keyed
102
+ off `$payload->uuid`, so **every successful AIG contract silently broke the WH address pin** (the
103
+ pin lookup matched no row). Preserve the ordering fix — snapshot the entitlement uuid **before**
104
+ this branch. See the
105
+ [WH per-address purchase guard](whole-home-warranty-purchase-guard.md) "AIG-success clobbers the
106
+ pin" gotcha (`_underscore` `_beta` `9c2e4b1c`).
99
107
 
100
108
  ## Change history
101
109
 
110
+ - 2026-07-23 — Documented that the **AIG-success branch reassigns `$payload->uuid`** (as the contract
111
+ number) inside the shared `postPost`, which silently broke the WH service-address pin on the
112
+ pre-redesign build (pin keyed off the now-reassigned uuid). Fixed by snapshotting the entitlement
113
+ uuid before the AIG branch (`_underscore` `_beta` `9c2e4b1c`). Cross-linked the WH guard doc. (tcox)
102
114
  - 2026-07-07 — `postPost` now also persists the validated service address
103
115
  (`Entitlements.c_serviceAddressId`, `Addresses.isValidated=1`), wrapped so it never blocks the
104
116
  AIG-contract/email flow; a new WH `prePost` guard carrier-normalizes the address onto the
@@ -106,6 +106,10 @@ Ticket: TRUE-79533. Business rule owner: PM Paulina.
106
106
  `Addresses.isValidated = 1` and writes `Entitlements.serviceAddressId`. This block is
107
107
  wrapped so a failure never interrupts the AIG-contract creation or the confirmation email
108
108
  (see the [AIG contract creation doc](aig-contract-creation.md) — the same `postPost`).
109
+ **The entitlement UUID is snapshotted up front** (`$entitlementUuid = $payload->uuid`) at the
110
+ top of `postPost`, **before** the AIG branch runs — because the AIG-success branch reassigns
111
+ `$payload->uuid` (see the "AIG-success clobbers the pin" gotcha). The pin lookup must use the
112
+ snapshot, not `$payload->uuid`.
109
113
  6. All SQL uses `_Database::escape()` / int-casts — `_Query` has **no bind API** in this path.
110
114
 
111
115
  ## Field & model: `serviceAddressId` is now a STANDARD field (reviewer Jeff)
@@ -280,6 +284,22 @@ live carrier waterfall is opt-in via `RUN_LIVE=1` (defaults off). Verified **18/
280
284
  - **`ApiPayloadInterceptors` (Client_Rate) has no `phpMethod` column.** The interceptor method
281
285
  is resolved **by convention** from `(prePostProcessing, httpMethod)`: `PRE+POST → prePost`,
282
286
  `POST+POST → postPost` (matches the framework `[pre|post][HttpMethod]` convention).
287
+ - **ROOT CAUSE of NULL pins on the pre-redesign build: the pin silently fails whenever the AIG
288
+ contract is created SUCCESSFULLY.** The old `postPost` pinned the entitlement by `$payload->uuid`,
289
+ but the **AIG-success branch reassigns `$payload->uuid = _String::generateUuid()`** (reusing it as
290
+ the AIG contract number) **before** the pin step. So on an AIG success the pin lookup by
291
+ `$payload->uuid` matches **no row**, logs `"TRUE-79533: could not resolve service address"`, and
292
+ gives up — leaving `Entitlements.serviceAddressId` (and, pre-redesign, `c_serviceAddressId`) NULL
293
+ and the purchase's `Addresses.isValidated` NULL (that pair is the fingerprint of a lost pin).
294
+ Pinning is therefore **intermittent — it works only when AIG auth FAILS** (the branch that
295
+ clobbers the uuid never runs). Proven on beta `Client_Rate` 2026-07-23: entitlement 68 pinned,
296
+ 69/70/71 unpinned despite an intact Contact→`primaryContactAddressId`→ContactAddresses chain.
297
+ **Fixed in `_underscore` `_beta` commit `9c2e4b1c`**: `postPost` snapshots
298
+ `$entitlementUuid = $payload->uuid` up front, before the AIG branch, and pins by the snapshot.
299
+ **DEPLOY DEPENDENCY:** any environment still running a **pre-redesign** `api2`/`_underscore` build
300
+ loses the address pin every time AIG succeeds; the fix only takes effect on **EB redeploy**
301
+ (api2 pulls `_underscore` at deploy). This is the mechanism behind the COALESCE fallback and the
302
+ dangling-pin gotcha below — the fallback is what keeps the dedup correct while pins are missing.
283
303
  - **Dedup is defeated by a DANGLING pin (not just a NULL one).** The COALESCE fallback covers a
284
304
  **NULL** `serviceAddressId`, but if `serviceAddressId` points at a **deleted `Addresses` row**,
285
305
  the join drops that entitlement and it becomes **invisible to the guard** — a duplicate WH slips
@@ -324,6 +344,17 @@ live carrier waterfall is opt-in via `RUN_LIVE=1` (defaults off). Verified **18/
324
344
  (verified in `api2/Component/Api/V2/V2.php` `getAclFieldPermissions()` ~line 5731), which is why
325
345
  a custom→standard promotion must re-create the read grant as an `AclFieldPermissions` row.
326
346
  (mhammontree)
347
+ - 2026-07-23 — **Root cause of intermittent NULL pins + guard hardening confirmed** (tcox). (1) The
348
+ `postPost` service-address pin **silently fails whenever the AIG contract creation SUCCEEDS**,
349
+ because the AIG-success branch reassigns `$payload->uuid` before the pin step, so pinning by
350
+ `$payload->uuid` matches no row (fingerprint: `serviceAddressId` + `Addresses.isValidated` both
351
+ NULL). Proven on beta (entitlement 68 pinned, 69/70/71 not). Fixed in `_underscore` `_beta`
352
+ `9c2e4b1c` (snapshot `$entitlementUuid` up front). Deploy-dependency: pins are lost on AIG success
353
+ on any pre-redesign build until EB redeploy. (2) The dedup blind spot (unpinned entitlements
354
+ invisible to `hasActiveWarrantyAtAddress` — demonstrated live: two active WH at "STE 250" slipped
355
+ through on the old build) is **CLOSED in `_beta` `a9f408bc`** — the dedup joins Addresses via
356
+ `COALESCE(serviceAddressId, ContactAddresses.addressId)`, disables the query cache for the read,
357
+ and documents an accepted TOCTOU. (tcox)
327
358
  - 2026-07-23 — **LAUNCH BLOCKER found: the standard `serviceAddressId` field has NO read ACL grant**
328
359
  (tcox). Probe-verified on beta: `GET /v2/entitlements?fields=serviceAddressId` → 403 `EZ-2` for
329
360
  roles 1/2/4 (`aclDatabase=CLIENT`). Standard fields are authorized via **`AclFieldPermissions`**
@@ -3,6 +3,7 @@ title: "Rate"
3
3
  framework: "2.0"
4
4
  apps:
5
5
  - _underscore
6
+ - api2
6
7
  - saml
7
8
  - toga2-view
8
9
  - worker
@@ -12,8 +13,8 @@ project: SAML SSO Gateway
12
13
  client: rate
13
14
  type: profile
14
15
  status: active
15
- updated: 2026-06-30
16
- owners: ["rgirish", "bala", "mhammontree"]
16
+ updated: 2026-07-23
17
+ owners: ["rgirish", "bala", "mhammontree", "tcox"]
17
18
  files: []
18
19
  related:
19
20
  - clients/rate/features/saml-sso.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.427",
3
+ "version": "1.0.429",
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",