toga-ai 1.0.970 → 1.0.972
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/api-payload-interceptors.md +9 -5
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +6 -1
- package/knowledge/2.0/apps/worker2/features/netsuite-salesorder-outbound-push.md +7 -6
- package/knowledge/2.0/apps/worker2/features/service-request-sales-order-generation.md +9 -7
- package/knowledge/clients/adyen/features/service-request-salesorder-netsuite.md +29 -21
- package/knowledge/clients/adyen/profile.md +4 -6
- package/knowledge/clients/compass-usa/features/cost-centers.md +17 -8
- package/knowledge/clients/compass-usa/features/location-hierarchy-and-sectors.md +5 -2
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
10
|
-
owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal"]
|
|
9
|
+
updated: 2026-10-08
|
|
10
|
+
owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- api2/Controller/Index.php
|
|
@@ -211,7 +211,7 @@ A `postPost`/`postPut` hook that hands work to worker2 (`_Worker::runTask`) sits
|
|
|
211
211
|
5. **The pattern that works:**
|
|
212
212
|
- Read the record **inside the same request** with `internalApiRequest`, wrapped in `getIsReadHostEnabled()` / `setIsReadHostEnabled(false)` and restored in a **`finally`** (pin the read to the writer, then put the flag back for everyone else).
|
|
213
213
|
- **Superseded 2026-10-05 for queueing: pass only the uuid, never the object** — a nested record can exceed the SQS 256KB cap and the job is lost. The worker reads the record itself with a bounded wait for the commit. Rule + reference: [runTask parameters are ids only](../../worker2/features/creating-worker-actions.md#-runtask-parameters-are-idsuuids-only--never-record-objects). (The Elite sales-order push still carries `?object $salesOrder` — legacy.) The writer-pinned read stays correct when the hook itself needs the record.
|
|
214
|
-
- Pass **`false`** for `internalApiRequest`'s 5th argument (`$throwExceptionsOnError`). Its throw is guarded by `!empty($this->response->messages)` on the **shared** response object, and `json_encode([])` is the **truthy** string `"[]"` — so it throws even with **zero real errors** once any message (even a warning) is present anywhere in the request. **Not theoretical:** it took Elite's production `POST /v2/sales-orders` down on 2026-08-20, from the *parent* model's own `internalApiRequest` (see [the inherited-chain section](#-activating-a-clients-first-interceptor-row-also-switches-on-the-whole-inherited-chain)). The throw site is `V2.php
|
|
214
|
+
- Pass **`false`** for `internalApiRequest`'s 5th argument (`$throwExceptionsOnError`). Its throw is guarded by `!empty($this->response->messages)` on the **shared** response object, and `json_encode([])` is the **truthy** string `"[]"` — so it throws even with **zero real errors** once any message (even a warning) is present anywhere in the request — **including the OUTER request's own warnings** collected before the internal call (e.g. `WO-1`, request depth below an interceptor row's `minDepth`; the worker posts at `depth -1`). **Not theoretical:** it took Elite's production `POST /v2/sales-orders` down on 2026-08-20, from the *parent* model's own `internalApiRequest` (see [the inherited-chain section](#-activating-a-clients-first-interceptor-row-also-switches-on-the-whole-inherited-chain)). The throw site is `V2.php` ~L2436 (open bug, 2026-10-08, needs its own ticket).
|
|
215
215
|
|
|
216
216
|
**Two more failure modes on this path:**
|
|
217
217
|
- **Config drift kills it before it starts.** `_Worker::runTask`'s non-debug path reads `[cloud] aws_worker_queue_url` / `aws_worker_queue_region`, and `_Config::__callStatic` **throws on a missing key even inside a group that exists** — an uncaught throw from `postPost` surfaces as **HTTP 500 `EO-1` with a null envelope and no log row**. See [_Config group access](../../_underscore/features/config-group-access.md).
|
|
@@ -225,10 +225,10 @@ A client whose `ApiPayloadInterceptors` set is **empty** has never run `postPost
|
|
|
225
225
|
1. Elite's `postPost` calls `parent::postPost()` first (correctly — approvals must still be created);
|
|
226
226
|
2. the parent, `_underscore/Model/Client/SalesOrder.php:359`, reads `/approval-templates` via `internalApiRequest` with **throwing left ON**;
|
|
227
227
|
3. Elite has **zero `ApprovalTemplates`** rows, so that nested GET returns empty and puts a message on the **shared** response object;
|
|
228
|
-
4. `V2.php
|
|
228
|
+
4. `V2.php` ~L2436 does `json_encode(array_filter($messages, ...ERROR...))` → the string `"[]"` → **truthy** → throw, with no real error in it;
|
|
229
229
|
5. the controller catches it, returns `EO-1`, and **rolls back** the write.
|
|
230
230
|
|
|
231
|
-
**Reading the signature:** an exception whose **message is `[]`** on a write route means *this* — a message-array truthiness throw. Nothing is wrong with the payload, and the client's own interceptor code may never have executed. **Two fixes, both in shared code:** pass **`false`** as `internalApiRequest`'s 5th argument in the **parent** (narrow, per-caller); or fix **`V2.php
|
|
231
|
+
**Reading the signature:** an exception whose **message is `[]`** on a write route means *this* — a message-array truthiness throw. Nothing is wrong with the payload, and the client's own interceptor code may never have executed. **Two fixes, both in shared code:** pass **`false`** as `internalApiRequest`'s 5th argument in the **parent** (narrow, per-caller); or fix **`V2.php` ~L2436** to look only at messages added **after `$savedMessageCount`** and throw only when that filtered **array** has a real `TYPE_ERROR` — which fixes **every** caller on this path. Senior-owned: raise it rather than patching one caller.
|
|
232
232
|
|
|
233
233
|
**Before activating a row for a client that has none:** walk the parent chain for the derived method and confirm the data each inherited step depends on actually exists for that tenant (here: at least one `ApprovalTemplates` row). Same reason activation must be staged `isActive = 0` first — see Gotchas.
|
|
234
234
|
|
|
@@ -263,6 +263,7 @@ The reverse direction: not "queue a worker from an interceptor" but **"run the p
|
|
|
263
263
|
- **⚠ A throw from a PRE interceptor leaves NO `Logs_<Client>.Api` row.** The transaction log is written near the END of `_Component_Api_V2::execute()` (~L2225-2271), *after* route processing (~L1742-2043), so an exception raised from `prePost` never reaches it and the rejected request looks like it never arrived. See [V2 request logging](./request-logging.md#-a-throw-from-a-pre-interceptor-leaves-no-api-log-row-at-all).
|
|
264
264
|
- **⚠ Client behaviour never goes on `_Model_Client_X`.** Every tenant with a row for that record inherits it through `parent::` calls. Put it on `_Model_<Client>_X`.
|
|
265
265
|
- **A client's rows may be in `Core` (with `clientId`), not in `Client_<X>`** — check both before concluding a client has no interceptors.
|
|
266
|
+
- **⚠ The same hook in BOTH `Core` (with `clientId`) and `Client_<X>` fires TWICE** — both tables are read, nothing dedupes. Adyen (2026-10-08): Core ids 15–17 duplicated client rows 1, 2, 5 → double worker jobs and double NetSuite queueing; fixed by deactivating the Core rows. Keep each `(record, phase, method)` in one table only. See [Adyen reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md#gotchas).
|
|
266
267
|
- **The method name is derived, so a typo'd enum silently misses.** A row with `prePostProcessing = 'PRE'`, `httpMethod = 'PUT'` resolves to `prePut`, not `prePost`; the engine finds no method and moves on.
|
|
267
268
|
- **Registration is per-environment.** Like `RecordFields` / `RecordScripts` / ACL rows, this is metadata — "works in prod, not in beta/dev-sandbox" is the signature of drift, not of a bad deploy. See the [non-prod metadata drift repair workflow](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
|
|
268
269
|
- **⚠ An in-request read-after-write in an interceptor works on beta and fails 100% in production.** Reads default to the **reader** connection, which cannot see the request's own uncommitted row; dev-sandbox points writer and reader at the same endpoint, so it hides the bug. Pin with `setIsReadHostEnabled(false)` (restore in `finally`).
|
|
@@ -287,3 +288,6 @@ The reverse direction: not "queue a worker from an interceptor" but **"run the p
|
|
|
287
288
|
- [Email template sending](../../_underscore/features/email-template-sending.md)
|
|
288
289
|
- [Elite SalesOrder → NetSuite push](../../../clients/elite/features/salesorder-netsuite-push.md)
|
|
289
290
|
- [Compass sales-order line renumbering](../../../clients/compass-usa/features/sales-order-line-renumbering.md)
|
|
291
|
+
|
|
292
|
+
## Change history
|
|
293
|
+
- 2026-10-08 — `internalApiRequest` "[]" throw also fires on the OUTER request's warnings (e.g. WO-1); throw site now ~L2436; Core + client duplicate rows fire twice. (rgirish)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-08
|
|
10
10
|
owners: [jcardinal, dfranks, mhammontree, tcox, bala, ajean, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/
|
|
@@ -151,6 +151,7 @@ in the deployed worker code, so ship the code deploy and the dbchanges2 cron-row
|
|
|
151
151
|
```bash
|
|
152
152
|
curl -X POST https://worker.togahub.com/ \
|
|
153
153
|
-H "Content-Type: application/json" \
|
|
154
|
+
-H "Authorization: <worker key>" \
|
|
154
155
|
-d '{
|
|
155
156
|
"action": "Category/MyAction/DoSomething",
|
|
156
157
|
"parameters": { "param1": "example_value", "count": 5 }
|
|
@@ -159,6 +160,9 @@ curl -X POST https://worker.togahub.com/ \
|
|
|
159
160
|
|
|
160
161
|
Use realistic placeholder values, not empty strings/nulls.
|
|
161
162
|
|
|
163
|
+
- Needs the worker key in `Authorization` (never paste the value into docs, tickets or chat).
|
|
164
|
+
- Runs **synchronously** and creates **no `Core.WorkerJobs` row** — the SQS path (`_Worker::runTask` → `WorkerProductionQueue`) is the only one that logs a job. Use it to re-run a single action (e.g. one service request); read the result from the HTTP reply.
|
|
165
|
+
|
|
162
166
|
### The HTTP reply is PREFIXED — never `JSON.parse` it raw
|
|
163
167
|
|
|
164
168
|
The dispatcher returns your action's return value **wrapped in the same composed success
|
|
@@ -539,6 +543,7 @@ Rules: rename **both sides in one PR and one deploy**; grep every repo for the o
|
|
|
539
543
|
commit-before-SQS transaction pattern that the worker relies on.
|
|
540
544
|
|
|
541
545
|
## Change history
|
|
546
|
+
- 2026-10-08 — Direct POST needs the worker key in `Authorization`, runs synchronously, writes no `WorkerJobs` row. (rgirish)
|
|
542
547
|
- 2026-10-06 — Absent-row section: a 200 + MessageId send can still produce no row for up to 1h (failed first delivery hidden by the visibility timeout). (ajean)
|
|
543
548
|
- 2026-10-05 — Rule: `runTask` parameters are ids/uuids only; worker reads with bounded wait; producer catch must `_Error::captureException`. (jcardinal)
|
|
544
549
|
- 2026-10-05 — Added read-host read-after-write gotcha for worker actions (TRUE-82185) (mhammontree)
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["bala", "jcardinal"]
|
|
9
|
+
updated: 2026-10-08
|
|
10
|
+
owners: ["bala", "jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite/SalesOrder.php
|
|
13
13
|
- test/@Bala/tests/netsuite_salesorder_payload_tests.php
|
|
@@ -105,10 +105,11 @@ Four helpers gained a transfer-order branch, each **defaulting to the old behavi
|
|
|
105
105
|
`fetchSalesOrder($ctx, $uuid, $isTransferOrder = false)`, `writeBackNetSuiteId()` (route), and
|
|
106
106
|
`buildNetSuiteOrder()`, which now prefers an explicit `$order->otherRefNum` **before** the
|
|
107
107
|
ticket-number → service-request-number chain. **`CreateNetSuite` / `UpdateNetSuite` / `Sync` keep
|
|
108
|
-
their original signatures
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
their original signatures**. `_Model_Client_SalesOrder` declares **no `otherRefNum` field**, but the
|
|
109
|
+
branch **is** reachable for a sales order when the caller sets it on the queued snapshot: since
|
|
110
|
+
2026-10-08 `_Model_Adyen_SalesOrder::queueNetsuiteCreate()` sets `otherRefNum` = employee name for
|
|
111
|
+
SR-generated orders (see [Adyen reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md)).
|
|
112
|
+
When you add a precedence like that, check every producer of the snapshot, not only the model fields.
|
|
112
113
|
|
|
113
114
|
### The job carries the order snapshot — the worker does not re-read it
|
|
114
115
|
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-08
|
|
10
10
|
owners: ["snaredla", "bala", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Sync/ServiceRequest.php
|
|
@@ -37,9 +37,9 @@ _Worker_Sync_ServiceRequest::GenerateSalesOrderFromServiceRequest(
|
|
|
37
37
|
): string
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Queued from a client's `service-requests` `postPost` interceptor; invocable directly for testing like any worker action. Consumers: **Elite** ([desk intake](../../../clients/elite/features/togadesk-service-request-intake.md)) and **Adyen** ([reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md)
|
|
40
|
+
Queued from a client's `service-requests` `postPost` interceptor; invocable directly for testing like any worker action. Consumers: **Elite** ([desk intake](../../../clients/elite/features/togadesk-service-request-intake.md)) and **Adyen** ([reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md)).
|
|
41
41
|
|
|
42
|
-
**Per-client config = `SALES_ORDER_DEFAULTS_BY_CLIENT`**, keyed by `Core.Clients.clientIdentifier`: `locationUuid` (required — NetSuite rejects an order with no location) + `
|
|
42
|
+
**Per-client config = `SALES_ORDER_DEFAULTS_BY_CLIENT`**, keyed by `Core.Clients.clientIdentifier`: `locationUuid` (required — NetSuite rejects an order with no location) + `shippingMethodId` (fallback when the SR names none). Elite → location `3ca47b91-…`, ship method 1; Adyen → location `191c75ff-f02e-4dda-abc6-c8d26c35da62` (`Locations` id 1 → NS location 70), ship method **5** (Ground). Sales order type is one constant for every client: drop-ship `5994b81d-…` (`SALES_ORDER_TYPE_UUID`). **A client missing from the map throws before any API call** — onboarding a client to this worker = add a map entry with **that tenant's own** row ids (verify the location has `c_netsuiteInternalLocationId`). The uuids are the same in beta and prod.
|
|
43
43
|
|
|
44
44
|
**Proven end to end on dev-sandbox** (`https://api.dev.sandbox.togahub.com/v2`): one service request produced **SA100004** plus purchase order **P1**, both bridge rows linked, and a **rerun returned early** rather than duplicating.
|
|
45
45
|
|
|
@@ -55,8 +55,8 @@ Steps:
|
|
|
55
55
|
1. **Resolve the tenant.** `getClientContextFromCore()` looks up by `Core.Clients.clientIdentifier`, registers the client DB connection, returns Toga API credentials. (Deliberate local copy of the same private in `_Worker_Netsuite_SalesOrder`; lift to a shared parent when a third caller appears.)
|
|
56
56
|
2. **Guard.** `fetchServiceRequestFromToga2()` joins `SalesOrders` on `serviceRequestId`. If a sales order already exists, **return early** with a summary — makes the action idempotent under SQS redelivery and manual reruns. A request already in the **`Exception`** stage is skipped for the same reason.
|
|
57
57
|
3. **Reconcile the requester's address** (`reconcileRequesterAddressInToga2`) and **opt the requester into Order Updates** (`optInRequesterToOrderUpdatesInToga2`).
|
|
58
|
-
4. **Build and POST the sales order** (`buildSalesOrderPayloadForToga2`): customer by uuid, `serviceRequestId`, `ticketId`, `
|
|
59
|
-
5. **Generate purchase orders** (`tryGeneratePurchaseOrdersInToga2` → `generatePurchaseOrdersInToga2`): re-read created lines with `fetchSalesOrderItemsFromToga2()`, resolve a vendor per item with `fetchVendorItemsFromToga2()`, group lines by vendor, POST **one PO per vendor** with line numbers restarted per order. Items with no vendor are listed in the job result as `No vendor for: …` — the job does **not** fail
|
|
58
|
+
4. **Build and POST the sales order** (`buildSalesOrderPayloadForToga2`): customer by uuid, `serviceRequestId`, `ticketId`, `location` from the client's map, drop-ship type, `shippingMethodId` = the SR's, else the client's map value, and `shipToAddress` **only when the request has a deliverable address**. Lines: one per SR **unit** (qty 1); if the SR has no units, one per **bundle item** from `fetchServiceRequestBundleItemsFromToga2()` (`ServiceRequests_Bundles` → `BundleItems` → `Items`, `BundleItems.isActive = 1`, `BundleItems.serviceRequestTypeId` NULL or equal to the SR's type, qty = `BundleItems.quantity`). Price 0, `lineNumber` assigned by the worker. No units and no bundle items → throws (no empty order).
|
|
59
|
+
5. **Generate purchase orders — units path only.** A bundle-only SR gets **no** PO (result: `No purchase orders: the Service Request has no units.`). (`tryGeneratePurchaseOrdersInToga2` → `generatePurchaseOrdersInToga2`): re-read created lines with `fetchSalesOrderItemsFromToga2()`, resolve a vendor per item with `fetchVendorItemsFromToga2()`, group lines by vendor, POST **one PO per vendor** with line numbers restarted per order. Items with no vendor are listed in the job result as `No vendor for: …` — the job does **not** fail.
|
|
60
60
|
6. **On a rejected sales-order POST**, `moveServiceRequestToExceptionInToga2()` parks the request in the `Exception` stage so the failure is visible *and* a redelivered job short-circuits on the step-2 stage guard.
|
|
61
61
|
|
|
62
62
|
### Vendor resolution replaces Prudential's hardcoded vendor
|
|
@@ -93,8 +93,9 @@ The three structural checks (string resolves → producer matches → intercepto
|
|
|
93
93
|
- **⚠ Before nesting a child collection in a payload, confirm the child is registered in `Core.InherentRecordChildren` under that parent record.** `salesOrderItemsCommittedUnits` was briefly nested under sales-order lines; it is **impossible** — record 152 has **zero** `Core.InherentRecordChildren` parent registrations under record 15, nothing writes that table via the API at all, and its model's `unitId` is a foreign key to `_Model_Client_User`, not to a unit. A nested key the API does not recognize is not obviously rejected; run the `InherentRecordChildren` check rather than assuming.
|
|
94
94
|
- **`shipToAddress` carries `childPolicy CREATE`** — posting an all-null address shape **creates a blank address row**. `hasDeliveryAddressForServiceRequest()` omits the key entirely instead.
|
|
95
95
|
- **Line numbers are assigned by the worker and re-checked.** After the POST the worker maps created lines back by `lineNumber`; a line it cannot map means the API renumbered them, and that is **reported** rather than silently dropped from the PO.
|
|
96
|
-
- **⚠ Do not add Prudential to the map without a review.**
|
|
97
|
-
-
|
|
96
|
+
- **⚠ Do not add Prudential to the map without a review.** Bundle items become plain SO lines with no PO — not Prudential's `BundleItems.preferredVendorId` / Dell flow. Prudential is not in `SALES_ORDER_DEFAULTS_BY_CLIENT`, so a run for it throws.
|
|
97
|
+
- **⚠ Map ids are per-tenant row ids — never copy one client's entry.** Elite's ids broke Adyen: `ShippingMethods` id 1 does not exist in `Client_Adyen` (FK failure), and Adyen `Locations` id 2 has no `c_netsuiteInternalLocationId` (NetSuite push failed).
|
|
98
|
+
- **`SalesOrders.serviceRequestId` is the only duplicate guard.** A NetSuite order entered by hand has it NULL, so re-running an old SR whose order was hand-made creates a **second** order. Link or skip those SRs first (Adyen backlog: [reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md#gotchas)).
|
|
98
99
|
- **⚠ Inbound NetSuite sync can duplicate an order this worker created (all clients, open).** `library/app/api/toga2.php:1002` matches sales orders only by `c_netsuiteInternalSalesOrderId`. If the inbound sync runs after NetSuite has the order but before the push writes the id back, it imports a second copy.
|
|
99
100
|
- **Placement:** cross-system sync workers live under `worker2/Worker/Sync/`. This worker does **not** belong in `Worker/Netsuite/` (which keeps the NetSuite push, `Worker/Netsuite/SalesOrder.php`) and not in `Worker/Client/`.
|
|
100
101
|
- **⚠ Renaming this action is a TWO-REPO, single-deploy change.** The method name here and the `runTask(action: 'Sync/ServiceRequest/GenerateSalesOrderFromServiceRequest', …)` **string** in `_underscore/Model/Elite/ServiceRequest.php:44` must land and deploy **together** — the rename from `GenerateSalesOrder` on 2026-08-12 spans both. Ship either alone and the dispatcher targets a name that no longer exists; because `postPost` wraps `runTask` in try/catch + `error_log`, Sales Order generation then stops **silently**. Same exposure for `Sync/SalesOrderStatus/PostSalesOrderStatusToTogadesk1`. See [creating worker actions](./creating-worker-actions.md).
|
|
@@ -112,6 +113,7 @@ Recorded so nobody re-debugs them as bugs. All four confirmed against Elite's da
|
|
|
112
113
|
- The worker is otherwise complete; these are data/config gaps for the client to close.
|
|
113
114
|
|
|
114
115
|
## Change history
|
|
116
|
+
- 2026-10-08 — Map gains `shippingMethodId` (replaces hardcoded id 1); bundle lines filtered by `isActive` + SR type, qty from `BundleItems.quantity`; no POs for bundle-only SRs. (rgirish)
|
|
115
117
|
- 2026-09-30 — Per-client `SALES_ORDER_DEFAULTS_BY_CLIENT` (sets `locationId`, unknown client throws); bundle-item lines when an SR has no units; no-vendor items reported, not failed; Adyen added as second consumer. Not deployed. (rgirish)
|
|
116
118
|
|
|
117
119
|
## Related
|
|
@@ -5,8 +5,8 @@ repo: _underscore
|
|
|
5
5
|
project: _Underscore
|
|
6
6
|
client: adyen
|
|
7
7
|
type: client-feature
|
|
8
|
-
status:
|
|
9
|
-
updated: 2026-10-
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-08
|
|
10
10
|
owners: ["rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Adyen/ServiceRequest.php
|
|
@@ -22,38 +22,46 @@ related:
|
|
|
22
22
|
- ../profile.md
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
Adyen's Reclaim SR → SO → NetSuite chain (same as Elite, but items come from a seeded bundle because Adyen sends no serials); open for the prePost bundle attach, the
|
|
25
|
+
Adyen's Reclaim SR → SO → NetSuite chain (same as Elite, but items come from a seeded bundle because Adyen sends no serials); open for the prePost bundle attach, the setup migration, the employee-name NetSuite PO #, the Core/client interceptor double-fire, or the backlog SR↔NS order map.
|
|
26
26
|
|
|
27
27
|
## Summary
|
|
28
28
|
|
|
29
|
-
Adyen's API (`Apis` id 4) posts Service Requests, all of type **"Reclaim"**, and **cannot send serial numbers**.
|
|
29
|
+
Adyen's API (`Apis` id 4, used by ServiceNow) posts Service Requests, all of type **"Reclaim"**, and **cannot send serial numbers**. Chain = Elite's: SR `postPost` → worker2 builds the Sales Order → SO `postPost` → NetSuite push. Adyen differences: items come from a seeded **"Reclaim" bundle** attached in `prePost`, and the NetSuite PO # is the **employee name**.
|
|
30
30
|
|
|
31
|
-
**Status (2026-10-
|
|
31
|
+
**Status (2026-10-08):** code committed in worker2 + `_underscore`; prod `Client_Adyen.ApiPayloadInterceptors` rows 1, 2, 5 active; prod Core duplicates removed (see Gotchas).
|
|
32
32
|
|
|
33
33
|
## How it works
|
|
34
34
|
|
|
35
|
-
1. `_Model_Adyen_ServiceRequest::prePost` —
|
|
36
|
-
2. `_Model_Adyen_ServiceRequest::postPost` —
|
|
37
|
-
3.
|
|
38
|
-
4.
|
|
35
|
+
1. `_Model_Adyen_ServiceRequest::prePost` — type `Reclaim` with **no units and no bundles** → adds `serviceRequestBundles: [{bundle:{uuid:'23717330-7672-4590-9e3d-7f4097c18723'}}]` (`RECLAIM_BUNDLE_UUID`).
|
|
36
|
+
2. `_Model_Adyen_ServiceRequest::postPost` — queues `Sync/ServiceRequest/GenerateSalesOrderFromServiceRequest`.
|
|
37
|
+
3. Worker turns the bundle items into SO lines; location + ship method come from Adyen's map entry (`Locations` id 1 → NS location 70 "Adyen"; `ShippingMethods` id 5 Ground). No PO for bundle-only SRs — see [SR → SO generation](../../../2.0/apps/worker2/features/service-request-sales-order-generation.md).
|
|
38
|
+
4. `_Model_Adyen_SalesOrder::postPost` → `queueNetsuiteCreate()` reads the order back (writer-pinned, `throwExceptionsOnError = false`), and **for SR-generated orders sets `otherRefNum` = employee name** from the SO contact, then queues `Netsuite/SalesOrder/CreateNetSuite` ([shared push](../../../2.0/apps/worker2/features/netsuite-salesorder-outbound-push.md)), which prefers `otherRefNum` over ticket/SR number.
|
|
39
|
+
- `buildEmployeeName()`: Adyen often sends the full name in **both** first and last name; if they match (case-insensitive) it uses one copy.
|
|
39
40
|
|
|
40
|
-
**Why a bundle, not units:** a unit with only an item (no serial) matches an **existing** unit, so every SR would link the same unit — see [nested writes](../../../2.0/apps/api2/features/nested-relationship-writes.md). `ServiceRequests_Bundles.bundleId` is `MATCH` (links only)
|
|
41
|
+
**Why a bundle, not units:** a unit with only an item (no serial) matches an **existing** unit, so every SR would link the same unit — see [nested writes](../../../2.0/apps/api2/features/nested-relationship-writes.md). `ServiceRequests_Bundles.bundleId` is `MATCH` (links only).
|
|
42
|
+
|
|
43
|
+
### Decisions (PM, 2026-10-08)
|
|
44
|
+
- Reclaim bundle = `SVC-FS-OFFBOARD-ADN` (NS 1579821) + **`Misc Laptop`** (NS 1545437), both $0. Keep Misc Laptop — **not** "Apple Macbook 14in" (NS 1625438).
|
|
45
|
+
- NetSuite PO # (`otherrefnum`) = employee name, matching the hand-entered offboarding orders so ops can search NetSuite by name.
|
|
41
46
|
|
|
42
47
|
### Setup migration — `dbchanges2/Client_Adyen/2026-09-30a - ServiceRequestSalesOrderSetup.sql`
|
|
43
|
-
- `ApiPayloadInterceptors`: record 35 (`service-requests`) PRE+POST / POST,
|
|
44
|
-
- Items
|
|
45
|
-
|
|
46
|
-
-
|
|
48
|
+
- `ApiPayloadInterceptors`: record 35 (`service-requests`) PRE+POST / POST, `apiId` 4 only; record 14 (`sales-orders`) POST/POST and POST/PUT, `apiId` NULL (the worker writes as a different API identity).
|
|
49
|
+
- Items, "Reclaim" bundle + 2 `BundleItems`, Location "Adyen" (NS 70), FedEx Ground (NS 318627), `Customers.c_netsuiteInternalCustomerId = 31054`.
|
|
50
|
+
|
|
51
|
+
These copy Adyen's hand-made NetSuite reclaim orders (e.g. 290879, 288260, 279871).
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
### Backlog — 8 Reclaim requests ↔ NetSuite SOs (2026-10-08)
|
|
54
|
+
NS SOs: 290879, 292873, 292882, 292874, 292875, 292876, 288261, 292872. **290879 and 288261 were hand-entered and are NOT linked to their SRs in Toga** (`SalesOrders.serviceRequestId` NULL).
|
|
49
55
|
|
|
50
56
|
## Gotchas
|
|
51
57
|
|
|
52
|
-
- **⚠
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
- **
|
|
56
|
-
- **Open —
|
|
58
|
+
- **⚠ Never re-run the worker for an SR whose NS order was hand-entered.** The worker's only duplicate guard is `SalesOrders.serviceRequestId`; hand-made orders have it NULL, so a re-run creates a second order (and a second NetSuite push). Link the SR first.
|
|
59
|
+
- **⚠ Adyen hooks ran TWICE until 2026-10-08.** Prod `Core.ApiPayloadInterceptors` ids 15, 16, 17 (`clientId 48`, `minDepth 1`, records 35 + 14) duplicated `Client_Adyen` rows 1, 2, 5 — double worker jobs, and would double-queue NetSuite pushes. Their `minDepth 1` vs the worker's `depth -1` also raised warning **WO-1**, which trips the `internalApiRequest` `"[]"` throw (see [interceptors](../../../2.0/apps/api2/features/api-payload-interceptors.md)). Deactivated; the client rows still fire. Never re-activate them.
|
|
60
|
+
- **⚠ Deploy order: worker2 → `_underscore` → SQL.** SQL first registers a PRE row whose `prePost` does not exist yet → every Adyen SR POST fails.
|
|
61
|
+
- **Before 2026-09-30a, `Client_Adyen` had 0 client `ApiPayloadInterceptors` rows** — "nothing happened" on Adyen is a data check first.
|
|
62
|
+
- **Open — Adyen resending the same SR creates a duplicate.** `c_externalReferenceId` is not unique (SR100024 duplicates SR100021 — see [profile](../profile.md)).
|
|
63
|
+
- **Open — inbound-sync duplicate race** is a shared worker issue — see [SR → SO generation](../../../2.0/apps/worker2/features/service-request-sales-order-generation.md#gotchas).
|
|
57
64
|
|
|
58
65
|
## Change history
|
|
59
|
-
- 2026-
|
|
66
|
+
- 2026-10-08 — NetSuite PO # = employee name (`buildEmployeeName`); Adyen map entry (location id 1, ship method 5); prod Core interceptor duplicates 15–17 deactivated; Misc Laptop kept (PM). (rgirish)
|
|
67
|
+
- 2026-09-30 — Built: Reclaim bundle attach in `prePost`, Elite-style `postPost`, setup migration 2026-09-30a. (rgirish)
|
|
@@ -12,7 +12,7 @@ project: Worker
|
|
|
12
12
|
client: adyen
|
|
13
13
|
type: profile
|
|
14
14
|
status: draft
|
|
15
|
-
updated: 2026-10-
|
|
15
|
+
updated: 2026-10-08
|
|
16
16
|
owners: ["rgirish"]
|
|
17
17
|
files:
|
|
18
18
|
- worker/crons/toga2/netsuite/sync_togasupply_adyen.php
|
|
@@ -47,7 +47,7 @@ Adyen live TOGa Supply client (`Client_Adyen`) on the 1.0 NetSuite→TOGa Supply
|
|
|
47
47
|
- **NetSuite → TOGa Supply sync.** Per-client thin wrapper over the shared engine — full mechanics on [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
48
48
|
- **Transfer orders ENABLED** (`IS_ENABLED_INTEGRATION_TRANSFER_ORDERS = true`). Adyen's NetSuite transfer orders arrive **as NetSuite sales orders**, classified by `TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD` — `holdInvoice === true` **AND** `total === 0`, same rule as GroWrk (see [GroWrk transfer order flow](../growrk/features/transfer-order-flow.md)). Mode is now declared explicitly in Adyen's launcher; `App_Api_Toga2::isTransferOrder()` throws if it is missing, so **`library` and `worker` must deploy together** for Adyen.
|
|
49
49
|
- **IDs:** `Core.Clients` id **48**; NetSuite customer **31054** ("4950 Adyen"); Adyen API = `Apis` id 4.
|
|
50
|
-
- **Reclaim SR → Sales Order → NetSuite
|
|
50
|
+
- **Reclaim SR → Sales Order → NetSuite**: same chain as Elite, items from a seeded "Reclaim" bundle (Adyen sends no serials); NetSuite PO # = employee name — see [Adyen reclaim flow](./features/service-request-salesorder-netsuite.md) (also holds the 8-order backlog map).
|
|
51
51
|
- **`Client_Adyen`** is one of the client DBs with surface-layer / ACL drift noted on the [ACL permission chain](../../2.0/apps/_underscore/features/acl-permission-chain.md) and [surface-layer schema](../../2.0/apps/dbchanges2/features/surface-layer-schema.md) docs.
|
|
52
52
|
|
|
53
53
|
## Gotchas
|
|
@@ -84,10 +84,8 @@ Adyen tests the tracking API on **prod**. `2026-09-22a - AdyenApiTestSeed.sql` s
|
|
|
84
84
|
**Known data gaps (2026-09-28):**
|
|
85
85
|
|
|
86
86
|
1. SR100024 duplicates SR100021 (same Adyen ref 48748808) — a lookup by that ref returns **2 SRs**.
|
|
87
|
-
2. SR100026
|
|
88
|
-
3.
|
|
89
|
-
4. NS SO 288261 has no SR in Toga.
|
|
90
|
-
5. SR100000/01/02/25 are Adyen test SRs with no NS order.
|
|
87
|
+
2. Items 2–4 of the 2026-09-28 list (SR100026, SR100027–30, NS SO 288261) are superseded by the **2026-10-08 backlog map** — 8 Reclaim SRs ↔ NS SOs, 290879 + 288261 hand-entered and not linked in Toga. See [reclaim flow → Backlog](./features/service-request-salesorder-netsuite.md#backlog--8-reclaim-requests--netsuite-sos-2026-10-08).
|
|
88
|
+
3. SR100000/01/02/25 are Adyen test SRs with no NS order.
|
|
91
89
|
|
|
92
90
|
### Open defect — `Client_Adyen.Items` missing `isFulfillable` (prod AND beta)
|
|
93
91
|
|
|
@@ -5,10 +5,11 @@ project: _Underscore
|
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-10-
|
|
9
|
-
owners: [bala, ajean]
|
|
8
|
+
updated: 2026-10-08
|
|
9
|
+
owners: [bala, ajean, rgirish]
|
|
10
10
|
files:
|
|
11
11
|
- toga2-commerce/src/pages/Cart/api/CartApi.ts
|
|
12
|
+
- toga2-commerce/src/pages/Cart/viewModel/useCartViewModel.ts
|
|
12
13
|
- worker1.5/crons/toga2/compass/import_locations.php
|
|
13
14
|
- _underscore/Model/Compass/SalesOrder.php
|
|
14
15
|
- api2/Component/Api/V2/V2.php
|
|
@@ -33,17 +34,20 @@ US Compass only (`Client_Compass`). Compass Canada is explicitly out of scope. S
|
|
|
33
34
|
|
|
34
35
|
### What a cost center actually is
|
|
35
36
|
- A cost center = a **Unit Location** in `Client_Compass.Locations` (`locationTypeId = 1`), keyed by custom field `c_erpSystemEntityId`.
|
|
36
|
-
- `c_erpEntityId` is a **different** field
|
|
37
|
+
- `Locations.c_erpEntityId` is a **different** field: the full `"<entity>-<CC>"` unit id (e.g. `1001-20180`). At checkout only the **CC number** (e.g. `20180`) is written to `SalesOrders.c_erpEntityId` as a **plain string, not a foreign key** — the entity (`1001` / `1019`) is not stored. `_underscore/Model/Compass/SalesOrder.php` exposes a `_costCenter` computed field.
|
|
38
|
+
- Entity `1001` = US cost centers. Entity `1019` rows look like old / reused CCs (feed active flag `U`, not `Y`).
|
|
37
39
|
- The `CostCenters` table is empty for Compass; nothing reads `Users_CostCenters` / `Locations_CostCenters` / `SalesOrders.costCenterId` / `Users.defaultCostCenterId`.
|
|
38
40
|
|
|
39
41
|
### The commerce dropdown
|
|
40
|
-
- Built by `fetchCostCenter()` in `toga2-commerce/src/pages/Cart/api/CartApi.ts`, which calls `GET /locations` filtered on `c_erpSystemEntityId LIKE %search% AND isActive = 1`,
|
|
42
|
+
- Built by `fetchCostCenter()` in `toga2-commerce/src/pages/Cart/api/CartApi.ts`, which calls `GET /locations` filtered on `c_erpSystemEntityId LIKE %search% AND isActive = 1`, fields `c_erpSystemEntityId,name`, distinct (exact `=` match on the display path).
|
|
43
|
+
- `useCartViewModel.ts` (~426-460) labels options `"<CC> - <name>"` and dedupes by `c_erpSystemEntityId` — **first row wins**. So when one CC number exists under several entities, the shown name can belong to the wrong entity.
|
|
41
44
|
- Funnels both the cart checkout and the New-User form, so a single filter in `fetchCostCenter` covers both entry points.
|
|
42
45
|
- **api2 does no cost-center validation** — the FE is the only selection gate.
|
|
43
46
|
|
|
44
47
|
### Where the values come from (import)
|
|
45
|
-
- `worker1.5/crons/toga2/compass/import_locations.php` reads
|
|
46
|
-
-
|
|
48
|
+
- `worker1.5/crons/toga2/compass/import_locations.php` (1.0, daily) reads pipe-delimited `UNIT*.txt` from SFTP. Col 0 = `<entity>-<CC>` → `c_erpEntityId`; col 16 = CC → `c_erpSystemEntityId`; cols 17-20 = address; col 23 = active flag (`Y` → 1, else 0).
|
|
49
|
+
- Units are looked up by `c_erpEntityId` with an **INNER JOIN** to `Locations_Addresses` / `Addresses` on `primaryLocationAddressId` (~line 210-220). A unit with no address is never found, so a **new `Locations` row is inserted every run** — the source of the duplicates (see gotcha).
|
|
50
|
+
- The update branch sets a new `uuid` on every run, so Location uuids are not stable.
|
|
47
51
|
|
|
48
52
|
### Numeric-only enforcement (2026-07-06) — three layers
|
|
49
53
|
1. **Front-end selection block** — in `fetchCostCenter`, on the **search path** (`useSearchOptions === true`), returned locations are filtered to numeric-only with `/^[0-9]+$/` on `c_erpSystemEntityId.trim()`. The non-search / display path is left untouched (an already-saved value still renders). Done client-side because api2 rejects a server-side numeric filter (see gotcha) and api2 was out of scope.
|
|
@@ -61,12 +65,15 @@ Deactivation proven safe against prod data:
|
|
|
61
65
|
- Proven end-to-end in dev-sandbox: after the migration, active non-numeric rows = **0** (all 1,185 codes incl. the 725 number+alphabet combos) while pure-numeric stayed untouched at **440,005** active rows.
|
|
62
66
|
|
|
63
67
|
## Gotchas
|
|
68
|
+
- **⚠ "TechHub shows the wrong location name for CC X" is display-only.** The CC number is shared by more than one entity (176 numeric CCs among active units, prod 2026-10-08); the dropdown keeps the first row. The order stores only the CC number, so the order data is correct. Example: `20180` → `1001-20180` "General Overhead" (id 58192, RA B&I sector) vs `1019-20180` "Williamsburg Pottery Main" (548 active rows). Quick fix per CC: set `isActive = 0` on the stale entity's rows — check `Users.locationId` first (`1019-20180` had 0 users).
|
|
69
|
+
- **⚠ BUG (found 2026-10-08, NOT fixed) — the import duplicates units with no address, forever.** Cause: the INNER JOIN unit lookup above. Prod 2026-10-08: **874,887** active unit rows with no address; many units have ~1,200 rows (e.g. `1019-20180`: 1,038 rows, 859 active). Duplicates made before the active-flag logic stay active forever — the cron can never find them to deactivate. Fix: LEFT JOIN the unit lookup, then clean up the duplicates (soft delete, `isActive = 0`).
|
|
70
|
+
- **⚠ SECURITY — `import_locations.php` has the `cgsftp` SFTP password hardcoded in source.** Move it to config and rotate it. Never copy the value anywhere.
|
|
64
71
|
- **⚠ A cost-center number is NOT unique across sectors — never match on the number alone.** Scope
|
|
65
72
|
every match to a sector / hierarchy path. Measured 2026-10-05: 10 of the client's 634 "corporate"
|
|
66
73
|
cost centers also exist under other sectors — `4018` / `45781` / `62937` under B&I Overhead
|
|
67
74
|
(`M00000`), `22772` / `26963` / `28184` / `54188` under Corporate Income (`H00000`), `47258` /
|
|
68
75
|
`62230` under CCL Hospitality (`Y00000`), `31335` under ESFM (`W00000`). Three carry real users
|
|
69
|
-
(`4018` → 10, `31335` → 9, `47258` → 9), so the wrong match hits live people. See
|
|
76
|
+
(`4018` → 10, `31335` → 9, `47258` → 9), so the wrong match hits live people. Same number can also repeat across entities (`1001` / `1019`) — see the display gotcha above. See
|
|
70
77
|
[Location Hierarchy & Sectors](./location-hierarchy-and-sectors.md) for the walk and the
|
|
71
78
|
dedupe-before-counting recipe.
|
|
72
79
|
- **⚠ `Users.c_erpSystemEntityId` has a DOUBLE meaning — never overwrite it to persist a cost-center change.** In `worker2/Worker/Client/Compass/PeopleFile.php` it is (a) copied straight into `c_hrEmpDirectoryKey` and treated as **unique per employee** (`:544-546`, e.g. `"1001-1003"`), and (b) used as the **primary key for resolving `Users.locationId`** (`:566-571`). Writing a location code into it corrupts employee identity to buy one night of persistence. This is why the [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) writes only `Users.locationId` + `Users.c_erpEntityId` and accepts that a cost-center edit is **undone that night** for anyone in the PEOPLE file.
|
|
@@ -74,7 +81,6 @@ Deactivation proven safe against prod data:
|
|
|
74
81
|
- **⚠ There is no usable index for a cost-center search, so never wire one to a type-ahead.** `Client_Compass` has ~909,700 active `locationTypeId = 1` (Unit) rows carrying only ~36,352 distinct `c_erpSystemEntityId` values (~730 duplicate rows per code); **442,549** of the active rows have a numeric short code. Per the `dbchanges2` schema the only key on `Locations` is `locationTypeId`, which covers nearly the whole table — so there is no index on `c_erpSystemEntityId`, on `isActive`, or any composite. Every cost-center search reads the table then groups. **Recommended follow-up (not done): index `Locations (locationTypeId, isActive, c_erpSystemEntityId)` in BOTH Compass tenants** (dbchanges2).
|
|
75
82
|
- **⚠ BUG (found 2026-09-04, NOT fixed) — `_Model_Compass_SalesOrder::_costCenter()` joins the wrong column.** `_underscore/Model/Compass/SalesOrder.php:287-305` does `INNER JOIN Contacts ON Contacts.id = Users.id`; it should be `Users.contactId`. The user-level cost-center fallback therefore resolves an **unrelated user's** contact. Needs an `_underscore` change.
|
|
76
83
|
- **api2 cannot do a server-side numeric filter.** The V2 query-string where-parser (`api2/Component/Api/V2/V2.php` → `parseOptionsWhere`) whitelists operators and **rejects `regexp` with a 500**. Any numeric/regex filtering must happen client-side (or via a new operator in api2, which was out of scope here).
|
|
77
|
-
- **Two fields, easy to swap.** `c_erpSystemEntityId` is the cost center (what users pick); `c_erpEntityId` is the unit id and the value orders record on `SalesOrders.c_erpEntityId`.
|
|
78
84
|
- **Second, possibly-live copy of the import cron not yet guarded.** A second copy exists at `worker/crons/toga2/compass/DOA_Scripts/import_locations.php` (appears disabled / "DOA"). It did **not** receive the numeric guard — confirm whether it is live before trusting the guard to be complete.
|
|
79
85
|
|
|
80
86
|
## Related
|
|
@@ -83,3 +89,6 @@ Deactivation proven safe against prod data:
|
|
|
83
89
|
- [Cart page config architecture](../../../2.0/apps/toga2-commerce/features/cart-page-config-architecture.md)
|
|
84
90
|
- [Tools Compass user/persona admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md)
|
|
85
91
|
- [PEOPLE-file user lifecycle](./people-file-user-lifecycle.md)
|
|
92
|
+
|
|
93
|
+
## Change history
|
|
94
|
+
- 2026-10-08 — Documented dropdown first-row-wins dedupe (wrong name = display only), entity 1001 vs 1019, the INNER JOIN import bug that inserts duplicate address-less units every run, uuid churn, and the hardcoded SFTP credential. (rgirish)
|
|
@@ -5,8 +5,8 @@ project: _Underscore
|
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-10-
|
|
9
|
-
owners: [bala]
|
|
8
|
+
updated: 2026-10-08
|
|
9
|
+
owners: [bala, rgirish]
|
|
10
10
|
files:
|
|
11
11
|
- worker/crons/toga2/compass/DOA_Scripts/import_locations.php
|
|
12
12
|
- worker1.5/crons/toga2/compass/import_locations.php
|
|
@@ -69,6 +69,8 @@ not re-derive it. Ids are set in `import_locations.php:37-45`.
|
|
|
69
69
|
| 16 | cost center entity id → `Locations.c_erpSystemEntityId` |
|
|
70
70
|
| 17-20 | street / city / state / zip |
|
|
71
71
|
|
|
72
|
+
The live daily copy, `worker1.5/crons/toga2/compass/import_locations.php`, also reads **col 23 = unit active flag** (`Y` → active). Its duplicate-unit bug is in [Cost Centers](./cost-centers.md).
|
|
73
|
+
|
|
72
74
|
Because new corporate cost centers arrive on this feed on their own, **anything scoped to "corporate"
|
|
73
75
|
must key off the hierarchy, never a client-maintained spreadsheet.**
|
|
74
76
|
|
|
@@ -120,6 +122,7 @@ All of them are flagged Active and all have **zero users**. Drop them before any
|
|
|
120
122
|
- [Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md)
|
|
121
123
|
|
|
122
124
|
## Change history
|
|
125
|
+
- 2026-10-08 — Noted the live worker1.5 import copy reads col 23 as the active flag. (rgirish)
|
|
123
126
|
- 2026-10-05 — Created from the field-laptop research. Recorded that corporate is identified only by
|
|
124
127
|
sector `J00000` (`Locations.id = 52`) and the ERP-id letter prefix (no flag, no column in the
|
|
125
128
|
21-column feed), that Levy's corporate office is the **district** `K9902` not a sector, that the
|
package/package.json
CHANGED