toga-ai 1.0.431 → 1.0.433
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/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/model-interceptor-unit-testing.md +67 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -1
- package/knowledge/2.0/apps/worker2/features/netsuite-salesorder-open-orders-sync.md +120 -23
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/prudential/INDEX.md +2 -2
- package/knowledge/clients/prudential/features/dell-lch-iop-transmissions.md +91 -6
- package/knowledge/clients/prudential/features/service-request-address-validation.md +26 -7
- package/package.json +1 -1
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/probe_je_shape.php, test/@dave/fixer.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
19
19
|
| [isFulfillable Propagation Up the SO↔PO Chain](features/fulfillable-item-propagation.md) | `Items.isFulfillable` is a boolean that gates whether a storefront line's **Qty Fulfilled** cell is actionable. | _underscore/Model/Client/Item.php, _underscore/Model/Compass/Item.php, dbchanges2/Core/2026-07-17 - Items isFulfillable RecordField.sql, dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql, dbchanges2/Client/2026-07-17 - ItemsisFulfillable.sql |
|
|
20
20
|
| [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Every ItemFulfillment (IF) now carries an explicit **stage** — picked → packed → shipped — resolved through `ItemFulfillmentStages → ItemFulfillmentStatuses` (m | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderStatus.php, _underscore/Model/Client/SalesOrderItem.php, _underscore/Model/Client/Item.php, _underscore/Model/Client/PurchaseOrderItem.php, library/app/api/toga2.php, dbchanges2/Client/2026-06-30a - BackfillNullStageItemFulfillmentsToShipped.sql, dbchanges2/Client/2026-06-30b - SalesOrderStatusesPickedPacked.sql, dbchanges2/Client/2026-06-30c - ItemFulfillmentStageIdNotNull.sql, dbchanges2/Client_CompassCanada/2026-06-30a - ItemFulfillmentLifecycleAndShippedBackfill.sql |
|
|
21
|
+
| [DB-free unit testing for _underscore model interceptors](features/model-interceptor-unit-testing.md) | `_underscore` shipped with **no** PHPUnit setup (no `composer.json`/`phpunit`; only vendored PhpOffice tests existed). | _underscore/Test/bootstrap.php, _underscore/Test/Prudential/ServiceRequestTest.php |
|
|
21
22
|
| [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php |
|
|
22
23
|
| [_Model::save() vs raw _Query — no atomic conditional update](features/model-save-vs-query-atomic-update.md) | `_Model::save()` is a plain load-then-write ORM primitive and **cannot express an atomic conditional update** (an optimistic-concurrency / row-claim guard such | _underscore/Model.php, _underscore/Query.php |
|
|
23
24
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "DB-free unit testing for _underscore model interceptors"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-24
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Test/bootstrap.php
|
|
13
|
+
- _underscore/Test/Prudential/ServiceRequestTest.php
|
|
14
|
+
related:
|
|
15
|
+
- ../../../../clients/prudential/features/service-request-address-validation.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
`_underscore` shipped with **no** PHPUnit setup (no `composer.json`/`phpunit`; only vendored
|
|
21
|
+
PhpOffice tests existed). This is the first test scaffold in the repo and establishes a runnable,
|
|
22
|
+
**DB-free** pattern for exercising the pure-validation logic inside a model's `prePost` /
|
|
23
|
+
interceptor methods without a database or the full framework bootstrap. It was introduced with the
|
|
24
|
+
Prudential `validateCustomer()` guard (see the service-request address-validation client-feature).
|
|
25
|
+
|
|
26
|
+
## Key files / entry points
|
|
27
|
+
|
|
28
|
+
- `_underscore/Test/bootstrap.php` — the test bootstrap. It **stubs `_Model_Client_ServiceRequest`**
|
|
29
|
+
(the DB-backed base class) and then `require`s the real `Exception/Validation.php` and the real
|
|
30
|
+
model under test, so the guard code runs unchanged with **no DB connection**.
|
|
31
|
+
- `_underscore/Test/Prudential/ServiceRequestTest.php` — 6 regression tests for the private
|
|
32
|
+
`validateCustomer()` guard.
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
1. The bootstrap defines a minimal stub for the model's DB-backed parent
|
|
37
|
+
(`_Model_Client_ServiceRequest`) so the class under test can be loaded without the ORM/DB layer.
|
|
38
|
+
2. It `require`s the **real** `_Exception_Validation` and the **real** model file, so the actual
|
|
39
|
+
production validation code executes — only its persistence base is stubbed.
|
|
40
|
+
3. Private validators (e.g. `validateCustomer()`) are invoked through **PHP reflection**, letting a
|
|
41
|
+
test assert on a single private method's behaviour directly instead of driving a full POST.
|
|
42
|
+
4. Run it with:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
phpunit --bootstrap _underscore/Test/bootstrap.php _underscore/Test
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The initial suite covers the `validateCustomer()` guard: absent `customer`, null `uuid`,
|
|
49
|
+
empty-string `uuid`, whitespace-only `uuid`, error-message content, and a valid `uuid` passing.
|
|
50
|
+
Verified 6/6 passing against the real code.
|
|
51
|
+
|
|
52
|
+
## Gotchas / known issues
|
|
53
|
+
|
|
54
|
+
- **Reflection on private methods is the pattern here** — the interceptor validators are private,
|
|
55
|
+
so tests use `ReflectionMethod::setAccessible(true)`. Keep testing them directly rather than
|
|
56
|
+
making them public just to test them.
|
|
57
|
+
- **Stub only the persistence base, load the real logic.** The value of the pattern is that the
|
|
58
|
+
production validator runs unchanged; do not reimplement the validator in the stub.
|
|
59
|
+
- There is still no `composer.json`/autoloader for the repo — tests rely on the explicit
|
|
60
|
+
`--bootstrap` file to wire up requires. Adding more model tests means extending
|
|
61
|
+
`Test/bootstrap.php` with the stubs that model needs.
|
|
62
|
+
|
|
63
|
+
## Change history
|
|
64
|
+
- 2026-07-24 — Created: first PHPUnit scaffold in `_underscore` (`Test/bootstrap.php` +
|
|
65
|
+
`Test/Prudential/ServiceRequestTest.php`), a DB-free pattern that stubs the model's persistence
|
|
66
|
+
base and reflects into private interceptor validators; seeded with 6 regression tests for the
|
|
67
|
+
Prudential `validateCustomer()` guard. (bala)
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
| [Etilize Item Translation Import](features/etilize-item-translation-import.md) | The abstract worker class `_Worker_Etilize_ItemTranslations` imports **non-English** item text from Etilize into the client's `ItemTranslations` table. | worker2/Worker/Etilize/ItemTranslations.php |
|
|
20
20
|
| [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Monitors/RateEntitlement.php, worker2/Worker/Notification/Email.php, worker2/Worker/Rate.php, dbchanges2/Core/2026-05-21 - Monitors.sql, dbchanges2/Core/2026-06-29a - Rate Entitlement Contract Monitor.sql |
|
|
21
21
|
| [NetSuite ↔ ClickUp / TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Clickup.php, worker2/Worker/Clickup/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, test/@dave/probe_clickup_desc_match.php, test/@dave/test_model_load_behavior.php, dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql, _underscore/Model/Forecast/Opportunity.php, test/@dave/approach/TRUE-80044.md, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
22
|
-
| [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
22
|
+
| [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, worker2/Component/Forecast/Db/Db.php, worker2/Worker/Netsuite/Location.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_amq_invoice_resync_salesorder.js, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
23
23
|
| [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, worker/crons/toga2/forecast2/import_supporting_records.php |
|
|
24
24
|
| [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, _underscore/Model/Client/EmailTemplate.php |
|
|
25
25
|
| [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
|
|
@@ -6,11 +6,15 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-24
|
|
10
10
|
owners: ["dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite/SalesOrder.php
|
|
13
13
|
- worker2/Worker/Netsuite.php
|
|
14
|
+
- worker2/Component/Forecast/Db/Db.php
|
|
15
|
+
- worker2/Worker/Netsuite/Location.php
|
|
16
|
+
- test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js
|
|
17
|
+
- test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_amq_invoice_resync_salesorder.js
|
|
14
18
|
- test/@dave/probe_salesorder_rest_shape.php
|
|
15
19
|
- test/@dave/probe_open_order_lines.php
|
|
16
20
|
- test/@dave/check_so_status.php
|
|
@@ -127,32 +131,105 @@ The table lives in the **`Forecast` schema on the core2 cluster** (reader
|
|
|
127
131
|
sales-order FK column is **`netsuiteSalesOrderInternalId`** (`int unsigned`) — note this is *not* the
|
|
128
132
|
same column name used by `Forecast.Sales` (see the column-name gotcha below).
|
|
129
133
|
|
|
130
|
-
###
|
|
134
|
+
### Backfilling location / backorder / amountDue onto open orders (TRUE-79162 / TRUE-80262)
|
|
131
135
|
|
|
132
|
-
A
|
|
133
|
-
|
|
136
|
+
A backfill of **location, backorder, and amount-due** data onto open orders runs into model/schema
|
|
137
|
+
facts plus NetSuite field realities (all verified against the prod core2 reader + live NetSuite;
|
|
138
|
+
planning/investigation — see change history for what is built vs. planned):
|
|
134
139
|
|
|
135
140
|
- **`_Model_Forecast_OpenOrderItem` (`_underscore/Model/Forecast/OpenOrderItem.php`) does NOT declare
|
|
136
141
|
`locationId` or `quantityBackordered` — even though both columns already exist in the prod
|
|
137
142
|
`Forecast.OpenOrderItems` table** (`locationId` `int unsigned NULL`, `quantityBackordered`
|
|
138
|
-
`decimal(15,4) NULL`).
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
- **`Forecast.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
143
|
+
`decimal(15,4) NULL`). **Writing those two fields through the 2.0 importer requires adding the
|
|
144
|
+
properties to the model first** — the column existing in the DB is not sufficient. (And per the
|
|
145
|
+
change-detection gotcha above, wire any new column into **both** `buildOpenLineRows()` and the
|
|
146
|
+
`syncOpenLines()` diff.)
|
|
147
|
+
- **`Forecast.OpenOrderItems` has NO `amountDue` column** — importing open-order amountDue needs a
|
|
148
|
+
**new column + model field**.
|
|
149
|
+
|
|
150
|
+
#### Why `locationId` is NULL — a missing dimension sync, not an importer bug
|
|
151
|
+
|
|
152
|
+
`Forecast.Locations` is empty/underpopulated in prod, and **`resolveLocationId`
|
|
153
|
+
(`worker2/Component/Forecast/Db/Db.php`) returns NULL on a miss with NO self-heal** — unlike the item
|
|
154
|
+
path, which calls `_Worker_Netsuite_Item::syncItem` on a miss. `resolveLocationId` walks the leaf
|
|
155
|
+
location → root via `netsuiteParentLocationId` up to the top-level warehouse (NetSuite hands back a
|
|
156
|
+
**leaf sub-location** but the dashboard wants the **top-level** one).
|
|
157
|
+
|
|
158
|
+
`Forecast.Locations` is populated **only** by the action `Netsuite/Location/SyncAll`
|
|
159
|
+
(`_Worker_Netsuite_Location::SyncAll`, `worker2/Worker/Netsuite/Location.php`) — a full SuiteQL
|
|
160
|
+
re-pull of `id/name/parent`, **upsert-only, never deletes**. There is **no per-record location
|
|
161
|
+
webhook** (`location` is absent from the AMQ enqueuer `RECORD_TYPE_MAP`) and **no cron**. So if
|
|
162
|
+
`SyncAll` never runs, `Locations` stays empty and **every `locationId` resolves NULL** — the
|
|
163
|
+
"warehouse data missing" class of bug is a missing dimension sync, not the SO importer.
|
|
164
|
+
|
|
165
|
+
**Recommended durable design:** on a `resolveLocationId` miss, call `SyncAll` **once per process**
|
|
166
|
+
(the whole small dimension re-pulls in ~0.9s; a surgical single-row fetch is *worse* because you'd
|
|
167
|
+
have to walk/fetch the entire parent chain anyway). Optionally add per-record location webhook
|
|
168
|
+
methods `post()`/`put()` → `SyncAll`, and `delete()` → **no-op** (deleting the row would orphan
|
|
169
|
+
`OpenOrderItems.locationId`). To populate prod now, enqueue `Netsuite/Location/SyncAll` manually (see
|
|
170
|
+
the [architecture doc](../architecture.md) WorkerJobs pending path: INSERT a `jobType='ACTION'` row
|
|
171
|
+
with `dtQueued` NULL and the JobScheduler Lambda queues it within a minute).
|
|
172
|
+
|
|
173
|
+
#### amountDue on an open SO — the AR field lives on the invoice, not the sales order
|
|
174
|
+
|
|
175
|
+
The amountDue semantics from the prior Sales work (TRUE-78923) do **not** transfer directly: **a
|
|
176
|
+
NetSuite Sales Order REST record has NO amount-remaining / AR field.** A GET of
|
|
177
|
+
`record/v1/salesOrder` returns only `subtotal`/`total`/`totalCostEstimate`; `$salesOrder->amountRemaining`
|
|
178
|
+
**does not exist** and yields NULL — this is the root cause of the empty amountDue column. AR lives
|
|
179
|
+
**only on the invoice**: SuiteQL `transaction.foreignamountunpaid` (equivalently REST invoice
|
|
180
|
+
`amountRemaining` / `amountRemainingTotalBox`). `foreignamountunpaid` is already
|
|
181
|
+
invoice-amount-less-payments (it nets applied payments **and** applied credit memos) and is NULL/0 on
|
|
182
|
+
Paid-In-Full and on cash sales/refunds → wrap `NVL(...,0)`.
|
|
183
|
+
|
|
184
|
+
**Compute an open SO's outstanding AR by aggregating its linked invoices (reusable SuiteQL pattern):**
|
|
185
|
+
|
|
186
|
+
1. SO→invoice linkage is `previoustransactionlinelink`: `previousdoc=<soId>`, `previoustype='SalesOrd'`,
|
|
187
|
+
`nexttype='CustInvc'` → collect **DISTINCT** `nextdoc` = invoice ids. There are **multiple link
|
|
188
|
+
rows per (SO,invoice)** (`linktype` ShipRcpt/OrdBill/OrdRvCom) — you **MUST dedup on `nextdoc`** or
|
|
189
|
+
the AR triples.
|
|
190
|
+
2. `SUM(NVL(transaction.foreignamountunpaid,0))` over those invoice ids.
|
|
191
|
+
3. **Run PER-SO with small `IN`-lists** — the full-book JOIN+GROUP BY over all SOs returns a NetSuite
|
|
192
|
+
**400** (the framework renders an HTML error page).
|
|
193
|
+
4. **TOGA SuiteQL convention: NO table aliases** — fully-qualify (`previoustransactionlinelink.nextdoc`,
|
|
194
|
+
`transaction.foreignamountunpaid`). Verified live penny-exact (26 invoices → openAr $79,448.78).
|
|
195
|
+
5. **Gotcha:** NetSuite **lowercases the SuiteQL output alias** (request `openAr` → response key
|
|
196
|
+
`openar`) — read the result key case-insensitively.
|
|
197
|
+
|
|
198
|
+
**Customer Deposit unapplied balance has NO direct field either — derive it.** `CustDep` records
|
|
199
|
+
expose no remaining/unapplied scalar (`foreignamountunpaid` is NULL on deposits; REST
|
|
200
|
+
`customerDeposit` has no remaining field) — only `foreigntotal` and `status` ("Deposited" = has
|
|
201
|
+
remaining vs "Fully Applied"). Derive: `unapplied = customerDeposit.foreigntotal − SUM(the deposit's
|
|
202
|
+
DepAppl application amounts)`, where the applications are `previoustransactionlinelink`
|
|
203
|
+
`previousdoc=<depId>`, `previoustype='CustDep'`, `nexttype='DepAppl'` → each `DepAppl.foreigntotal`;
|
|
204
|
+
the deposit reaches its SO via the `OrdDep` link (SalesOrd→CustDep). Verified: 79176 − 26392 = 52784.
|
|
205
|
+
|
|
206
|
+
#### Freshness — balance events don't reach the SO's webhook; bridge via an invoice UE
|
|
207
|
+
|
|
208
|
+
A 180-day audit of `Core.WorkerJobs` (`action LIKE 'Netsuite/%'`) shows which record types the AMQ
|
|
209
|
+
enqueuer actually POSTs to `webhook.togahub.com/netsuite`: **SalesOrder, Invoice, Opportunity,
|
|
210
|
+
JournalEntry, CashSale, CreditMemo** (+ item/customer/employee dims). It does **NOT** send
|
|
211
|
+
`customerPayment`, `customerDeposit`, `customerRefund`, or `cashRefund` (all 0 in 180d) — and
|
|
212
|
+
`location` is not in the map. Compounding it: **billing an SO fires no SalesOrder UE** (the saved
|
|
213
|
+
record is the Invoice; submit events don't chain), and **applying a customer payment submits neither
|
|
214
|
+
the invoice nor the SO** — so no balance event reaches the SO's enqueuer. Any SO-derived AR value
|
|
215
|
+
therefore goes stale on billing/payment.
|
|
216
|
+
|
|
217
|
+
**Bridge pattern** (`test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_amq_invoice_resync_salesorder.js`):
|
|
218
|
+
a UE **on the Invoice** resolves the invoice's `createdfrom` (its parent SO — confirmed via SuiteQL
|
|
219
|
+
`type='SalesOrd'`) and enqueues a `salesOrder` **'edit'** webhook → worker2 router →
|
|
220
|
+
`_Worker_Netsuite_SalesOrder::importOpenOrder` re-reads and re-syncs. **To keep any SO-derived value
|
|
221
|
+
fresh across balance events, route those events to a `salesOrder` edit** rather than adding new
|
|
222
|
+
per-type handlers. (This is also the concrete form of the "fix direction" in the billing-not-removed
|
|
223
|
+
gotcha below.)
|
|
224
|
+
|
|
225
|
+
#### Backfilling recomputed columns — delete-and-reconcile beats a backfill script
|
|
226
|
+
|
|
227
|
+
The Forecast reconciler's fingerprint detects record-set drift (n / idsum). **Deleting a whole SO's
|
|
228
|
+
`OpenOrderItems` rows IS that drift** → the SO classifies `nsOnly` → the fixer re-fetches the SO and
|
|
229
|
+
`importOpenOrder` recreates the rows **with the recomputed columns**. So no presence-check or backfill
|
|
230
|
+
script is needed — delete in small batches (a brief gap until recreated). This depends on the recompute
|
|
231
|
+
code being deployed and the reconciler's OpenOrders category being live; the fallback is a **per-SO
|
|
232
|
+
`salesOrder`-edit webhook** (proven to recreate rows *and* resolve `locationId`).
|
|
156
233
|
|
|
157
234
|
## Client variations
|
|
158
235
|
|
|
@@ -180,7 +257,9 @@ None — uniform (platform-wide Forecast2 sync).
|
|
|
180
257
|
silent **false-negative "order missing"** reading. Verify the schema before concluding an order is
|
|
181
258
|
absent from Forecast.
|
|
182
259
|
- **A successful `WorkerJobs` row does NOT prove rows were written — two silent zero-write paths.** An
|
|
183
|
-
`isSuccess=1`
|
|
260
|
+
`isSuccess=1` job is **not** evidence that any `OpenOrderItems` rows persisted. (NB: prod
|
|
261
|
+
`Core.WorkerJobs` has **no `failureReason` column** — it was renamed to `output`, which holds both
|
|
262
|
+
failure text and success result; see the [architecture doc](../architecture.md).)
|
|
184
263
|
`importOpenOrder` has two success-with-zero-write paths:
|
|
185
264
|
- **(a) STATUS GATE** — if NetSuite `status->refName` is not in `OPEN_STATUSES` (`Pending Fulfillment`,
|
|
186
265
|
`Partially Fulfilled`, `Pending Billing/Partially Fulfilled`, `Pending Billing`) or is missing, it
|
|
@@ -336,6 +415,24 @@ test fixture (it surfaced the stale SO 7181316 above).
|
|
|
336
415
|
|
|
337
416
|
## Change history
|
|
338
417
|
|
|
418
|
+
- 2026-07-24 — **Resolved the open questions on the OpenOrderItems location / amountDue Power-BI
|
|
419
|
+
backfill (investigation + planning).** `locationId` is NULL because `Forecast.Locations` is
|
|
420
|
+
unpopulated and `resolveLocationId` (`worker2/Component/Forecast/Db/Db.php`) has no self-heal on a
|
|
421
|
+
miss; the dimension is filled **only** by `Netsuite/Location/SyncAll`, which has no location webhook
|
|
422
|
+
(absent from the AMQ `RECORD_TYPE_MAP`) and no cron — recommended a per-process `SyncAll` self-heal
|
|
423
|
+
(optionally a location webhook → SyncAll; delete → no-op). Root-caused the empty amountDue: **a
|
|
424
|
+
NetSuite SO REST record has no amount-remaining/AR field** (only the invoice does —
|
|
425
|
+
`transaction.foreignamountunpaid`); documented the reusable **per-SO SuiteQL AR rollup** over linked
|
|
426
|
+
invoices (`previoustransactionlinelink` `SalesOrd→CustInvc`, DISTINCT `nextdoc` or AR triples;
|
|
427
|
+
per-SO IN-lists — full-book JOIN 400s; no table aliases; NetSuite lowercases the output alias) and
|
|
428
|
+
the **Customer Deposit unapplied** derivation (`foreigntotal − Σ DepAppl`). Audited the AMQ
|
|
429
|
+
enqueuer's actual record types (180d WorkerJobs: SalesOrder/Invoice/Opportunity/JournalEntry/
|
|
430
|
+
CashSale/CreditMemo; NOT customerPayment/Deposit/Refund/cashRefund; not location) and recorded the
|
|
431
|
+
**invoice-UE → `createdfrom` → salesOrder-edit bridge** (`ue_amq_invoice_resync_salesorder.js`) as
|
|
432
|
+
the freshness fix for balance events that never reach the SO's webhook. Added the
|
|
433
|
+
**delete-and-reconcile backfill technique** (deleting an SO's OOI rows is reconciler drift → fixer
|
|
434
|
+
recreates them with the recomputed columns; no backfill script). Corrected the stale
|
|
435
|
+
`failureReason` reference (column renamed to `output`). (dfranks)
|
|
339
436
|
- 2026-06-29 — **Recorded the model/DB drift + prerequisites for the open-order location/backorder/
|
|
340
437
|
amountDue backfill (TRUE-79162, planning only — no code written).** `_Model_Forecast_OpenOrderItem`
|
|
341
438
|
declares neither `locationId` nor `quantityBackordered` though both columns already exist in prod
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,9 +17,9 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
20
|
+
- **_underscore** (_Underscore) _(framework core)_ — 38 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 31 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
|
-
- **api2** (API) —
|
|
22
|
+
- **api2** (API) — 16 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
24
24
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
25
25
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [Prudential: Dell ASN units PRE/POST interceptor (legacy key + flat tracking)](features/dell-asn-units-interceptor.md) | 2.0 | After the tracking-number bridge migration, the ASN unit route was renamed (`advance-shipping-notice-units` → `advance-shipping-notice-item-units`), so the inhe | _underscore/Model/Prudential/AdvanceShippingNotice.php, dbchanges2/Client_Prudential/2026-06-10 - AsnUnitsInterceptor.sql |
|
|
6
|
-
| [Prudential: Dell LCH IOP transmissions (LCHRequestV2)](features/dell-lch-iop-transmissions.md) | 1.0 | Outbound order transmissions from the 1.0 worker tier to Dell's Lifecycle Hub (LCH) ITSM integration. | library/app/api/delllch.php,
|
|
6
|
+
| [Prudential: Dell LCH IOP transmissions (LCHRequestV2)](features/dell-lch-iop-transmissions.md) | 1.0 | Outbound order transmissions from the 1.0 worker tier to Dell's Lifecycle Hub (LCH) ITSM integration. | library/app/api/delllch.php, library/app/apitransaction.php, worker/crons/toga2/prudential/transmissions_to_dell_usa.php, worker/crons/toga2/prudential/transmissions_to_dell_ireland.php, worker/crons/toga2/prudential/transmissions_to_dell_india.php |
|
|
7
7
|
| [Prudential: Device information import + unit→contact linking (import_device_information.php)](features/device-information-import-and-contact-linking.md) | 1.0 | Prudential's **device-sync** cron (`worker/crons/toga2/prudential/import_device_information.php`) pulls device/asset records (from ServiceNow / the Dell CMDB fe | worker/crons/toga2/prudential/import_device_information.php, worker/crons/toga2/prudential/backfill_unit_contacts.php, dbchanges2/Client_Prudential/2026-07-07 - Contact Dedup Merge.sql |
|
|
8
|
-
| [Prudential: Service Request Regional Address Validation](features/service-request-address-validation.md) | 2.0 | The `prePost` interceptor on `_Model_Prudential_ServiceRequest` validates `deliverToAddress` fields differently depending on which Prudential regional customer | _underscore/Model/Prudential/ServiceRequest.php |
|
|
8
|
+
| [Prudential: Service Request Regional Address Validation](features/service-request-address-validation.md) | 2.0 | The `prePost` interceptor on `_Model_Prudential_ServiceRequest` validates `deliverToAddress` fields differently depending on which Prudential regional customer | _underscore/Model/Prudential/ServiceRequest.php, _underscore/Test/Prudential/ServiceRequestTest.php |
|
|
9
9
|
| [Prudential Order Shipped Email — transmit_ordershipped_updates_prudential.php](features/transmit-ordershipped-email.md) | 1.0 | Cron script that transmits "Order Shipped" updates to ServiceNow (RITM) and sends a shipped notification email to the end user. | worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php, worker/crons/toga2/prudential/transmit_closecomplete_updates_prudential.php, worker/crons/toga2/prudential/transmit_rejected_cancelled_updates_prudential.php, worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/toga2/prudential_beta/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/notifications/reports/prudential_exception_report.php |
|
|
10
10
|
| [Prudential Financial](profile.md) | 2.0 | Prudential is a TOGA client whose device-fulfillment flow is driven by **Dell** via the Dell API (`Client_Prudential.Apis.id = 2`). | |
|
|
11
11
|
| [Prudential: Dell ASN failed POST backfill replay](workflows/dell-asn-backfill-replay.md) | 2.0 | When Dell ASN POSTs fail in bulk (e.g. | |
|
|
@@ -6,14 +6,14 @@ project: Worker
|
|
|
6
6
|
client: prudential
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["rgirish"]
|
|
9
|
+
updated: 2026-07-24
|
|
10
|
+
owners: ["rgirish", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/delllch.php
|
|
13
|
-
-
|
|
14
|
-
- worker/crons/toga2/
|
|
15
|
-
- worker/crons/toga2/
|
|
16
|
-
- worker/crons/toga2/
|
|
13
|
+
- library/app/apitransaction.php
|
|
14
|
+
- worker/crons/toga2/prudential/transmissions_to_dell_usa.php
|
|
15
|
+
- worker/crons/toga2/prudential/transmissions_to_dell_ireland.php
|
|
16
|
+
- worker/crons/toga2/prudential/transmissions_to_dell_india.php
|
|
17
17
|
related:
|
|
18
18
|
- ../profile.md
|
|
19
19
|
- dell-asn-units-interceptor.md
|
|
@@ -31,6 +31,68 @@ only the request route and payload schema changed.
|
|
|
31
31
|
This is the **outbound** counterpart to the 2.0 inbound Dell ASN flow (see
|
|
32
32
|
`dell-asn-units-interceptor.md`).
|
|
33
33
|
|
|
34
|
+
## Which cron is live (READ THIS FIRST)
|
|
35
|
+
|
|
36
|
+
The current **production** Dell transmitters are the three regional split files in
|
|
37
|
+
`worker/crons/toga2/prudential/`: `transmissions_to_dell_usa.php`,
|
|
38
|
+
`transmissions_to_dell_ireland.php`, `transmissions_to_dell_india.php`. They are active in
|
|
39
|
+
`worker/schedules/cron.worker.sync.json`, run every 5 minutes, and are each scoped to one
|
|
40
|
+
region by `Customers.uuid`. Only the split files have the pre-send `validateLCHRequestV2`
|
|
41
|
+
step and Apple-device handling.
|
|
42
|
+
|
|
43
|
+
- The 119 KB monolith `transmissions_to_dell.php` is **DISABLED** — do not debug or edit it.
|
|
44
|
+
- `transmissions_to_dell_v1.php` is a **dead backup** — ignore it.
|
|
45
|
+
- The regional files were formerly under `crons/toga2/prudential_beta/`; they have since been
|
|
46
|
+
promoted to `crons/toga2/prudential/` and are the live production crons.
|
|
47
|
+
|
|
48
|
+
## Selection query (how a REQ becomes a Dell send)
|
|
49
|
+
|
|
50
|
+
Each regional cron selects Dell POs that have not yet been transmitted:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
INNER JOIN Customers ON Customers.id = ServiceRequests.customerId
|
|
54
|
+
WHERE PurchaseOrders.vendorId = 1 # Dell = Vendors.id 1
|
|
55
|
+
AND PurchaseOrders.dtSubmitted IS NULL
|
|
56
|
+
AND Customers.uuid = '<region-uuid>'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- **Region lives on `ServiceRequests.customerId`**, resolved through the join to
|
|
60
|
+
`Client_Prudential.Customers`. Region rows: **USA = id 3, uuid
|
|
61
|
+
`b3f7a2c1-4e89-4d6a-9c3b-8f1e5d2a7b04`**; India and Ireland are separate `Customers` rows /
|
|
62
|
+
uuids. This is NOT the SalesOrder customer — the SO customer is always
|
|
63
|
+
**"Agilant - Tech Hub" (id 1, uuid `36da53f8-38d2-404f-becf-f58f33c215d3`)**.
|
|
64
|
+
- A REQ is only ever transmitted, validated, or marked once it is selected here. Unit/bundle/SO/PO
|
|
65
|
+
creation happens elsewhere and does **not** send anything to Dell.
|
|
66
|
+
|
|
67
|
+
## Send mechanism
|
|
68
|
+
|
|
69
|
+
- `App_Api_Delllch::send` (`library/app/api/delllch.php`) POSTs to route
|
|
70
|
+
`api/v2/request/LCHRequestV2`. Prod host `https://cms-iop.us.dell.com/iopv2/`; non-prod
|
|
71
|
+
`.../iopv2np/`.
|
|
72
|
+
- Auth: OAuth2 client-credentials Bearer token obtained via `auth/token` (credentials stored in
|
|
73
|
+
config, not in this doc).
|
|
74
|
+
- The split files first call `validateLCHRequestV2` (route `api/v2/validate/LCHRequestV2`) before
|
|
75
|
+
the real submit.
|
|
76
|
+
|
|
77
|
+
## Transaction log (where to pull any Dell send for an audit/RCA)
|
|
78
|
+
|
|
79
|
+
Dell's requested transaction record (date/time, payload, response code, response body) lives in
|
|
80
|
+
`Logs_Prudential.Api`:
|
|
81
|
+
|
|
82
|
+
| Column | Meaning |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `dtStamp` | timestamp (indexed) |
|
|
85
|
+
| `direction` | `'OUT'` for our sends to Dell |
|
|
86
|
+
| `method` / `hostname` / `route` | HTTP verb, `https://cms-iop.us.dell.com`, e.g. `.../request/LCHRequestV2` |
|
|
87
|
+
| `requestPayload` / `responseCode` / `responsePayload` | outbound body, HTTP status, Dell response body |
|
|
88
|
+
|
|
89
|
+
Reading it:
|
|
90
|
+
- A row with a `requestPayload` but **NULL `responseCode`** = curl never got an answer.
|
|
91
|
+
- A `/validate/` row with **no** matching `/request/` row = validated but never submitted.
|
|
92
|
+
- **Do not `LIKE`-scan on `requestPayload`** — `Logs_Prudential.Api` is millions of rows and the
|
|
93
|
+
payload is not indexed, so a `requestPayload LIKE '%REQ%'` query times out. Filter by `dtStamp`
|
|
94
|
+
(indexed) + `direction` + exact `route` instead.
|
|
95
|
+
|
|
34
96
|
## How it works
|
|
35
97
|
|
|
36
98
|
- **Route:** `api/v2/request/LCHRequest` → **`api/v2/request/LCHRequestV2`** in all four
|
|
@@ -101,6 +163,23 @@ This is the established pattern for all Prudential script changes.
|
|
|
101
163
|
3. After beta passes a full order cycle (New → shipped), confirm the production script is live.
|
|
102
164
|
|
|
103
165
|
## Gotchas
|
|
166
|
+
- **NULL `ServiceRequests.customerId` = order silently never reaches Dell.** The selection
|
|
167
|
+
query's `INNER JOIN Customers ON Customers.id = ServiceRequests.customerId` drops any REQ whose
|
|
168
|
+
`customerId` is NULL, so it is never selected, never validated, never POSTed — and the Dell PO's
|
|
169
|
+
`dtSubmitted` stays NULL forever (not sent, not marked, invisible). This is the sole
|
|
170
|
+
differentiator behind "Dell has no trace of the REQ though Talos says we sent it": SO/PO/unit
|
|
171
|
+
creation all succeeded, only transmission was skipped. At one investigation 79 Dell POs were
|
|
172
|
+
stuck this way, all with `customerId` NULL. The 2.0-side guard added in
|
|
173
|
+
`service-request-address-validation.md` stops NEW customer-less REQs; already-stuck ones need a
|
|
174
|
+
`customerId` backfill from ship-to country.
|
|
175
|
+
- **`dtSubmitted` is stamped without inspecting the response (latent bug, not yet fixed).** Every
|
|
176
|
+
regional cron sets `PurchaseOrders.dtSubmitted = NOW()` immediately after the send without
|
|
177
|
+
checking Dell's HTTP status, and `App_ApiTransaction::execute()` (`library/app/apitransaction.php`)
|
|
178
|
+
only throws on curl transport failure / empty / non-JSON body — **not** on HTTP >= 400. So a Dell
|
|
179
|
+
4xx/5xx rejection is recorded as "sent", the PO drops out of the `dtSubmitted IS NULL` queue, and
|
|
180
|
+
is never retried — it looks sent internally while Dell never accepted it. `getAccessToken()` also
|
|
181
|
+
does not check the token-call result (silent 401 path). This is a second way an order can silently
|
|
182
|
+
fail to reach Dell; fix later by inspecting the response before stamping.
|
|
104
183
|
- `requestType` is the SR type only (Refresh/Breakfix/Reclaim/Bulk) — do **not** put the event
|
|
105
184
|
there. Event signalling moved to the separate required `requestEvent` field
|
|
106
185
|
(Create/Update/Cancel). Conflating them is the easiest way to reproduce the old overloaded
|
|
@@ -111,6 +190,12 @@ This is the established pattern for all Prudential script changes.
|
|
|
111
190
|
do not reintroduce them.
|
|
112
191
|
|
|
113
192
|
## Change history
|
|
193
|
+
- 2026-07-24 — Documented the live-cron reality (production is the three regional split files under
|
|
194
|
+
`crons/toga2/prudential/`; monolith `transmissions_to_dell.php` disabled, `_v1` dead), the
|
|
195
|
+
selection query, region model on `ServiceRequests.customerId` (USA id 3), the `App_Api_Delllch`
|
|
196
|
+
send route/host/auth, and the `Logs_Prudential.Api` transaction-log location + safe-query rule.
|
|
197
|
+
Recorded two silent-failure gotchas: NULL `customerId` excluded by the inner join (root cause of
|
|
198
|
+
79 stuck Dell POs) and `dtSubmitted` stamped without response inspection (latent bug). (bala)
|
|
114
199
|
- 2026-06-30 — Migrated all Prudential Dell transmissions (3 beta regional + production) and
|
|
115
200
|
`App_Api_Delllch` from deprecated `LCHRequest` to `LCHRequestV2` per Dell's May 2025 IOP
|
|
116
201
|
spec: re-shaped payload (employee/ship/return objects, renamed return-item keys), new
|
|
@@ -6,13 +6,16 @@ project: _Underscore
|
|
|
6
6
|
client: prudential
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["rgirish"]
|
|
9
|
+
updated: 2026-07-24
|
|
10
|
+
owners: ["rgirish", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Prudential/ServiceRequest.php
|
|
13
|
+
- _underscore/Test/Prudential/ServiceRequestTest.php
|
|
13
14
|
related:
|
|
14
15
|
- clients/prudential/profile.md
|
|
15
16
|
- clients/prudential/features/dell-asn-units-interceptor.md
|
|
17
|
+
- clients/prudential/features/dell-lch-iop-transmissions.md
|
|
18
|
+
- ../../../2.0/apps/_underscore/features/model-interceptor-unit-testing.md
|
|
16
19
|
---
|
|
17
20
|
|
|
18
21
|
## Summary
|
|
@@ -27,12 +30,20 @@ back to USA rules.
|
|
|
27
30
|
## Key files / entry points
|
|
28
31
|
|
|
29
32
|
- `_underscore/Model/Prudential/ServiceRequest.php` — `prePost` interceptor; private methods
|
|
30
|
-
`
|
|
33
|
+
`validateCustomer()`, `resolveRegion()`, `validateUsaAddress()`, `validateIndiaAddress()`,
|
|
34
|
+
`validateIrelandAddress()`
|
|
31
35
|
|
|
32
36
|
## How it works
|
|
33
37
|
|
|
34
38
|
1. `prePost` fires before each POST to `/v2/service-requests` for the Prudential client.
|
|
35
|
-
2.
|
|
39
|
+
2. **`validateCustomer()` is the first check** (before `deliverToAddress` / region resolution). It
|
|
40
|
+
rejects the create when `customer.uuid` is missing/null/blank — the test is
|
|
41
|
+
`trim($payload->customer->uuid ?? '') === ''` — by throwing `_Exception_Validation`, which
|
|
42
|
+
`Controller/Index.php` maps to **HTTP 400**. This stops customer-less / unroutable REQs at the
|
|
43
|
+
API boundary. A customer-less REQ that slips through is exactly what strands an order: with a
|
|
44
|
+
NULL `ServiceRequests.customerId` it is silently excluded from every regional Dell
|
|
45
|
+
transmission cron (see `dell-lch-iop-transmissions.md`).
|
|
46
|
+
3. `resolveRegion()` reads `payload->customer->uuid`, escapes it, and queries
|
|
36
47
|
`Client_Prudential.Customers` for the matching `name`. Returns `'India'`, `'Ireland'`, or
|
|
37
48
|
`'USA'` (default for anything unrecognised or when uuid is null).
|
|
38
49
|
3. A `match` expression dispatches to the correct validator.
|
|
@@ -82,9 +93,12 @@ with a space (e.g. `F92 FP83`). No numeric restriction.
|
|
|
82
93
|
|
|
83
94
|
## Gotchas / known issues
|
|
84
95
|
|
|
85
|
-
- **Null UUID
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
- **Null/blank UUID is now REJECTED (changed 2026-07-24).** Previously a missing `customer.uuid`
|
|
97
|
+
silently fell through `resolveRegion()` to the USA default — which is how customer-less,
|
|
98
|
+
unroutable REQs (NULL `ServiceRequests.customerId`) got created and then silently dropped by the
|
|
99
|
+
Dell crons. The new `validateCustomer()` guard rejects a blank uuid with a 400, so region
|
|
100
|
+
fallback to USA now only ever applies to a uuid that is *present but unrecognised*. Already-stuck
|
|
101
|
+
REQs created before this guard still need a `customerId` backfill from ship-to country.
|
|
88
102
|
- **Region check is by `Customers.name` string, not by id.** If the customer name is ever
|
|
89
103
|
changed in the DB (e.g. `'USA'` → `'United States'`), the validator will silently fall
|
|
90
104
|
back to USA rules for all regions. The match arms are `'India'` and `'Ireland'` only;
|
|
@@ -97,5 +111,10 @@ with a space (e.g. `F92 FP83`). No numeric restriction.
|
|
|
97
111
|
their Dell spec). If Dell starts sending it for USA/Ireland it is silently ignored.
|
|
98
112
|
|
|
99
113
|
## Change history
|
|
114
|
+
- 2026-07-24 — Added `validateCustomer()` as the first check in `prePost`: rejects a create when
|
|
115
|
+
`customer.uuid` is missing/null/blank (`trim(... ?? '') === ''`) via `_Exception_Validation`
|
|
116
|
+
(→ HTTP 400), stopping customer-less/unroutable REQs at the boundary. Previously a blank uuid
|
|
117
|
+
silently defaulted to USA, producing NULL-`customerId` REQs that the Dell transmission crons
|
|
118
|
+
drop. Backed by 6 regression tests in `_underscore/Test/Prudential/ServiceRequestTest.php`. (bala)
|
|
100
119
|
- 2026-06-16 — Created: regional address validation (USA/India/Ireland) replacing the single
|
|
101
120
|
USA-only `validateDeliverToAddress` method; region resolved from `customer.uuid` lookup (rgirish)
|
package/package.json
CHANGED