toga-ai 1.0.914 → 1.0.916
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/2.0/apps/api2/features/nested-relationship-writes.md +3 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nycdoe/features/asn-sftp-ingestion-2-0.md +18 -2
- package/knowledge/clients/nycdoe/features/servicenow-integration.md +8 -1
- package/knowledge/clients/prudential/INDEX.md +1 -0
- package/knowledge/clients/prudential/features/dell-lch-iop-transmissions.md +6 -1
- package/knowledge/clients/prudential/features/service-request-address-validation.md +6 -2
- package/knowledge/clients/prudential/features/service-request-bundle-catalog-resolution.md +56 -0
- package/knowledge/clients/prudential/profile.md +2 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-29
|
|
10
10
|
owners: ["bala", "mhammontree", "tcox", "apeterson", "rgirish", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts
|
|
@@ -193,9 +193,11 @@ behaviour — it bites any write to a nested child, not one feature. Worked exam
|
|
|
193
193
|
- **The UPDATE path returns 200 even when a child write failed** — write children directly against the child route and check each, if you need to know they landed.
|
|
194
194
|
- **Not every record is linkable by `uuid`** — read `searchableIdentifierFields` in the EV-12 body; fall back to `id` resolved by a natural key, never hardcoded.
|
|
195
195
|
- **Declaring a `c_` field on the model does not make it searchable** — the `CustomRecordFields.isIdentifier` flag is the gate; set the flag, not the child policy.
|
|
196
|
+
- **An identifier that is unique only inside a parent (e.g. `bundles.number` per catalog) links the WRONG record, silently.** The match uses only the identifier fields sent, so `bundle: {number: "001"}` hits another catalog's `001`. Send `uuid` (or add the scoping identifier) resolved in the caller. Seen on Prudential India REQs — see [bundle catalog resolution](../../../clients/prudential/features/service-request-bundle-catalog-resolution.md).
|
|
196
197
|
- **Singular-FK back-reference defects:** UPDATE path drops the parent's forward FK (echoes `null`); CREATE path re-parents a value-matched child to the newest writer (stolen child row). One defect, two symptoms.
|
|
197
198
|
|
|
198
199
|
## Change history
|
|
200
|
+
- 2026-09-29 — Gotcha: a per-parent identifier (`bundles.number`) matches across parents and links the wrong row. (rgirish)
|
|
199
201
|
- 2026-09-24 — Documented that the engine recurses into a nested inherent child with `POST`
|
|
200
202
|
hardcoded (`V2.php:5415`), so a child nested in a PUT is duplicated or fails 1062; update
|
|
201
203
|
children through their own route (TRUE-82138). (apeterson)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
19
19
|
|
|
20
20
|
## 2.0 framework
|
|
21
21
|
|
|
22
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
22
|
+
- **_underscore** (_Underscore) _(framework core)_ — 93 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
23
23
|
- **worker2** (Worker) — 80 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
24
24
|
- **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
25
25
|
- **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: nycdoe
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-29
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Sftp/Import.php
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
- dbchanges2/Client_Nycdoe/2026-09-22b - Add ASN parsing fields to ServiceRequests.sql
|
|
18
18
|
- dbchanges2/Client_Nycdoe/2026-09-22c - Add customerPurchaseOrder field to Cases.sql
|
|
19
19
|
- worker2/tests/Integration/ParseCest.php
|
|
20
|
+
- dbchanges2/Core/2026-09-14a - Add SFTP Import cron - NYCDOE Lenovo.sql
|
|
20
21
|
related:
|
|
21
22
|
- servicenow-integration.md
|
|
22
23
|
- ../profile.md
|
|
@@ -46,7 +47,21 @@ The 2.0 replacement for 1.0's `legacy_import_asn.php` + `asnprocessor/*` (see [S
|
|
|
46
47
|
| Acer | CDW 47-col, 3 names differ from Lexmark | `ACER_HEADER` | `YYYY/MM/DD` |
|
|
47
48
|
| Apple | own | `APPLE_HEADER` | `order_date` `M/D/YY` or `M/D/YYYY` |
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
### Activation / cutover (NOT built — design under review)
|
|
51
|
+
- **`Sftp/Import/Run` is read-only on the SFTP** (`nlist`/`stat`/`get` only). 1.0 still deletes each file within minutes and runs `*/5` vs 2.0 `*/15`, so a naive 2.0 shadow run sees a random subset. See 1.0 Stage 1b in [ServiceNow / ASN Integration](servicenow-integration.md).
|
|
52
|
+
- **DECIDED (Mark, 2026-09-25/29): 1.0 must not delete a file until 2.0 has imported and parsed it.** Replaces the 2026-08-28 "copy to temp subfolder" bridge idea.
|
|
53
|
+
- **The 4 `dbchanges2/Core/2026-09-14a - Add SFTP Import cron - NYCDOE <Mfr>` rows ship `isActive=0`** (ordered rollout); they stay off through shadow mode.
|
|
54
|
+
- **PROPOSED design (2026-09-29, for review with Jeff; needs a fresh `cto` pass — the 2026-09-25 CTO agreement was for an earlier polling variant):**
|
|
55
|
+
1. 1.0 replaces its `DeleteSftpFile` runTask with `Sftp/Import/RunForFile(clientId, username, filePath)` — no password in SQS.
|
|
56
|
+
2. `RunForFile` loads **all** `Sftp/Import/Run` CronJobs rows for clientId+username (the CDW username returns both Lexmark and Acer rows — never trust a manufacturer from 1.0), matches `fnmatch` patterns (no match → delete directly, old behavior), fetches, saves `Files`, queues Parse with `deleteSourceOnSuccess=true`.
|
|
57
|
+
3. Parse on success queues new `Sftp/Import/DeleteSource(clientId, filesUuid)`, which re-checks `c_status` (`succeeded`/`released`) **and** the remote file's sha256 vs `Files.checksum` before deleting.
|
|
58
|
+
4. Checksum already in `Files`: `succeeded` → delete; stale `pending` → re-queue Parse; `failed` → keep the file.
|
|
59
|
+
5. 1.0 skips files already in `Logs.FileLog` (job+fileName+fileTimestamp) but still re-queues `RunForFile`.
|
|
60
|
+
6. Kill switch `[nycdoe] asn_delete_mode` = `gated` | `immediate`; missing = `immediate`.
|
|
61
|
+
7. Cutover: turn the CronJobs rows on with `deleteSourceOnSuccess`, remove the 1.0 processors.
|
|
62
|
+
8. Add `released` to `Files.c_status` for manual release of failed files.
|
|
63
|
+
|
|
64
|
+
Schema (`dbchanges2/Client_Nycdoe/`): `2026-09-14a` Files `c_status` (ENUM `pending`/`processing`/`succeeded`/`failed`, not shipped)/`c_manufacturer`/`c_dtProcessed`; `2026-09-22a` ServiceRequestStages seed (New/Scheduled/On Hold/Delivered/Cancelled); `2026-09-22b` ServiceRequests fields; `2026-09-22c` Cases `c_customerPurchaseOrder`; `2026-09-25a` Cases `longDescription`.
|
|
50
65
|
|
|
51
66
|
Tests: `worker2/tests/Integration/ParseCest.php` (all 4 mfrs + header rejection + comma repair) — see [worker2 Codeception integration tests](../../../2.0/apps/worker2/features/codeception-integration-tests.md).
|
|
52
67
|
|
|
@@ -60,4 +75,5 @@ Tests: `worker2/tests/Integration/ParseCest.php` (all 4 mfrs + header rejection
|
|
|
60
75
|
- **Schema drift: `_Model_Client_Case` declares `longDescription` but `Client_Nycdoe.Cases` lacked the column** → any direct-model Case load/save 1054s. Fixed for DOE (`2026-09-25a`). Other tenants' `Cases` may have the same gap — unchecked (ask Rohan).
|
|
61
76
|
|
|
62
77
|
## Change history
|
|
78
|
+
- 2026-09-29 — Activation plan: gated-delete decision, proposed RunForFile/DeleteSource design, CronJobs rows set isActive=0 (mhammontree)
|
|
63
79
|
- 2026-09-25 — Phase 2 parsing built for all 4 mfrs; header validation, comma repair, real-sample header/date corrections; integration tests added (mhammontree)
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- dbchanges/Core/SK/
|
|
39
39
|
- dbchanges/TOGaDeskSupport/SK/
|
|
40
40
|
- worker2/Worker/Monitor/Nycdoe.php
|
|
41
|
+
- worker2/Worker/Utilities/Sftp/DeleteFile.php
|
|
41
42
|
related:
|
|
42
43
|
- ../profile.md
|
|
43
44
|
- edi-850-receive-and-997-ack.md
|
|
@@ -110,6 +111,10 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
|
|
|
110
111
|
|
|
111
112
|
- **Stage 1a (API):** vendor detected by string match on `u_asset_mfg` (apple=1, acer=2, lenovo=3, lexmark=4; unknown → Lexmark). Serial 'S'-prefix stripped for Lexmark/Lenovo. Ship-to resolved via `App_Api_NYCDOEV2::resolveShippingAddress()`; on failure a tracking `NYCDOETicket` is recorded but **ASN creation is skipped**. `dedupeKey = vendorId|PO|DOE-part|serialNumber`. Dedup is SELECT-before-INSERT with per-row try/catch (unique index is the race backstop).
|
|
112
113
|
- **Stage 1b (SFTP):** the only source of **non-serialized** items (in practice the Lexmark feed). Serial-less lines of the same PO+part are summed per file into one queue row with `dedupeKey = vendorId|PO|DOE-part|<file token>`. Dedup is INSERT + catch "Duplicate entry" (race-safe).
|
|
114
|
+
- **Per file, in order** (`App_ASNProcessor_{Lenovo,Apple,Lexmark}::processFiles()`): read over plain FTP (`App_FTP`: `ftp_connect`/`ftp_rawlist`/`ftp_get`) → save legacy `Logs.FileLog` (jobs `ASN_LENOVO` / `ASN_APPLE` / `ASN_LEXMARK`) → parse → cache to disk → email → queue `App_Worker::runTask('Utilities/Sftp/DeleteFile/DeleteSftpFile')` (`lenovo.php:176-184`, `apple.php:105-113`, `lexmark.php:102-110`). worker2 `Worker/Utilities/Sftp/DeleteFile.php` then deletes the file over SFTP (phpseclib3). ⚠ The SFTP password travels **inside the SQS message** (source: hardcoded constants in `asnprocessor/*.php`) — see the credentials gotcha.
|
|
115
|
+
- **Delete is gated only on the disk-cache write** (`$filePutContentsResults !== false`), **not on a good parse** — a bad file is still deleted within minutes, and `Logs.FileLog` is the only copy left.
|
|
116
|
+
- **No Acer processor runs.** `asnprocessor/acer.php` is dead code (not called from `legacy_import_asn.php`, never appends to `$output`, never deletes). Acer files do arrive — via CDW on the shared CDW/Lexmark SFTP account — and Lexmark `processFiles()` has **no file-name filter**, so 1.0 processes every Acer file **as Lexmark** (Lexmark vendorId). Proof: 458 `ASN_LEXMARK` FileLog rows with `acer` in `fileName` (2025-08-18 → 2026-09-25), 0 `ASN_ACER`. Any 1.0-vs-2.0 count must compare 1.0 Lexmark against 2.0 Lexmark + Acer.
|
|
117
|
+
- **Vendors upload and forget** — they never re-send or notice if a file sits on the SFTP (Mark, 2026-09-25). A deleted-but-unparsed file is gone unless recovered from `Logs.FileLog`.
|
|
113
118
|
- **Stage 2:** queue → ASN/Items/Units/TrackingNumbers in a transaction. Serialized: one Unit per serial. Non-serialized: the file's quantity is a **cumulative** total — `unsent item qtyOrder = cumulative − SUM(committed qtyOrder)`; post-freeze growth becomes a **fresh item row**, a decrease fires a discrepancy email and changes nothing. Exactly one Unit per serial-less item row regardless of quantity. Failed rows retry 5× then alert `devteam@goagilant.com`.
|
|
114
119
|
- **Stage 3 (THE FREEZE):** creates the NetSuite SO and stamps `AdvanceShippingNoticeItems.netSuiteInternalSalesOrderId`. **Once stamped, that item's `qtyOrder` is immutable** — there is no update path to a sent SO line; growth must be a new item row → new SO line.
|
|
115
120
|
- **Stage 4:** reconciles the NetSuite **PO** (`netSuiteInternalPurchaseOrderId` — not the SO id) and sends serial inventory assignments. Already multi-PO aware (`listPurchaseOrdersCreatedFromSalesOrder`, groups items by PO, stamps each item's `netSuiteInternalPurchaseOrderId`) **but only stamps items where the PO is NULL and never re-stamps** — so an item stamped to PO-A but later *received* on PO-B is never corrected. **TOGA does NOT create NetSuite POs anywhere in the DOE flow.** Stage 3 creates only the SO (and, when needed, a special-order inventory Item); POs are generated inside NetSuite by procurement/special-order. A single SO can therefore be fulfilled across **multiple POs** (back-orders, multi-vendor, re-cut lines) — a legitimate NetSuite outcome, so the durable fix lives downstream (resolve receipts SO-wide), not in preventing PO splits.
|
|
@@ -138,6 +143,7 @@ The 1.0 worker cannot run on a dev/Windows box; use the toga DB MCP + `Logs.API`
|
|
|
138
143
|
- **NetSuite SOAP request/response payloads** are logged in legacy `Logs.API` — filter `sourceJob LIKE '%3_create_installation_ticket%'`; `requestPayload` contains the PO internalId. (The multi-PO case was verified this way: the receipt search for PO 6971859 returned `totalRecords=1`, only `38S0500`.)
|
|
139
144
|
- **`Bridge_NetSuite`** (legacy) has `SalesOrders` / `SalesOrderItems` / `SerialNumbers` but **no `PurchaseOrders` table** — POs are pulled live from the NetSuite API.
|
|
140
145
|
- **Stuck-vs-received check, per ASN item:** compare `listItemReceiptsCreatedFromSalesOrder($stampedSO)` against `listItemReceiptsCreateFromPurchaseOrder($stampedPO)`, dumping each receipt line's `itemName` plus its `inventoryAssignment` serials. If the SO-wide call returns 0 but the stamped PO returns the receipt with the matching serials, it is the cross-wired-stamp regression below.
|
|
146
|
+
- **Legacy `Logs.FileLog` (~47k rows) has no `fileName` index** — only `PRIMARY(id)`, `FileLog_job(job)`, `dtStamp`. Filter by `job` first (uses `FileLog_job`; `ASN_LENOVO` 1,899 / `ASN_LEXMARK` 954 / `ASN_APPLE` 113 rows as of 2026-09-25), then `fileName`. Select `id` only — never `fileData` (longblob) in a scan.
|
|
141
147
|
- **`App_Model_Core_AdvanceShippingNotice` does NOT map `dtCreatedInstallationTicket`** — its `__get` throws "No field or quick query defined for key …". Read that column via **raw SQL**, not the model.
|
|
142
148
|
|
|
143
149
|
### Monitoring the ServiceNow endpoint (worker2 liveness probe — TRUE-80587)
|
|
@@ -157,7 +163,7 @@ All **live** DOE business logic is 1.0. The only other 2.0 code is the in-progre
|
|
|
157
163
|
|
|
158
164
|
## Gotchas
|
|
159
165
|
|
|
160
|
-
- **⚠ SECURITY (cso review 2026-09-18 — NOT fixed, needs its own ticket): live production ServiceNow credentials are hardcoded in committed PHP.** `library/app/api/nycdoev2.php` and `library/app/api/nycdoe.php` hold the ServiceNow **`clientSecret` and `refreshToken` as literals** (stage values too). A test-repo diagnostic script (committed 2026-06-18) embeds the same prod credential — one rotation covers both. Treat as compromised: move them to config/env and **rotate with DOE**. Separately, `Vision_Log.ErrorLog` rows dump `$_SERVER` including live AWS access key id / secret in plain text — its own ticket, same rotation treatment. **No credential values are recorded here or anywhere in the KB.**
|
|
166
|
+
- **⚠ SECURITY (cso review 2026-09-18 — NOT fixed, needs its own ticket): live production ServiceNow credentials are hardcoded in committed PHP.** `library/app/api/nycdoev2.php` and `library/app/api/nycdoe.php` hold the ServiceNow **`clientSecret` and `refreshToken` as literals** (stage values too). A test-repo diagnostic script (committed 2026-06-18) embeds the same prod credential — one rotation covers both. Treat as compromised: move them to config/env and **rotate with DOE**. Separately, `Vision_Log.ErrorLog` rows dump `$_SERVER` including live AWS access key id / secret in plain text — its own ticket, same rotation treatment. Vendor FTP/SFTP credentials are also hardcoded constants in `library/app/asnprocessor/*.php`, and Stage 1b sends the SFTP password in plain SQS message bodies to worker2's delete task. **No credential values are recorded here or anywhere in the KB.**
|
|
161
167
|
|
|
162
168
|
- **⚠ OPEN BUG / NAMED PATTERN — "Lenovo Off-Layout ASN File" (silent column shift + unit collapse).** Investigated 2026-08-11 (read-only; **no code fix exists**). Lenovo sends NYCDOE ASNs in **two different column layouts**, and `library/app/asnprocessor/lenovo.php` picks its column mapping by **column COUNT with no header-name validation** (`:110`, `if ($colCount >= 30`), so the off-layout file is parsed with the wrong mapping and silently mis-read.
|
|
163
169
|
- **The two layouts.** The normal daily feed `DOE-Lenovo-ASN-MM-DD-YYYY.CSV` (~2 MB) has **30** columns and matches the code's mapping. A second file, `DOE-Open-Lenovo-MM-DD-YYYY.csv` (**lowercase** `.csv`, ~45 KB), has **31** columns in a **different order**: `Contact Email` moves from column 29 to column **6**, and a trailing `Full Shipment` column is appended. Everything from `Street Address` onward is therefore read **one column too early**. The "extra comma" repair heuristic at `:119-127` does not catch this (unchanged since March 2026 — commits `3442ddcf`, `6956dfbf`, `864cf073`, so not a regression).
|
|
@@ -262,6 +268,7 @@ All **live** DOE business logic is 1.0. The only other 2.0 code is the in-progre
|
|
|
262
268
|
- Local deep-dive (full WR260236464 case study with record IDs): `doe_service_now_integration.md` in Mark's WWW workspace root; also the `service-now-integration` skill (`asn-import-flow.md`)
|
|
263
269
|
|
|
264
270
|
## Change history
|
|
271
|
+
- 2026-09-29 — Stage 1b file-delete path (delete not gated on parse; SFTP password rides in SQS), Acer-as-Lexmark, FileLog index facts (mhammontree)
|
|
265
272
|
- 2026-09-29 — Assignment-group scoping corrected (inverted `isValidDepartmentName`, three competing group lists) with detail in hold-status-sync; `Logs.API` 30-day retention shipped. (mhammontree)
|
|
266
273
|
- 2026-09-25 — TRUE-82332: cross-link to the close-clobber fix; test-repo copy of the ServiceNow credential noted (mhammontree)
|
|
267
274
|
- 2026-09-25 — Corrected "no 2.0 tenant" (TRUE-80519 2.0 ASN rewrite now exists); DOE-Open-Lenovo marked retired (mhammontree)
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
| [Prudential: order email PDF attachments (which guide goes on which email)](features/order-email-pdf-attachments.md) | 1.0 | Which S3 PDF guide attaches to which Prudential order email by service-request type/stage, and how to change an attachment safely; open before editing any Prude |
|
|
11
11
|
| [Prudential: Service Request Regional Address Validation](features/service-request-address-validation.md) | 2.0 | The prePost interceptor that validates a Prudential service request's deliverToAddress per region (USA/India/Ireland) and rejects a blank customer uuid; open fo |
|
|
12
12
|
| [Prudential: refused REQ alert (401/403 on POST /v2/service-requests)](features/service-request-auth-rejection-alert.md) | 1.0 | The 15-min cron that alerts on refused (401/403) inbound Prudential REQs, where each code is logged, and how to tell "we refused it" from "they never sent it"; |
|
|
13
|
+
| [Prudential: Service Request bundle lookup scoped to the customer's catalog (wrong Dell India provision profile)](features/service-request-bundle-catalog-resolution.md) | 2.0 | Why India REQs link to USA bundles (bundle numbers repeat across catalogs) and the prePost fix that resolves the bundle uuid inside the customer's catalog; open |
|
|
13
14
|
| [Prudential: Service Request rejection alert email](features/service-request-rejection-alert-email.md) | 2.0 | The real-time internal alert email fired when a Prudential SR POST is rejected by validation (400); open for SR rejection alerting or why a rejected REQ did/did |
|
|
14
15
|
| [Prudential: Sales Orders CSV export (TOGa 2.5 Supply)](features/supply-orders-csv-export.md) | 2.0 | The Prudential-only Sales Orders CSV export in toga25-supply: its two queries, the join fixes, and the fan-out/dedupe rule; open before touching the export or i |
|
|
15
16
|
| [Prudential Order Shipped Email — transmit_ordershipped_updates_prudential.php](features/transmit-ordershipped-email.md) | 1.0 | The two-phase Order Shipped transmit+email cron, its stranding flaws, the PDF-fetch-throws root cause, and the overloaded close column; open before touching Pru |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: prudential
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-29
|
|
10
10
|
owners: ["rgirish", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/delllch.php
|
|
@@ -18,6 +18,7 @@ files:
|
|
|
18
18
|
related:
|
|
19
19
|
- ../profile.md
|
|
20
20
|
- dell-asn-units-interceptor.md
|
|
21
|
+
- service-request-bundle-catalog-resolution.md
|
|
21
22
|
---
|
|
22
23
|
Outbound 1.0 worker transmissions to Dell's LCH ITSM via LCHRequestV2, which cron is live, and the two ways an order silently fails to reach Dell; open for Dell send debugging or the V2 payload schema.
|
|
23
24
|
|
|
@@ -101,6 +102,7 @@ Verification plan:
|
|
|
101
102
|
3. After beta passes a full order cycle (New → shipped), confirm the production script is live.
|
|
102
103
|
|
|
103
104
|
## Gotchas
|
|
105
|
+
- **Provision profile comes from the linked bundle (`Bundles.c_provisionProfile`).** If the REQ is linked to the wrong region's bundle, Dell gets the wrong profile — India REQs sent USA `10649405` instead of `74361088` until 2026-09. Cause and fix: [bundle catalog resolution](service-request-bundle-catalog-resolution.md).
|
|
104
106
|
- **⚠ NULL `ServiceRequests.customerId` = order silently never reaches Dell.** The selection query's `INNER JOIN Customers ON Customers.id = ServiceRequests.customerId` drops any REQ whose `customerId` is NULL, so it is never selected, never validated, never POSTed — and the Dell PO's `dtSubmitted` stays NULL forever (not sent, not marked, invisible). This is the sole differentiator behind "Dell has no trace of the REQ though Talos says we sent it": SO/PO/unit creation all succeeded, only transmission was skipped. At one investigation 79 Dell POs were stuck this way, all with `customerId` NULL. The 2.0-side guard in `service-request-address-validation.md` stops NEW customer-less REQs; already-stuck ones need a `customerId` backfill from ship-to country.
|
|
105
107
|
- **Root cause is a TOGa-side mapping gap, not missing SNOW data.** The inbound ServiceNow payload carries **no customer field** at all — `ServiceRequests.customerId` is assigned *inside TOGa* by `generate_sales_and_purchase_orders_from_service_requests.php`, so a NULL means the generation cron failed to derive/assign a region, not that SNOW omitted anything.
|
|
106
108
|
- **The country-split cutover (~2026-07-17) is when this started biting.** Before the split, `transmissions_to_dell_v1.php` had **no customer gate**, so every REQ transmitted regardless. The regional split files added `INNER JOIN Customers` + `WHERE Customers.uuid = '<region>'`, which drops NULL-`customerId` REQs. ~38 REQs stranded this way since the cutover.
|
|
@@ -110,6 +112,9 @@ Verification plan:
|
|
|
110
112
|
- Dell deprecated `LCHRequest`; any script still POSTing to the old route will fail. All four transmission scripts must point at `LCHRequestV2`.
|
|
111
113
|
- The removed fields (e.g. `deviceType`, `lob`, `imageType`) will be rejected by V2 validation — do not reintroduce them.
|
|
112
114
|
|
|
115
|
+
## Change history
|
|
116
|
+
- 2026-09-29 — Noted the provision profile is read from the linked bundle; wrong bundle link sent India REQs the USA profile. (rgirish)
|
|
117
|
+
|
|
113
118
|
## Related
|
|
114
119
|
- Prudential: [Dell ASN units interceptor](dell-asn-units-interceptor.md) (inbound 2.0 counterpart).
|
|
115
120
|
- [Prudential profile](../profile.md).
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: prudential
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-29
|
|
10
10
|
owners: ["rgirish", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Prudential/ServiceRequest.php
|
|
@@ -16,6 +16,7 @@ related:
|
|
|
16
16
|
- clients/prudential/features/dell-asn-units-interceptor.md
|
|
17
17
|
- clients/prudential/features/dell-lch-iop-transmissions.md
|
|
18
18
|
- clients/prudential/features/service-request-rejection-alert-email.md
|
|
19
|
+
- clients/prudential/features/service-request-bundle-catalog-resolution.md
|
|
19
20
|
- ../../../2.0/apps/_underscore/features/model-interceptor-unit-testing.md
|
|
20
21
|
---
|
|
21
22
|
The prePost interceptor that validates a Prudential service request's deliverToAddress per region (USA/India/Ireland) and rejects a blank customer uuid; open for SR address-validation rules or customer-less REQ stranding.
|
|
@@ -26,7 +27,7 @@ The `prePost` interceptor on `_Model_Prudential_ServiceRequest` validates `deliv
|
|
|
26
27
|
## How it works
|
|
27
28
|
|
|
28
29
|
### Key files / entry points
|
|
29
|
-
- `_underscore/Model/Prudential/ServiceRequest.php` — `prePost` interceptor; private methods `validateCustomer()`, `resolveRegion()`, `validateUsaAddress()`, `validateIndiaAddress()`, `validateIrelandAddress()`.
|
|
30
|
+
- `_underscore/Model/Prudential/ServiceRequest.php` — `prePost` interceptor; private methods `validateCustomer()`, `resolveRegion()`, `validateUsaAddress()`, `validateIndiaAddress()`, `validateIrelandAddress()`. After validation, `prePost` also calls `resolveBundlesInCustomerCatalog()` — see [bundle catalog resolution](service-request-bundle-catalog-resolution.md).
|
|
30
31
|
|
|
31
32
|
### Flow
|
|
32
33
|
1. `prePost` fires before each POST to `/v2/service-requests` for the Prudential client.
|
|
@@ -76,6 +77,9 @@ India optional fields (`mobileNumber`, `telephoneNumber`, `customerCode`, `origi
|
|
|
76
77
|
- **`line3` is India-only.** USA and Ireland validators do not check `line3` (it is not in their Dell spec). If Dell starts sending it for USA/Ireland it is silently ignored.
|
|
77
78
|
- **⚠ OPEN (pre-existing HIGH) — unguarded unit access after the validation block throws a 500.** Code that runs *after* the validation block reads `serviceRequestUnits[0]->unit->serialNumber` / `->assetTag` without guarding for a missing `serviceRequestUnits`. A payload with no units throws an **uncaught non-validation `_Exception` → HTTP 500** (which dumps globals in debug mode) and, because it is not an `_Exception_Validation`, it **skips the rejection-alert email** entirely. Fix: guard the unit access (or validate `serviceRequestUnits` presence inside the collected-error block) so it fails as a 400 with an alert instead of a silent 500. Tracked as a follow-up.
|
|
78
79
|
|
|
80
|
+
## Change history
|
|
81
|
+
- 2026-09-29 — prePost now also resolves bundle uuids inside the customer's catalog (separate doc). (rgirish)
|
|
82
|
+
|
|
79
83
|
## Related
|
|
80
84
|
- [Prudential profile](clients/prudential/profile.md)
|
|
81
85
|
- [Dell ASN units interceptor](clients/prudential/features/dell-asn-units-interceptor.md)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Prudential: Service Request bundle lookup scoped to the customer's catalog (wrong Dell India provision profile)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: prudential
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: draft
|
|
9
|
+
updated: 2026-09-29
|
|
10
|
+
owners: ["rgirish"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Prudential/ServiceRequest.php
|
|
13
|
+
- api2/Component/Api/V2/V2.php
|
|
14
|
+
- worker/crons/toga2/prudential/transmissions_to_dell_india.php
|
|
15
|
+
related:
|
|
16
|
+
- service-request-address-validation.md
|
|
17
|
+
- dell-lch-iop-transmissions.md
|
|
18
|
+
- ../profile.md
|
|
19
|
+
- ../../../2.0/apps/api2/features/nested-relationship-writes.md
|
|
20
|
+
---
|
|
21
|
+
Why India REQs link to USA bundles (bundle numbers repeat across catalogs) and the prePost fix that resolves the bundle uuid inside the customer's catalog; open for a wrong Dell provision profile or bundle mis-link.
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
ServiceNow POSTs `/v2/service-requests` with `serviceRequestBundles:[{bundle:{number:"001"}}]` + `customer:{uuid}`. Bundle numbers are **not unique across catalogs**, and api2 matches the nested bundle on number alone, so every India REQ linked to a **USA** bundle. The Dell India cron then sent the USA provision profile **10649405** instead of India's **74361088** (reported by Dell India). Fix: `_Model_Prudential_ServiceRequest::prePost` adds the bundle `uuid` found inside the customer's own catalog. **Status: fix written, reviewed (php-reviewer + cso clean), uncommitted, NOT deployed, NOT tested end to end.**
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
### Data (prod, 2026-09-29)
|
|
29
|
+
- Customer → catalog is `Customers_Catalogs`: USA customer 3 → catalog 2, India 4 → catalog 3, Ireland 5 → catalog 4 (Ireland catalog has no bundles).
|
|
30
|
+
- USA catalog 2 has bundles numbered `001` and `002`. India catalog 3 has `001` (id 105, Knowledge Worker) and `002` (id 106, Developer), both `c_provisionProfile = 74361088`.
|
|
31
|
+
- `transmissions_to_dell_india.php` reads the provision profile from `Bundles.c_provisionProfile` of the REQ's linked bundle. A wrong bundle link = a wrong profile sent to Dell.
|
|
32
|
+
|
|
33
|
+
### Fix — `resolveBundlesInCustomerCatalog()` (private, called in `prePost()` after validation)
|
|
34
|
+
1. For each `serviceRequestBundles[].bundle` that has a `number` and no `uuid`:
|
|
35
|
+
2. Look up `Bundles` by that number inside the customer's catalog (`Customers` → `Customers_Catalogs` → `Bundles`), by `customer.uuid`.
|
|
36
|
+
3. Exactly 1 match → **add** `bundle.uuid`, **keep** `bundle.number` (other scripts/interceptors may read the number).
|
|
37
|
+
4. 0 matches → leave the payload as sent. No reject (developer's choice).
|
|
38
|
+
|
|
39
|
+
### Why adding uuid (and keeping number) works
|
|
40
|
+
- `Core.RecordFields` for record 43 `bundles` has `uuid`, `name`, `number`, `catalogId` all `isIdentifier = 1`, and `Client_Prudential` has no `Apis_RecordFields` overrides for them. So `getForeignKeyValue` (`V2.php` ~7434) searches **number AND uuid together** — only the catalog-correct bundle matches.
|
|
41
|
+
- A nested object holding **only** `uuid` forces `CHILD_POLICY__MATCH` (`V2.php` ~7460). Keeping `number` means the object is not uuid-only; both shapes resolve to the same row here.
|
|
42
|
+
|
|
43
|
+
## Gotchas
|
|
44
|
+
- **Bundle `number` is unique only within a catalog.** Never resolve a bundle by number alone — scope it by the customer's catalog, or send `uuid`.
|
|
45
|
+
- **ServiceNow sends `"003"` for India Developer, but India Developer is `"002"`.** `003` has no match in the India catalog, so the fix leaves it unchanged and api2 still links it to USA bundle id 3. Action: ServiceNow must send `"002"` for India Developer.
|
|
46
|
+
- **The 129 India REQs already sent (2026-05-28 .. 2026-09-25, customer id 4) are NOT re-linked** (decided 2026-09-29). They sit on USA bundles 1 (127) and 3 (2) and were all transmitted to Dell with the USA bundle's profile. Do not "clean them up" without a new decision.
|
|
47
|
+
- No-match does not reject. A REQ with an unknown bundle number for its catalog is still created on whatever api2 matches by number.
|
|
48
|
+
|
|
49
|
+
## Change history
|
|
50
|
+
- 2026-09-29 — Root-caused the USA provision profile on India REQs; added catalog-scoped bundle uuid resolution in prePost (not deployed). (rgirish)
|
|
51
|
+
|
|
52
|
+
## Related
|
|
53
|
+
- [Service Request address validation](service-request-address-validation.md) — the same `prePost` interceptor.
|
|
54
|
+
- [Dell LCH IOP transmissions](dell-lch-iop-transmissions.md) — the India cron that sends the profile.
|
|
55
|
+
- [api2 nested relationship writes](../../../2.0/apps/api2/features/nested-relationship-writes.md) — identifier matching rules.
|
|
56
|
+
- [Prudential profile](../profile.md)
|
|
@@ -17,7 +17,7 @@ project: _Underscore
|
|
|
17
17
|
client: prudential
|
|
18
18
|
type: profile
|
|
19
19
|
status: active
|
|
20
|
-
updated: 2026-09-
|
|
20
|
+
updated: 2026-09-29
|
|
21
21
|
owners: ["jcardinal", "rgirish", "bala", "mhammontree"]
|
|
22
22
|
files: []
|
|
23
23
|
related:
|
|
@@ -35,6 +35,7 @@ related:
|
|
|
35
35
|
- features/order-email-pdf-attachments.md
|
|
36
36
|
- features/itemclasses-device-tier-data.md
|
|
37
37
|
- workflows/store-saml-deploy-topology.md
|
|
38
|
+
- features/service-request-bundle-catalog-resolution.md
|
|
38
39
|
---
|
|
39
40
|
|
|
40
41
|
## Summary
|
package/package.json
CHANGED