toga-ai 1.0.935 → 1.0.936

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,13 +6,14 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-24
9
+ updated: 2026-10-02
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - library/app/api/toga2.php
13
13
  - library/app/api/netsuite/rest.php
14
14
  - dbchanges2/_modules/netsuite/2026-09-24a - TransferOrderDownstreamPoBridgeAcl.sql
15
15
  - worker/crons/toga2/netsuite/process_netsuite_order.php
16
+ - test/@jeff/netsuite/test_match_transfer_order_purchase_order_lines.php
16
17
  related:
17
18
  - ../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
18
19
  - ../../worker/features/netsuite-togasupply-per-client-sync.md
@@ -39,14 +40,15 @@ Callers:
39
40
  - `linkTransferOrderPurchaseOrders()` — kept as a thin public wrapper (old callers).
40
41
 
41
42
  Expected sets, from NetSuite only:
42
- - **Upstream:** `otherRefNum` split on `' + '` → `findCustomerPurchaseOrderByNumber` under `$cfg['importCustomerPurchaseOrdersOnVendor']`. Missing customer PO → **created** (header + lines). TO line with no matching PO line → a new PO line is created (on NYCHH this adds stock-in — expected).
43
+ - **Upstream:** `otherRefNum` split on `' + '` → `findCustomerPurchaseOrderByNumber` under `$cfg['importCustomerPurchaseOrdersOnVendor']`. Missing customer PO → **created** (header + lines). A new PO line is created only when the TO item is on NO line of that PO. A new PO line adds NO stock-in (stock-in = linked stocking SO's received adjustments; see [NYCHH quantities](../../../clients/nychh/features/transfer-order-inventory-quantities.md)).
43
44
  - **Downstream:** `App_Api_Netsuite_Rest::listPurchaseOrderIdsCreatedFromOrder($orderId)` (SuiteQL on `transactionline.createdfrom`). A vendor PO not yet in Toga is skipped here; the PO sync imports it later and reconciles again.
44
45
 
45
- Line matching (per direction):
46
- 1. Same item `uuid`, then `partNumber`.
47
- 2. Keep an existing link if still valid (Supply-created TO lines keep the PO line the user picked).
48
- 3. Prefer equal quantity; each PO line used once.
49
- 4. A TO line with NetSuite `createdPo` pairs only inside that PO.
46
+ Line matching — `matchTransferOrderItemsToPurchaseOrderItems()` (public static, options array; helpers `chooseReusablePurchaseOrderLine`, `chooseUnclaimedPurchaseOrderLine`, `getStockedPurchaseOrderItemUuids`):
47
+ 1. Candidates = same item `uuid`, then `partNumber` (exact — no normalization; `210-BFZY-A` ≠ `210-BFZY`).
48
+ 2. A TO line with NetSuite `createdPo` pairs only inside that PO.
49
+ 3. **Upstream (customer PO, 325/329)** — rank: existing link (unless it is unstocked while a stocked line exists) > stocked line (has a `PurchaseOrderItems_SalesOrderItems` row) > not yet used by this TO > equal qty > lowest line. A PO line MAY be reused by several TO lines (soft claim). A moved link logs `RELINK` to error_log.
50
+ 4. **Downstream (vendor PO, 326/330)** — unchanged: keep valid existing link, prefer equal qty, each PO line used once.
51
+ 5. Empty/orphan PO lines are never auto-deleted.
50
52
 
51
53
  Deletes are logged: grep `reconcileTransferOrderPurchaseOrderLinks` in the worker log.
52
54
 
@@ -57,7 +59,9 @@ Deletes are logged: grep `reconcileTransferOrderPurchaseOrderLinks` in the worke
57
59
  - **ACL first.** 326/330 had no API-role ACL anywhere → 403 `EZ-1`. `_modules/netsuite/2026-09-24a - TransferOrderDownstreamPoBridgeAcl.sql` seeds the 4-layer chain + field grants 2214-2216 (326) and 2230-2232 (330), roles by NAME (`API` = id 3 in all 37 prod client DBs). Must run **before** the library deploy.
58
60
  - **Repair model = re-process, not SQL.** Fix the code so a re-run heals, then reset cursors / replay ([per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md)) or run [process_netsuite_order.php](../../worker/workflows/reprocessing-one-netsuite-order.md) for one order. No data-repair SQL.
59
61
  - **Pre-2026-09-24 upstream rows were polluted.** The old `linkPurchaseOrderToTransferOrder` path (PO `createdFrom` a TO) and old `retireStale` wrote vendor POs into 325/329. `Client_Nychh` before fix: ~533/1,593 rows in 325 and 3,981/5,893 in 329 were vendor POs; GroWrk/Elite a few. Example: NYCHH TO id 11 (NS SO 71759 / internal 1582638) had vendor POs 49338 + 49324 upstream and its real customer PO EIT0005971 was never imported. After re-process: EIT0005971 created (PO id 1005, 20 lines, customer 28908) upstream; 49324 lines 1-15 + 49338 lines 1-5 downstream; 0 vendor rows upstream.
62
+ - **Old upstream matcher picked wrong lines (fixed 2026-10-02, `1bc4de52`).** It took the first same-item line (often an unstocked line created from a TO) and its "each PO line once" rule made new empty PO lines when a TO had the same item twice. Fix = upstream ranking above; downstream kept strict. Heal by re-processing the TO. Tests: `test/@jeff/netsuite/test_match_transfer_order_purchase_order_lines.php`.
60
63
  - Stale test: `test/@Bala/tests/netsuite_transferorder_payload_tests.php` "PO number from the column" case fails since the `TransferOrders.purchaseOrderId` drop ([transfer-orders page](../../../2.0/apps/toga25-supply/features/transfer-orders-page.md)).
61
64
 
62
65
  ## Change history
66
+ - 2026-10-02 — Upstream line matching: stocked-line ranking + reuse; new PO line only when item absent; RELINK log; downstream unchanged. CTO alternative (soft claim, upstream only) adopted. (jcardinal)
63
67
  - 2026-09-24 — Created: two-direction reconcile replaces the upstream-only linker; fixed vendor POs written upstream; 326/330 now live + ACL'd. Prod-verified. (jcardinal)
@@ -21,6 +21,7 @@
21
21
  | [Walmart Daily Sales Report (send_toga_walmart_report.php)](features/walmart-daily-sales-report.md) | The two Walmart sales workbooks (summary + details) built from `TOGA_Walmart` (in-store) and `TOGA_WalmartEComm` (.COM); open for the pseudo-store 1472 channel |
22
22
  | [Diagnosing frozen 1.0 worker cron check-ins (Sentry "missed" flood)](workflows/diagnosing-frozen-cron-checkins.md) | Ordered path to diagnose frozen 1.0 worker cron timestamps + a Sentry `worker1` "missed" flood; open when check-ins stall and you suspect a wedged lock. |
23
23
  | [isFulfillable Multi-Client Backfill (all togasupply clients)](workflows/isfulfillable-multi-client-backfill.md) | One-time backfill catching up `Items.isFulfillable` on existing items across all 17 togasupply clients; open when running/re-running it or deciding whether rema |
24
+ | [Mapping NetSuite customer address-book entries to Toga Locations (process_netsuite_addressbook_mapping.php)](workflows/netsuite-addressbook-location-mapping.md) | Operator CLI that maps a client customer's NetSuite address-book entry ids to Toga Locations (`Locations_Addresses.c_netsuiteInternalAddressbookId`), so TO impo |
24
25
  | [Deduplicating NetSuite duplicate Items across client DBs (recursive MERGE)](workflows/netsuite-duplicate-items-dedup.md) | Per-client cleanup collapsing duplicate `Items` rows (same `partNumber`) to one canonical via recursive MERGE; open before running it — and read the catalog-bli |
25
26
  | [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer; open when onboarding a client, and read the vendor-seed / ACL / Parameters-see |
26
27
  | [Re-processing NetSuite orders, fulfillments, receipts and inventory adjustments (process_netsuite_*.php)](workflows/reprocessing-one-netsuite-order.md) | Run the real sync for given NetSuite ids on ONE client, without touching cursors: one order (+ its vendor POs), or a list of fulfillments/receipts/inventory adj |
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: "Mapping NetSuite customer address-book entries to Toga Locations (process_netsuite_addressbook_mapping.php)"
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-10-02
10
+ owners: [jcardinal]
11
+ files:
12
+ - worker/crons/toga2/netsuite/process_netsuite_addressbook_mapping.php
13
+ - library/app/api/netsuite/rest.php
14
+ - _underscore/Trait/Netsuite/Locations/Address.php
15
+ - dbchanges2/_modules/netsuite/2026-10-01a - LocationsAddressesNetsuiteAddressbookId.sql
16
+ - dbchanges2/_modules/netsuite/2026-10-01b - LocationsAddressesAddressbookIdCustomField.sql
17
+ related:
18
+ - ../../../../clients/nychh/features/netsuite-transfer-order-import.md
19
+ - ../../../../clients/nychh/features/location-shipping-addresses.md
20
+ - ./reprocessing-one-netsuite-order.md
21
+ ---
22
+
23
+ Operator CLI that maps a client customer's NetSuite address-book entry ids to Toga Locations (`Locations_Addresses.c_netsuiteInternalAddressbookId`), so TO import resolves the destination by entry id; open to propose/apply or extend the mapping.
24
+
25
+ ## Summary
26
+ - Manual CLI, never scheduled. The sync only READS the mapping; it never writes it (no self-learning — CTO AGREE).
27
+ - Two passes: `--propose` writes a CSV for review; `--apply` writes the reviewed CSV.
28
+ - Column `Locations_Addresses.c_netsuiteInternalAddressbookId` (INT UNSIGNED, unique) is all-NetSuite-client (`_modules/netsuite/`); declared via `_Trait_Netsuite_Locations_Address` on every NetSuite client's `Model/<Client>/Locations/Address.php` (22 clients; Compass USA = `Model/Compass/Usa/Locations/Address.php`). Undeclared → /v2 EV-8.
29
+
30
+ ## How it works
31
+ From `worker/`:
32
+ ```
33
+ php crons/toga2/netsuite/process_netsuite_addressbook_mapping.php <client> --propose=<csv> [--id=<entry id>]
34
+ php crons/toga2/netsuite/process_netsuite_addressbook_mapping.php <client> --apply=<csv> [--id=<entry id>]
35
+ ```
36
+ Entries come from `App_Api_Netsuite_Rest::listCustomerAddressbookEntries()`. Each entry gets a reason (NetSuite decides; Toga only as fallback):
37
+ 1. `BILLING` — skipped: entry is any site's default billing.
38
+ 2. `AGILANT` — skipped (Agilant's own address).
39
+ 3. `ADDRESSEE` — addressee names exactly one site.
40
+ 4. `NETSUITE` — exactly one site's own NetSuite address book has the same street + zip.
41
+ 5. `NETSUITE_DEFAULT_SHIPPING` — tie broken by which site has it as default shipping.
42
+ 6. `TOGA` — fallback by Toga location address (for stale customer ids).
43
+ Anything else stays unmapped → import falls back to text match.
44
+
45
+ ## Gotchas
46
+ - NYCHH 2026-10-02: 61 entries → 18 sites. Unmapped by design: shared buildings and the shared 462 1st Ave dock — list on [NYCHH location addresses](../../../../clients/nychh/features/location-shipping-addresses.md).
47
+ - Stale customer ids (NYCHH Bellevue 29275, Jacobi 31917 do not exist in NetSuite) only map via `TOGA`.
48
+ - After apply, re-process affected TOs (`process_netsuite_order.php <client> --tranid=a,b`) to move their destination.
49
+
50
+ ## Change history
51
+ - 2026-10-02 — Created; applied for NYCHH. (jcardinal)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-30
9
+ updated: 2026-10-02
10
10
  owners: ["jcardinal", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/process_netsuite_order.php
@@ -37,7 +37,7 @@ From the `worker/` directory:
37
37
  php crons/toga2/netsuite/process_netsuite_order.php <client> --tranid=<NS order #>
38
38
  php crons/toga2/netsuite/process_netsuite_order.php <client> --id=<NS internal id>
39
39
  ```
40
- `<client>` = the wrapper suffix of `sync_togasupply_<client>.php` (e.g. `hh`, `growrk`, `elite`). Pass exactly one of `--tranid` / `--id`.
40
+ `<client>` = the wrapper suffix of `sync_togasupply_<client>.php` (e.g. `hh`, `growrk`, `elite`). Pass exactly one of `--tranid` / `--id`. Both accept a comma list (`--id=a,b`, `--tranid=a,b`): runs in order, stops on the first error.
41
41
 
42
42
  Steps the script runs:
43
43
  1. Refuses if `sync_togasupply_<client>.php` is running (`ps -ef`) — two writers can duplicate links.
@@ -73,6 +73,7 @@ php crons/toga2/netsuite/process_netsuite_inventory_adjustment.php <client> --id
73
73
  - Needs `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` true in the wrapper for TO routing; otherwise the order syncs as a sales order.
74
74
 
75
75
  ## Change history
76
+ - 2026-10-02 — `process_netsuite_order.php` accepts comma lists for `--id` / `--tranid` (`a72f17f5`). (jcardinal)
76
77
  - 2026-09-30 — Added `process_netsuite_item_fulfillment.php` + `process_netsuite_inventory_adjustment.php`. (jcardinal)
77
78
  - 2026-09-30 — Added `process_netsuite_records.php` (fulfillments/receipts by id); Closed routing follows the closed-order rule (v3: line-level). (rgirish)
78
79
  - 2026-09-24 — Created; prod-verified on NYCHH TO 11. (jcardinal)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
9
+ updated: 2026-10-02
10
10
  owners: ["bala", "jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Netsuite/SalesOrder.php
@@ -78,7 +78,7 @@ Nothing beyond this list. A transfer order has no shipping cost, ship method, te
78
78
  | `tranDate` | `dateOrder`, date only; **omitted** when null |
79
79
  | `otherRefNum` | the PO number (developer's explicit requirement) |
80
80
  | `memo` | `TransferOrders.description` — the NYCHH custom form renders it as **"ORDER DESCRIPTION"** |
81
- | `shippingAddress` | the **destination** Location's `primaryLocationAddress.address` (added 2026-09-09, live 2026-09-14). Key **omitted** when the destination has no address — and **omitting it is a bug, not a safe default**: NetSuite then ships to the customer's default site. |
81
+ | `shippingAddress` | the TO's own `shipToAddress` (`TransferOrders.shipToAddressId`, 2026-10-02) when set; else the **destination** Location's `primaryLocationAddress.address` (added 2026-09-09, live 2026-09-14). Key **omitted** when the destination has no address — and **omitting it is a bug, not a safe default**: NetSuite then ships to the customer's default site. |
82
82
  | `shippingAddress.attention` | the TOGA delivery contact (`transferOrder.contact`): `firstName + ' ' + lastName` (empty parts dropped), fallback `contact._name`. Set only when both an address and a name exist; no contact → attention untouched. Name only, no phone (developer decision). `resolveTransferOrderContactName()`; the address is **cloned** first so the Location's address object is not mutated. Live in prod 2026-09-25. |
83
83
  | `externalId` | `toga-to-<transfer order uuid>` |
84
84
  | line `item` / `quantity` | each transfer-order item |
@@ -260,6 +260,7 @@ When the payload omits the ship-to address the create **succeeds**, so it looks
260
260
  - **`git diff --stat` after any programmatic full-file write to `_underscore`.** A full-file write with an editor default flipped CRLF → LF on `_underscore/Model/Nychh/TransferOrder.php` — content identical, git reported it modified with no content hunks. Restored with `git checkout --`. Standing rule: [2.0 backend PHP standards](../../standards/backend-php.md).
261
261
 
262
262
  ## Change history
263
+ - 2026-10-02 — Push sends the TO's own `shipToAddress` when set (new `TransferOrders.shipToAddressId`, RecordField 2682); destination Location address is the fallback. (jcardinal)
263
264
  - 2026-09-24 — `resolvePurchaseOrderNumber()` bridge-only (column 2143 dropped); multiple POs joined with `' + '`. (jcardinal)
264
265
  - 2026-09-25 — delivery contact name sent as `shippingAddress.attention`. (jcardinal)
265
266
 
@@ -72,7 +72,7 @@ An item with no active vendor is absent from the result and produces no PO line.
72
72
 
73
73
  `c_netsuiteInternalVendorId` is present in **22 of 35 tenant databases — 13 do not have it**. Referencing it blind fails with MySQL *"Unknown column"* for those 13. So the worker runs a runtime `information_schema.COLUMNS` probe (`TABLE_SCHEMA` = client DB, `TABLE_NAME = 'Vendors'`) and only splices the ranking clause into `ORDER BY` when the column exists.
74
74
 
75
- Generalizes: `c_` columns are framework-dynamic and **per-tenant schema**, so any shared multi-tenant query touching one must probe for it or be scoped to tenants known to have it. See the `c_`-column convention in `2.0/standards/framework-rules.md`.
75
+ Generalizes: `c_` columns are **per-tenant schema**, so any shared multi-tenant query touching one must probe for it or be scoped to tenants known to have it. See the `c_`-column convention in `2.0/standards/framework-rules.md`.
76
76
 
77
77
  ### Verifying the whole chain — `test_elite_chain_toga2.php`
78
78
 
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
9
+ updated: 2026-10-02
10
10
  owners: [jcardinal, ajean]
11
11
  files:
12
12
  - worker2/Worker/Team/Transcripts.php
@@ -419,13 +419,7 @@ here by section/account, never by value.
419
419
  `_ApiRequest::execute()`** (reconnect/re-register before the post-call save) and affects every
420
420
  framework caller, so it is deferred pending architecture review. Do **not** work around it in
421
421
  `Transcripts.php`.
422
- - **`c_`-prefixed columns are FRAMEWORK-DYNAMIC in 2.0 — no model-class change needed.** The new
423
- `c_transcriptAiModelId` / `c_s3*` / `c_trueAiModelId` columns are read off raw `_Query` rows and
424
- written/filtered as `_Model` array keys **without** being declared in the model class and with **no**
425
- `Core.RecordFields` / ACL entries (matches the existing `c_clickupUserIdentifier` precedent).
426
- Consequence: the `_underscore`/model layer required **zero** changes for these columns (Phase 2
427
- verified NO-OP). See the framework standard
428
- [`c_`-prefixed custom columns](../../standards/framework-rules.md).
422
+ - **The `c_` columns here never go through /v2 — that is why they needed no model change.** `c_transcriptAiModelId` / `c_s3*` (worker2 `Worker/Team/Transcripts.php`), `c_trueAiModelId` (`tools/mvc/talos/vocabulary/post.php`) and the older `c_clickupUserIdentifier` (worker2 `Worker/Netsuite/Opportunity.php`) are read/written only by hand-written SQL / raw `_Query` rows, which return any column. Through /v2 or a `_Model` instance a `c_` column must be declared — see [`c_`-prefixed custom columns](../../standards/framework-rules.md).
429
423
  - **Cross-ACCOUNT S3.** `togaiq` (us-east-1) approved/archive writes use the `[talos]` key;
430
424
  `CopyObject` across accounts is impossible, so archive is get(in-memory)+put. (The old toga-private
431
425
  cross-account read is no longer in the loop.)
@@ -445,6 +439,7 @@ here by section/account, never by value.
445
439
  read will clean every transcript with the merged/global set instead of the routed model's copy.
446
440
 
447
441
  ## Change history
442
+ - 2026-10-02 — Corrected the `c_` column note: these columns are raw-SQL only, not proof that /v2 is dynamic. (jcardinal)
448
443
  - 2026-09-25 — renamed product references from "TOGa IQ" to "TOGa Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
449
444
 
450
445
  ## Related
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-09-25
8
+ updated: 2026-10-02
9
9
  owners: [jcardinal, ajean, apeterson, mhammontree]
10
10
  files: []
11
11
  related:
@@ -317,20 +317,23 @@ continue. Data accuracy outranks job "completion," even when the failure is not
317
317
  ACL error on one record) — a developer reconciling a stalled job is the intended outcome, not a
318
318
  job that reports success having quietly skipped records.
319
319
 
320
- ## `c_`-prefixed custom columns are dynamic in the /v2 API — not in `_Model`
320
+ ## `c_`-prefixed custom columns need a model declaration in the /v2 API
321
321
 
322
- Columns prefixed `c_` are **custom/dynamic** columns. The **/v2 API engine** handles them
323
- **without any model-class declaration**:
322
+ /v2 exposes a `c_` column ONLY when the client model declares it. Proven in api2
323
+ `Component/Api/V2/V2.php`: fields come from `$model->getObjectVars()` (~L7907);
324
+ `CustomRecordFields` (~L9653) feeds only identifiers/ACL.
324
325
 
325
- - Through /v2 they are read, written, and filtered **without** being declared as properties on
326
- the `_Model_*` subclass. Raw `_Query` result rows also return them as-is.
327
- - They require **no `Core.RecordFields` entry and no ACL entry**.
328
- - Precedent examples (`c_clickupUserIdentifier`, the 2026-07 transcript-routing columns): see framework-rules-cases.md.
326
+ A `c_` column needs all three:
327
+ 1. The column (`dbchanges2` migration).
328
+ 2. A declaration on the client model — usually a `_Trait_*` used by `_Model_<Client>_X`. The
329
+ override is resolved by `str_replace('_Model_Client_', '_Model_<clientIdentifier>_')`, so it
330
+ must exist for every client that uses the column (e.g. Compass USA =
331
+ `Model/Compass/Usa/...`, clientIdentifier `Compass_Usa`).
332
+ 3. `CustomRecordFields` + `AclCustomFieldPermissions` rows (ACL).
329
333
 
330
- **Consequence:** for data that only flows through /v2 (or raw `_Query`), adding a `c_` column
331
- is a **schema-only** change (a `dbchanges2` migration). Do not add `RecordFields`/ACL rows for a
332
- `c_` column. Use a non-`c_` column when you *do* want the full declared, ACL-governed field
333
- treatment.
334
+ Missing declaration → EV-8 "field does not exist", even with the column and
335
+ `CustomRecordFields` present. Example: `Locations_Addresses.c_netsuiteInternalAddressbookId`
336
+ (`_Trait_Netsuite_Locations_Address` on all 22 NetSuite clients, 2026-10-02).
334
337
 
335
338
  **Exception — direct `_Model` access.** `_Model` itself is **not** dynamic: `__get`/`__set` of an
336
339
  undeclared field throws `There is no field called 'c_…'`. Code that reads or writes a `c_` column
@@ -4,8 +4,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 31 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 41 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 32 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
+ - **worker** (Worker) — 42 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
10
10
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
11
11
  - **togadesk** (TOGa Desk) — 14 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -11,3 +11,4 @@
11
11
  | [NYCHH TransferOrder → NetSuite push (location roles, interceptor row 312, metadata state)](features/transfer-order-netsuite-push.md) | 2.0 | NYCHH is the **first and only** client on the outbound transfer-order push. |
12
12
  | [NYCHH transfer-order notification emails (Submitted / Packed / Shipped)](features/transfer-order-notification-emails.md) | 2.0 | NYC Health + Hospitals (H+H) gets three notification emails for a transfer order raised in **TOGa Supply**: **Submitted**, **Packed** and **Shipped**. |
13
13
  | [NYC Health + Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) client profile — open for supply-chain / TransferOrder / NetSuite-sync work on prod schema `Client_Nychh`. |
14
+ | [NYCHH PO-line \"no available qty\" triage (cursor / Toga bug / NetSuite-human)](workflows/po-availability-triage.md) | 1.0 | Steps to classify why a NYCHH customer PO line shows no available qty in Toga, and how to write Ops fix steps; open when Ops sends a list of POs that "show 0 av |
@@ -6,13 +6,16 @@ project: Database Changes
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-25
9
+ updated: 2026-10-02
10
10
  owners: [bala, jcardinal]
11
11
  files:
12
12
  - dbchanges2/Client_Nychh/2026-09-09c - UpdateLocationShippingAddresses.sql
13
13
  - dbchanges2/Client_Nychh/2026-09-09a - DeleteTestContacts.sql
14
14
  - dbchanges2/Client_Nychh/2026-09-09b - InsertDeliveryContacts.sql
15
15
  - _underscore/Model/Client/Location.php
16
+ - _underscore/Trait/Netsuite/Locations/Address.php
17
+ - worker/crons/toga2/netsuite/common_sync_togasupply.php
18
+ - worker/crons/toga2/netsuite/process_netsuite_addressbook_mapping.php
16
19
  - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTargetLocationOptions.ts
17
20
  related:
18
21
  - ../profile.md
@@ -22,6 +25,7 @@ related:
22
25
  - ../../../2.0/apps/_underscore/features/calculated-sql-fields.md
23
26
  - ../../../2.0/apps/dbchanges2/workflows/deleting-contacts-from-a-client-database.md
24
27
  - ../../../2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md
28
+ - ../../../1.0/apps/worker/workflows/netsuite-addressbook-location-mapping.md
25
29
  ---
26
30
 
27
31
  NYCHH per-location shipping addresses + delivery contacts for transfer orders; open when a transfer's ship-to/target-location/contact looks wrong or shared.
@@ -30,7 +34,7 @@ NYCHH per-location shipping addresses + delivery contacts for transfer orders; o
30
34
 
31
35
  Every one of NYCHH's **24 shipping locations** (`Locations.locationTypeId = 1`) resolved to the same address — **50 Water St., 3rd Floor, New York, NY 10004**, attention *"Accounts Payable OTPS"* — the head-office **Accounts Payable BILLING** address, not a delivery address. So the Create Transfer Order target-location list showed one identical address 24 times and a transfer had no real delivery address. Fixed 2026-09-09 by backfilling each location's own **NetSuite default shipping address**.
32
36
 
33
- Load-bearing fact: **for NYCHH a `Locations` address only affects transfer orders.** `SalesOrders` carries its own `shipToAddressId` per order and those already held the real hospital addresses (462 1st Ave, 451 Clarkson Ave, 100 N Portland Ave, …). `TransferOrders` has **no ship-to column at all** — only `originLocationId` / `destinationLocationId` — so a transfer's delivery address is read off the location record. That is why a wrong location address was invisible everywhere except the transfer screen.
37
+ Load-bearing fact: **for NYCHH a `Locations` address only affects transfer orders.** `SalesOrders` carries its own `shipToAddressId`. Since 2026-10-02 `TransferOrders` also has `shipToAddressId` (own `Addresses` row, backfilled from the destination's address — [import](./netsuite-transfer-order-import.md)); the location address is the default a new TO copies.
34
38
 
35
39
  ## How it works
36
40
 
@@ -55,6 +59,11 @@ The Create Transfer Order screen picks a **Target Contact** from `Contacts` thro
55
59
  - `Client_Nychh.ContactTypes` is **empty**, so `contactTypeId` is left NULL.
56
60
  - **Deleting the old rows was the hard part** — nothing cascades and `Contacts` points back at its own children. Full order in [deleting Contacts from a client database](../../../2.0/apps/dbchanges2/workflows/deleting-contacts-from-a-client-database.md).
57
61
 
62
+ ### Location auto-create + NetSuite address-book mapping (2026-10-02)
63
+ - NYCHH `Locations` = one per NetSuite child customer of parent **28908**, create-only (`common_sync_togasupply.php`). Auto-create now uses the customer's **default SHIPPING** address (was billing); logs when none.
64
+ - Customer 28908 has **125** address-book entries; one site can have several (Woodhull 720 Flushing / 760 Broadway). Each entry id maps to a site via `Locations_Addresses.c_netsuiteInternalAddressbookId` (INT UNSIGNED, unique; `_Trait_Netsuite_Locations_Address`, declared on every NetSuite client's `Model/<Client>/Locations/Address.php`). Set only by the operator CLI — [address-book mapping](../../../1.0/apps/worker/workflows/netsuite-addressbook-location-mapping.md).
65
+ - Applied 2026-10-02: 61 entries → 18 sites. Unmapped by design: 506 Lenox (Harlem + Renaissance both default), 545 E 142nd (Segundo Ruiz Belvis + Gotham), 462 1st Ave H Building (shared dock in ~18 site books), 50 Water (billing). These fall back to the text match.
66
+
58
67
  ## Gotchas
59
68
 
60
69
  - **🚩 OPEN — two contacts have no location link.** **Erald Velmishi** (Central Office) and **Harry Ho** (Community Care) got no `Contacts_Locations` row because **neither facility exists in `Locations`**. They will not appear in the Target Contact dropdown anywhere until the locations are created.
@@ -66,6 +75,11 @@ The Create Transfer Order screen picks a **Target Contact** from `Contacts` thro
66
75
  - **⚠ `attention = "Accounts Payable OTPS"` is the tell that a row is a BILL-TO** being used as a ship-to. If a whole tenant's locations share one address, suspect the head-office AP address.
67
76
  - **⚠ The state renders as a number until `_addressState` ships.** The fixed `_Model_Client_Location::_addressState()` (join `States`, select `States.code`) is written but **not deployed**, so production still shows `Bronx, 33 10451-5504`. See [FIELD_SQL calculated fields](../../../2.0/apps/_underscore/features/calculated-sql-fields.md).
68
77
 
78
+ ## Change history
79
+ - 2026-10-02 — Auto-create uses default shipping (not billing); address-book entry → site mapping (61 → 18 sites); TOs own `shipToAddressId`. (jcardinal)
80
+ - 2026-09-25 — Chosen contact → NetSuite `attention`. (jcardinal)
81
+ - 2026-09-09 — Backfilled 24 location shipping addresses; 19 real delivery contacts. (bala)
82
+
69
83
  ## Related
70
84
  - [NYCHH NetSuite → TransferOrders import](./netsuite-transfer-order-import.md)
71
85
  - [NYCHH TransferOrder → NetSuite push](./transfer-order-netsuite-push.md)
@@ -6,7 +6,7 @@ project: Library
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-30
9
+ updated: 2026-10-02
10
10
  owners: [jcardinal, rgirish]
11
11
  files:
12
12
  - library/app/api/toga2.php
@@ -140,11 +140,13 @@ php crons/toga2/netsuite/diagnose_hh_import_inventory_adjustment.php <togaClient
140
140
 
141
141
  ## Gotchas / known issues
142
142
 
143
- - **Old code cross-producted lines (fixed 2026-09-30, NOT deployed).** Every IA line of a product linked to every IF line of it: IA #1312 (NS 5211365, TOGa id 387) had 108 line-bridge rows vs 23 correct (210-BKVQ 9×9 = 81 vs 9). Prod: 615 rows over 8 IAs; only 1312 + 1318 wrong. Self-heals via reconcile on re-import (1312 108→23, 1318 4→2) — no SQL. After worker deploy: `php crons/toga2/netsuite/process_netsuite_inventory_adjustment.php hh --id=5211365,5211772`.
143
+ - **Old code cross-producted lines (fixed 2026-09-30, deployed — `df03dd8f` on `_production` since 2026-09-29).** Every IA line of a product linked to every IF line of it: IA #1312 (NS 5211365, TOGa id 387) had 108 line-bridge rows vs 23 correct (210-BKVQ 9×9 = 81 vs 9). Prod: 615 rows over 8 IAs; only 1312 + 1318 wrong. Self-heals via reconcile on re-import (1312 108→23, 1318 4→2) — no SQL. Re-run: `php crons/toga2/netsuite/process_netsuite_inventory_adjustment.php hh --id=5211365,5211772`. 2026-10-02 re-runs of IAs 4754924, 5190495, 5746859, 5746881 linked their missing lines.
144
144
 
145
145
  - **`custbody_stock_adjustment` is a MULTI-SELECT — one adjustment links MANY fulfillments.** The old code used `custbody_stock_adjustment[0]` and captured only the **first**: on adj **1320** (NS id 5218734) that linked **1 of 17** header + **1 of 16** lines. Always read every id from the scalar and loop.
146
- - **Older adjustments have no link.** `custbody_stock_adjustment` was added in NetSuite <1 year ago; adjustments predating it will never populate a link — not a bug.
147
- - **A missing bridge link on a NYCHH adjustment is usually CORRECT, not a bug.** Only records whose NetSuite multi-select body field `custbody_stock_adjustment` is populated get a link — and **0 of 27** NYCHH adjustments carry a value there (only **~1 in 40** recent adjustments account-wide do). So a null header/line FK almost always means NetSuite has **no** stock-adjustment link on that record, not a code fault. The REST shim's read of `custbody_stock_adjustment` as a **scalar SuiteQL column on `transaction`** works when the field IS populated (SuiteQL omits null columns from a row, so an empty field just looks "absent"); the shim only builds the link ref when a value exists. (A clarifying comment was added at the `customFieldList` build in `rest.php` `fetchInventoryAdjustmentById`.)
146
+ - **A blank `custbody_stock_adjustment` is a NetSuite/human data issue — even on pre-2024 adjustments** (developer decision 2026-10-02). Not a Toga bug; Ops fills the link in NetSuite, then re-run the IA. Triage: [PO availability triage](../workflows/po-availability-triage.md).
147
+ - **The IA → IF → SO chain imports only MISSING records; it never refreshes existing ones.** `attachInventoryAdjustmentToItemFulfillment` imports a missing linked fulfillment on demand; an already-imported IF/SO keeps its old data (TO re-sync always runs).
148
+ - **SuiteQL: `custbody_stock_adjustment LIKE '%<id>%'` in WHERE silently returns 0 rows.** Read the raw field per adjustment id instead.
149
+ - **A missing bridge link on a NYCHH adjustment is usually CORRECT, not a bug.** Only records whose NetSuite multi-select body field `custbody_stock_adjustment` is populated get a link — and **0 of 27** NYCHH adjustments carry a value there (only **~1 in 40** recent adjustments account-wide do). So a missing bridge almost always means NetSuite has **no** link on that record, not a code fault. The REST shim's read of `custbody_stock_adjustment` as a **scalar SuiteQL column on `transaction`** works when the field IS populated (SuiteQL omits null columns from a row, so an empty field just looks "absent"); the shim only builds the link ref when a value exists. (A clarifying comment was added at the `customFieldList` build in `rest.php` `fetchInventoryAdjustmentById`.)
148
150
  - **Reading the product uuid at `depth=2` silently returns nothing** — the level-3 `item` object is omitted at max depth. Use `depth=3` + explicit nested `fields` (see above).
149
151
  - **The header upsert's WRITE read-back depth is bounded too — `depth=4`, not `5` (fixed 2026-09-08).** `syncInventoryAdjustmentFromNetsuite`'s `POST|PUT /inventory-adjustments` (`toga2.php` ~L5852) used to request `depth=5`; on a large serialized adjustment api2 spent ~830s building that read-back and the worker's cURL timed out at 900s (0 bytes) → the section froze at `1-RUNNING`. `depth=4` is the safe minimum: the deepest object read off the response is `inventoryAdjustmentItems[] → inventoryAdjustmentItemUnits[] → unit` (scalars only, 3 hops out), and api2 drops a non-null FK **at** the max depth, so `depth=3` would null `unit` and crash. **Do not revive the dead `unit->unitDisposition` block** (`toga2.php` ~6135-6138, ~6177-6184) — it reads `unit` one level deeper and would force `depth=5` again, reopening the timeout. Shared-engine detail: [per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
150
152
  - **A V2 PUT echo that omits `itemFulfillment` does NOT mean the write failed** — verify against the DB / `Logs_<Client>.Api`, not the echoed body.
@@ -154,7 +156,8 @@ php crons/toga2/netsuite/diagnose_hh_import_inventory_adjustment.php <togaClient
154
156
  - **⚠ OPEN — the IA dedup / self-heal deletes are NOT idempotent against already-gone children.** A half-finished prior run leaves a child already deleted; the retry's `send()` **throws on the 404** and wedges the run. Fix (not yet done): treat not-found as done in the delete path.
155
157
 
156
158
  ## Change history
157
- - 2026-09-30 — Line pairing: per product, exact-qty 1:1 when both sides have many (was full cross-product); paged reads with totalRecordCount guard; synced-IA-line count guard. Verify harness deleted → `process_netsuite_inventory_adjustment.php`. CTO AGREE, php-reviewer CLEAN; NOT deployed. (jcardinal)
159
+ - 2026-10-02 — Marked line-pairing fix deployed (`df03dd8f`); blank link = NetSuite/human issue; chain imports missing only; SuiteQL LIKE gotcha. (jcardinal)
160
+ - 2026-09-30 — Line pairing: per product, exact-qty 1:1 when both sides have many (was full cross-product); paged reads with totalRecordCount guard; synced-IA-line count guard. Verify harness deleted → `process_netsuite_inventory_adjustment.php`. CTO AGREE, php-reviewer CLEAN. (jcardinal)
158
161
  - 2026-09-21 — Removed the wrong "adjustment qty must EQUAL fulfillment qty" match rule (an adjustment corrects stock independently of what shipped — proven adj 6868145: 400 adjusted vs 350 shipped); the IA→fulfillment line link is now by **PRODUCT only**, and a product with no fulfillment line **logs + skips** (service items, proven adj 6966700). Paged all 3 attach reads + the fulfillment dedup lookup (api2's 25-row unpaged cap was re-creating duplicate `ItemFulfillmentItems`). New `deleteItemFulfillmentDuplicateLines()` cleans a linked fulfillment's truncation duplicates before matching (serial-subset guard spares item-group members). Prod-verified: INVENTORY_ADJUSTMENTS reached IDLE, cursor draining, `Logs.Issue` 827 cleared. (jcardinal)
159
162
  - 2026-09-21 — Redesigned to **many-to-many**: `custbody_stock_adjustment` is multi-select (one adjustment → many fulfillments), so two bridge tables (`ItemFulfillments_InventoryAdjustments` rec 363, `ItemFulfillmentItems_InventoryAdjustmentItems` rec 364) replace the single FK columns; attach loops all fulfillments, line-matches by product quantity (fail-loud), reconciles each run with a gated delete. Prod-verified on adj 1320 (17 header + 16/16 line). (jcardinal)
160
163
  - 2026-09-03 — Adjustment→fulfillment link moved to its own `IS_ENABLED_INVENTORY_ADJUSTMENT_FULFILLMENT_LINK` flag, independent of transfer-order detection. (jcardinal)
@@ -6,7 +6,7 @@ project: Library
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-24
9
+ updated: 2026-10-02
10
10
  owners: [jcardinal, bala, rgirish]
11
11
  files:
12
12
  - library/app/api/toga2.php
@@ -17,6 +17,8 @@ files:
17
17
  - worker/crons/toga2/netsuite/sync_togasupply_hh.php
18
18
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
19
19
  - worker/crons/toga2/netsuite/process_netsuite_order.php
20
+ - worker/crons/toga2/netsuite/process_netsuite_addressbook_mapping.php
21
+ - _underscore/Trait/Netsuite/Locations/Address.php
20
22
  - dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql
21
23
  - dbchanges2/Client_Nychh/2026-09-16a - TransferOrderBridgeAcl.sql
22
24
  - dbchanges2/_modules/netsuite/2026-09-24a - TransferOrderDownstreamPoBridgeAcl.sql
@@ -32,6 +34,7 @@ related:
32
34
  - ./transfer-order-netsuite-push.md
33
35
  - ../../../2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md
34
36
  - ./location-shipping-addresses.md
37
+ - ../../../1.0/apps/worker/workflows/netsuite-addressbook-location-mapping.md
35
38
  - ../profile.md
36
39
  ---
37
40
 
@@ -41,8 +44,6 @@ The 1.0 NetSuite→`Client_Nychh.TransferOrders` importer (stocking-flag routing
41
44
 
42
45
  NYCHH's NetSuite **transfer orders are represented as NetSuite sales orders** (same as the GroWrk pattern), so the shared NetSuite → TOGa Supply importer (`App_Api_Toga2::sync*FromNetsuite`) must decide, per order, whether to write it to `Client_Nychh.SalesOrders` or `Client_Nychh.TransferOrders`. This doc covers the **1.0-side ingestion**: routing signal, PO↔order bridge links, transfer-order origin/destination resolution, the item-fulfillment transfer-order line link, and the status map. The **2.0-side** (quantity model + the model/ACL work that lets the TransferOrder POST succeed) is in [transfer-order inventory & quantities](./transfer-order-inventory-quantities.md).
43
46
 
44
- **End-to-end verified this session:** a transfer order imported into `Client_Nychh.TransferOrders` after deploy. One open edge case remains (order 128829, below).
45
-
46
47
  ## How it works
47
48
 
48
49
  ### Routing signal — the NetSuite stocking flag, NOT the dollar amount
@@ -71,7 +72,7 @@ Owned by the shared [TO↔PO link reconcile](../../../1.0/apps/library/features/
71
72
  NYCHH specifics:
72
73
  - Customer PO = NetSuite `otherRefNum` (split on `' + '`) under the synthetic **Agilant** vendor (`clientConfiguration['importCustomerPurchaseOrdersOnVendor']`). SO lines' `createdPo` is NULL for these (proof: SO 7326440 / tranid 284624 / otherrefnum EIT0012548).
73
74
  - Vendor POs (e.g. Amazon CC, Uline) exist too → downstream 326/330. Before 2026-09-24 they were wrongly written upstream: ~533/1,593 rows in 325, 3,981/5,893 in 329. Re-processing deletes them.
74
- - A missing customer PO is now **created** (header + lines) — adds stock-in via new PO lines. Verified TO id 11 → PO EIT0005971 (id 1005, 20 lines, customer 28908).
75
+ - A missing customer PO is now **created** (header + lines). New PO lines add NO stock-in (stock-in comes from the stocking SO's adjustments — [quantities](./transfer-order-inventory-quantities.md)). Verified TO id 11 → PO EIT0005971 (id 1005, 20 lines, customer 28908).
75
76
  - api2 never inlines `transfer-orders.transferOrderItems` (no `InherentRecordChildren` row) → read TO lines via `getTransferOrderItemsByUuid`.
76
77
  - ACL: 325/329 by `Client_Nychh/2026-09-16a - TransferOrderBridgeAcl.sql` (Base + API, 4-table chain + field grants); 326/330 by `_modules/netsuite/2026-09-24a`. Roles: 1 Base, 2 Developer, 3 API, 4 Public. RecordFields 325 = 2210/2211/2212, 329 = 2226/2227/2228, 326 = 2214-2216, 330 = 2230-2232 (id fields not granted). Chain rule: [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md).
77
78
  - Missing ACL is loud: existence GETs throw on no `meta->totalRecordCount`.
@@ -104,19 +105,28 @@ Verified in production 2026-09-02 (while building the **outbound** push): on a N
104
105
  **VERIFIED in prod:** after the library deploy the INVENTORY_ADJUSTMENTS cursor advanced past the blocker and ran forward through most of 2026; skipped TOs logged, no more EV-10 destination wedge. This is **separate** from the origin fallback above and from the 128829 no-location edge case.
105
106
 
106
107
  ##### Follow-on regression: the added param broke a THIRD call site → SALES_ORDERS froze at 1-RUNNING
107
- `syncTransferOrderFromNetsuite()` has **three** callers, not two: `library/app/api/toga2.php:923` (from `syncSalesOrder`) and `:4984` (the `syncItemFulfillment` cascade), **plus a DIRECT cron call** in `worker/crons/toga2/netsuite/common_sync_togasupply.php:782` (the SALES_ORDERS section). The skip fix added a param but updated only the **two library callers**; the worker cron still passed 7 args → `Logs.Event` issue **754**: *"Too few arguments to function `App_Api_Toga2::syncTransferOrderFromNetsuite()`, 7 passed in `crons/toga2/netsuite/common_sync_togasupply.php` on line 789 and at least 8 expected."* It surfaced only when SALES_ORDERS ran (cursors reset to recheck), because INVENTORY_ADJUSTMENTS reaches the function through the library-internal callers. **Fix:** added the missing 3rd arg `$lookupCustomerByClientUuidAndNetsuiteCustomerInternalId[$uuidClient]` to the `:782` call.
108
+ `syncTransferOrderFromNetsuite()` has **four** callers today (`toga2.php` x2, `common_sync_togasupply.php`, `process_netsuite_order.php`); in 2026-09 it had three: `library/app/api/toga2.php:923` (from `syncSalesOrder`) and `:4984` (the `syncItemFulfillment` cascade), **plus a DIRECT cron call** in `worker/crons/toga2/netsuite/common_sync_togasupply.php:782` (the SALES_ORDERS section). The skip fix added a param but updated only the **two library callers**; the worker cron still passed 7 args → `Logs.Event` issue **754**: *"Too few arguments to function `App_Api_Toga2::syncTransferOrderFromNetsuite()`, 7 passed in `crons/toga2/netsuite/common_sync_togasupply.php` on line 789 and at least 8 expected."* It surfaced only when SALES_ORDERS ran (cursors reset to recheck), because INVENTORY_ADJUSTMENTS reaches the function through the library-internal callers. **Fix:** added the missing 3rd arg `$lookupCustomerByClientUuidAndNetsuiteCustomerInternalId[$uuidClient]` to the `:782` call.
108
109
 
109
110
  > **Lesson (recorded on the shared engine doc too):** when you change a shared **library** function signature, **grep ALL repos** (library + worker) for callers — worker crons call library classes via the autoloader with **no `require`**, so an import search misses the cron call sites.
110
111
 
111
- **Deploy:** the library change (the skip) is deployed and verified over many cron cycles; the worker change (the missing 3rd arg + two stale-comment fixes) is the current fix — **redeploy worker** to pick it up.
112
+ ### Destination + ship-to resolution (2026-10-02; replaces the 2026-09-24 text-only rule)
113
+ Background: the outbound push sends every NYCHH TO under client customer **28908**; the real site is only on the NetSuite ship-to. Entity → `Locations.c_netsuiteInternalCustomerId` gives catch-all **Location 2**.
114
+
115
+ Rule in `syncTransferOrderFromNetsuite()` (gates: `entity == $clientConfiguration['netsuiteCustomerInternalId']` AND `isParentCustomer === true`):
116
+ 1. **Mapped address-book entry wins.** REST shim exposes the SO's `shipAddressList` id (address-book entry) + `shipOverride`. Entry → `Locations_Addresses.c_netsuiteInternalAddressbookId` (map loaded once per run; load error = fail loud). Mapping is written only by the operator CLI — [address-book mapping](../../../1.0/apps/worker/workflows/netsuite-addressbook-location-mapping.md); the sync never writes it.
117
+ 2. Else **text match** `resolveTransferOrderDestinationFromShipTo()` — zip equal AND location line1 == / word-prefix of NS addr1. `normalizeShipToText()` abbreviates street words and joins digit-hyphen-digit (Queens `79-01 Broadway` = `7901 Broadway`). Shared-building tie → location name inside addressee. Never picks the client-level location.
118
+ 3. Else keep Location 2 + `error_log` the entry id (grep `resolveTransferOrderDestinationFromShipTo:`). No throw (would freeze the section).
119
+ 4. An existing real (non-catch-all) destination is kept unless a mapped entry says otherwise. Supply-pushed TOs carry no entry id → user's pick stays.
120
+
121
+ **TO-owned ship-to address:** `TransferOrders.shipToAddressId` (standard field, Core RecordField 2682, FK `Addresses`). Import POSTs the TO's own `Addresses` row first, PUTs only on change (trimmed compare). A row shared with a Location is never edited — a new row is made. Outbound push sends it ([push](./transfer-order-netsuite-push.md)).
122
+
123
+ `syncTransferOrderFromNetsuite` takes country + state lookups to build that address. State lookup = `App_Api_Toga2::findStateUuidForNetsuiteAddress()`: state in its own country first, then any country; throws only if the state exists nowhere.
112
124
 
113
- #### FIX written 2026-09-24 (NOT deployed) — import overwrote the user's destination with catch-all location 2
114
- **Root cause (proven):** the outbound push sends every NYCHH TO under client-level customer **28908**; the real site is only on the NetSuite **ship-to**. The stocking-SO branch of `syncTransferOrderFromNetsuite()` resolves destination from `entity` → `Locations.c_netsuiteInternalCustomerId` → 28908 → **location 2**, and the change-check PUTs it over the user's choice on any later NetSuite update (e.g. status). Proof: TO **1962** / SO **290951** (internal 7520216) — `Logs_Nychh.Api` **3391680** front-end POST dest 23 (Segundo Ruiz Belvis), **3407179** import PUT (API "Agilant") dest 2.
115
- - **Keep existing destination** when `entity == $clientConfiguration['netsuiteCustomerInternalId']` AND `$clientConfiguration['isParentCustomer'] === true` AND the TO already has one. `isParentCustomer` gate: for a single-customer client the client-level customer IS the real destination.
116
- - `common_sync_togasupply.php` sets `$clientConfig['netsuiteCustomerInternalId']` from the fetched client record. Missing key → old behavior.
117
- - **New NetSuite-entered TOs under the client customer** (same gates, no existing TO): `resolveTransferOrderDestinationFromShipTo()` (+ `normalizeShipToText`, `getShipToLocations` = `GET /locations` depth 3, static per-run cache per client uuid; failure/empty logged, not cached). Match = 5-digit zip equal AND location line1 == or word-prefix of NS addr1 (addr1 can carry extra text, e.g. Queens "8268 164th St Goethels Ave and Parsons Blvd"); street words abbreviated (Parkway→PKWY, South→S…). Shared buildings (Harlem/Renaissance 506 Lenox; Segundo Ruiz Belvis/Gotham Health Belvis 545 E 142nd) → tie broken by location name inside NS addressee. Client-level location never picked. No/ambiguous match → keep location 2 + `error_log` grep `resolveTransferOrderDestinationFromShipTo:` (no throw — would freeze the section). CTO AGREE; runner-up = NetSuite addressbook-id mapping (needs new field) — revisit if logs show many misses.
118
- - **Unproven live:** depth=3 `/locations` returning `primaryLocationAddress.address` for the Agilant API role. Check the log grep after deploy.
119
- - **Data repair SQL handed to dev (not run):** TO 1962→23, 1959→24, 1960→24, 1961→9, 1966→20 (NS 291004, ship-to Queens), guarded `AND destinationLocationId = 2`.
125
+ SQL (dbchanges2): `Core/2026-10-01e - TransferOrderShipToAddressRecordField.sql`, `Client/2026-10-01c - TransferOrderShipToAddressId.sql`, `Client/2026-10-01d - TransferOrderShipToAddressApiAcl.sql`, `Client/2026-10-01e - TransferOrderShipToAddressBackfill.sql` (copies destination address into a NEW owned row per TO), `_modules/netsuite/2026-10-01a - LocationsAddressesNetsuiteAddressbookId.sql`, `_modules/netsuite/2026-10-01b - LocationsAddressesAddressbookIdCustomField.sql`. Commits: library `8f974d93`, _underscore `6839dd3e`. Tests: `test/@jeff/netsuite/test_normalize_ship_to_text.php`, `test_find_state_for_netsuite_address.php`.
126
+
127
+ ACL file `Client/2026-10-01d` (cso rules): widen only tenants that have TransferOrders; skip `appId` / `indirectRecordId` rows; bind only groups with `sqlExpression='1'`; never bind over a restricting sibling group. OPEN: Elite + Adyen have a second client-owned API key with role API — developer to decide whether it gets the grant.
128
+
129
+ Proof case: TO **291813** sat on Location 2 — NetSuite-entered, text match failed on the Queens hyphen and on multi-address sites (Woodhull 720 Flushing / 760 Broadway); old keep-existing rule blocked healing. `Logs_Nychh.Api` showed the POST created it with dest 2 (not the 9/24 overwrite bug). After mapping: Elmhurst (location 9) + own ship-to.
120
130
 
121
131
  ### Item-fulfillment transfer-order line link
122
132
  `ItemFulfillmentItems.transferOrderItemId` must populate so the 2.0 stock-out quantity calc can tie a fulfillment back to its transfer-order line. In `syncItemFulfillmentFromNetsuite`:
@@ -198,7 +208,8 @@ WARNING: NYCHH's `NETSUITE_LAST_SYNC_CURSOR_ITEM_RECEIPTS` is stale at **2024-08
198
208
 
199
209
  ## Gotchas / known issues
200
210
 
201
- - **Location 2 count (1,832 of 1,939) is not a real ship pattern** — at least partly the 2026-09-24 overwrite bug + NetSuite-entered orders under 28908. Location 2 address is now 55 Water St 10041. See the destination fix above.
211
+ - **Location 2 count (1,832 of 1,939) is not a real ship pattern** — 2026-09-24 overwrite bug + NetSuite-entered orders under 28908. Location 2 address = 55 Water St 10041. See destination resolution above.
212
+ - **`Client_Nychh` has TWO `Countries` rows with `c_netsuiteCountry = '_unitedStates'`** — id 1 (US, 52 states, ~20.8k addresses) and id 2 (stray `_t`, only NY, ~38.1k addresses). A by-NetSuite-country map keeps the last one (`_t`), so any non-NY state misses. Froze ITEM_FULFILLMENTS at 1-RUNNING 2026-10-02 (`Logs.Event` issue 935, NS order 3600216, "ship-to state CT has no TOGa State in country _unitedStates") — fixed by `findStateUuidForNetsuiteAddress()` fallback. Stray `_t` country cleanup is open.
202
213
  - **Real-TrnfrOrd item receipts are disabled — see the DEFERRED section above.** A receipt whose `createdFrom` is a real Transfer Order is skipped by design; do not read that as the importer being broken.
203
214
  - **Order 128829 (no location anywhere) is unresolved** — see the origin-resolution section. Do not assume the importer is broken if this single order is stuck; every located order imports.
204
215
  - **Routing mode is declared by the wrapper and there is NO fallback.** If a future client should route transfer orders by the stocking flag, add `const TRANSFER_ORDER_DETECTION_MODE = App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__STOCKING_FLAG;` before its `require_once 'common_sync_togasupply.php';` — and confirm that client actually sets `custbody_stocking_order` in NetSuite. Omitting the mode no longer lands everything in SalesOrders; it **throws** (deliberate — see the shared engine doc).
@@ -206,11 +217,11 @@ WARNING: NYCHH's `NETSUITE_LAST_SYNC_CURSOR_ITEM_RECEIPTS` is stale at **2024-08
206
217
  - **`custbody_stocking_order` must reach the shim.** `fetchSalesOrderById()` now carries the field via `customFieldList`; a missing custom field read returns null → the order routes as a transfer order (unchecked = transfer). Verify the field is populated in NetSuite before trusting the split.
207
218
 
208
219
  ## Change history
220
+ - 2026-10-02 — Destination: mapped address-book entry > text match (hyphen fix) > Location 2; TO-owned `shipToAddressId`; state lookup falls back across countries (issue 935 hotfix). (jcardinal)
221
+ - 2026-09-24 — Keep user destination on client-customer TOs; ship-to text match for new NS-entered TOs. (jcardinal)
209
222
  - 2026-09-24 — PO links now via the shared two-direction reconcile; vendor POs moved off upstream 325/329 onto downstream 326/330; 326/330 ACL fixed (403 EZ-1 resolved); missing customer POs are created; new `process_netsuite_order.php`. Prod-verified. (jcardinal)
210
223
  - 2026-09-21 — Fixed issue 587 (`ITEM_FULFILLMENTS` freeze): transfer-order fulfillment line was matched by `lineNumber` (= NetSuite stable `tl.id`) vs the shim's `orderLine` (= source `linesequencenumber`), which diverge on edited orders. Added `c_netsuiteLineUniqueKey` (new trait + Nychh model, `_modules/netsuite/` column/registration); match by it first, fall back to `lineNumber`. Needs a SALES_ORDERS cursor rewind on deploy. (jcardinal)
211
224
 
212
- - 2026-09-24 — Keep user destination on client-customer TOs (parent clients); ship-to → Location match for new NS-entered TOs. Not deployed. (jcardinal)
213
-
214
225
  ## Related
215
226
  - [NetSuite → TOGa Supply per-client sync (engine)](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)
216
227
  - [toga2 API client and bridge](../../../1.0/apps/library/features/toga2-api-client-and-bridge.md)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-24
9
+ updated: 2026-10-02
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - _underscore/Model/Nychh/Item.php
@@ -58,11 +58,8 @@ Per the 2026-08-17 and 2026-08-27 "TOGa Supply" meetings. **NYCHH ONLY** — the
58
58
 
59
59
  `_qtyOnOrder` was added alongside these. The stock-out side reads transfer orders (which is why the 1.0 importer must populate `ItemFulfillmentItems.transferOrderItemId` — see the import doc).
60
60
 
61
- #### Stock-in now reads inventory adjustments, NOT PO item receipts (changed 2026-08-31)
62
- Per the **2026-08-27 TOGa Supply meeting**: NetSuite has no "stocking" concept, so the only way stock is added is an **inventory adjustment** linked to the stocking order's item fulfillment (never directly to the PO). So `_Model_Nychh_Item::_qtyReceived` was changed to the **net `SUM(InventoryAdjustmentItems.quantityAdjustBy)`**, joined `InventoryAdjustmentItems.itemFulfillmentItemId → ItemFulfillmentItems.salesOrderItemId → SalesOrderItems` where `SalesOrderItems.itemId = Items.id`. This **supersedes** the old PO-item-receipt basis. Because on-hand and backordered both derive from `received`, they now use adjustments too.
63
- - **The adjustment↔fulfillment link** is the direct FK `InventoryAdjustmentItems.itemFulfillmentItemId` (recent; NULL on older adjustments). Details on [inventory-adjustment → item-fulfillment linking](./netsuite-inventory-adjustment-fulfillment-link.md).
64
- - **Implicit stocking filter:** joining through `ItemFulfillmentItems.salesOrderItemId` counts only **stocking-side** fulfillments — transfer-side fulfillment items have that column NULL — and all Client_Nychh SalesOrders are stocking orders by the import routing.
65
- - **⚠ Reads 0.00 today, by design.** Verified in prod Client_Nychh 2026-08-31: `InventoryAdjustmentItems` has 292 rows but **0** carry `itemFulfillmentItemId`, and all 351 `ItemFulfillmentItems` are transfer-side (`salesOrderItemId = 0`). INNER JOINs make an unlinked adjustment contribute 0, so every item reads 0 **until the NetSuite integration backfills the link** — expected, not a bug.
61
+ #### Stock-in = inventory adjustments linked to the stocking SO's fulfillment (since 2026-08-31)
62
+ NetSuite has no "stocking" concept: stock enters only by an **inventory adjustment** linked (NetSuite `custbody_stock_adjustment`) to the stocking SO's item fulfillment — never to the PO. `_qtyReceived` = net Σ `InventoryAdjustmentItems.quantityAdjustBy` through the line bridge `ItemFulfillmentItems_InventoryAdjustmentItems` → stocking `ItemFulfillmentItems` → `SalesOrderItems` (link import: [IA → IF linking](./netsuite-inventory-adjustment-fulfillment-link.md)). Joining through the stocking SO counts only stocking-side fulfillments. An unlinked adjustment contributes 0.
66
63
 
67
64
  ### Per-PO-line quantities — `_Model_Nychh_PurchaseOrderItem` (new 2026-08-31)
68
65
  `_Model_Nychh_PurchaseOrderItem extends _Model_Client_PurchaseOrderItem` gives the **same quantities as Item level, but limited to a single purchase-order line** (previously these existed only at Item level), which NYCHH needs for PO-specific fulfillment.
@@ -79,7 +76,7 @@ The item-level bridges mirror the header-level SO↔PO direction split documente
79
76
 
80
77
  Verified 0 SO lines map to >1 PO line, so PO-line `received` **cannot double-count**. Optional deferred hardening (not done): a `UNIQUE(purchaseOrderItemId, salesOrderItemId)` on the bridge as defense-in-depth.
81
78
 
82
- - **PO-level outbound reads `PurchaseOrderItems_TransferOrderItems` (329, upstream customer-PO lines).** Populated in prod since 2026-09-16; until 2026-09-24 it also held ~3,981 **vendor** PO lines (wrong direction), now moved to 330 by the [TO↔PO link reconcile](../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md). The reconcile also creates missing customer-PO lines, which adds stock-in.
79
+ - **PO-level outbound reads `PurchaseOrderItems_TransferOrderItems` (329, upstream customer-PO lines).** Populated in prod since 2026-09-16; until 2026-09-24 it also held ~3,981 **vendor** PO lines (wrong direction), now moved to 330 by the [TO↔PO link reconcile](../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md). A PO line the reconcile creates has no stocking SO link → adds NO stock-in. Shipped/committed land on the PO line the matcher picked, so a TO linked to an unstocked duplicate line shows wrong availability — fixed by the 2026-10-02 stocked-line ranking.
83
80
 
84
81
  #### The override is NOT on every branch — NYCHH PO-line quantities differ per environment
85
82
  Verified 2026-09-01 with `git ls-tree` against each `origin/` branch of `_underscore`:
@@ -133,51 +130,14 @@ The hand-run check used the base formula; the API ran the NYCHH override. **The
133
130
 
134
131
  Also eliminated and worth not repeating: ACL (bypassed entirely on the explicit-`fields=` path — see [v2-rest-query-contract](../../../2.0/apps/api2/features/v2-rest-query-contract.md)), `calcDepth` (echoed 3; the gate at `V2.php:7231` is `depth <= calcDepth`), wrong database (`Core.DatabaseHosts` maps "Sandbox Client" for `Client_Nychh` to `client.sandbox.database.togahub.com`, matching the API the frontend targets), deploy lag (the base `_qtyReceived` formula is unchanged across the 2026-08-19 rewrite), wrong table (`_Model_Client_ItemReceiptItem::TABLE = 'ItemReceiptItems'`), and missing data (sandbox has 823 PO lines, 207 `ItemReceiptItems`, 122 lines with available >= 1).
135
132
 
136
- #### OPEN BLOCKER — the override reads 0 for 100% of NYCHH PO lines, in sandbox AND production
137
- The override's chain `INNER JOIN`s `InventoryAdjustmentItems ON itemFulfillmentItemId = ItemFulfillmentItems.id`. **That column is NULL on every row in both environments** (verified 2026-09-01):
133
+ #### PO-line "available" — what Toga computes (2026-10-02)
134
+ `_qtyAvailable` on a NYCHH customer PO line = **received − shipped − committed**:
135
+ - **received** — adjustments linked to the stocking SO's fulfillment (above), scoped to the PO line via `PurchaseOrderItems_SalesOrderItems`.
136
+ - **shipped** — TO fulfillments through `PurchaseOrderItems_TransferOrderItems` (329).
137
+ - **committed** — `TransferOrderItems.qtyCommitted`, copied from NetSuite `quantityCommitted`. Committed stays stale after shipping (= double subtract) only when the TO was not re-synced.
138
+ "No available qty" triage (cursor / Toga bug / NetSuite-human buckets, worked cases): [PO availability triage](../workflows/po-availability-triage.md).
138
139
 
139
- | Environment | `InventoryAdjustmentItems` rows | rows with `itemFulfillmentItemId` set |
140
- |---|---|---|
141
- | sandbox `Client_Nychh` | 292 | **0** |
142
- | production `Client_Nychh` | 317 | **0** |
143
-
144
- Production PO lines the override would report with qty > 0: **0, out of all lines.** An INNER JOIN makes an unlinked adjustment contribute nothing, so `_qtyReceived` — and therefore `_qtyOnHand` and `_qtyAvailable` — is **0.00 for every NYCHH purchase-order line today.**
145
-
146
- This is precisely the "Old-data caveat" the override file states in its own header comment (NetSuite began linking inventory adjustments to fulfillments less than a year ago; unlinked adjustments carry a NULL `itemFulfillmentItemId` and are excluded until the integration backfills them). **It is not a corner case — it is currently 100% of the data.**
147
-
148
- **Consequence: the Create Transfer Order item picker cannot show a single selectable line for NYCHH.** The picker keeps only rows with availability >= 1, and there are none. This is **unresolved and needs a decision from the integration / data owner** — it is *not* a frontend fix. Two options:
149
- - **(a)** the NetSuite integration backfills `InventoryAdjustmentItems.itemFulfillmentItemId`, or
150
- - **(b)** the override's definition of "stock in" is changed (e.g. back to PO item receipts, which return real numbers today — 40.00 on the reference row).
151
-
152
- Until one lands, treat any NYCHH transfer-order availability figure as **structurally 0**, not as a bug to debug. Tracking the adjustment↔fulfillment link: [inventory-adjustment → item-fulfillment linking](./netsuite-inventory-adjustment-fulfillment-link.md).
153
-
154
- ##### Second reference row, and two more eliminations (2026-09-02)
155
- Re-hit on a **different** line and re-investigated from scratch. Reference row: `PurchaseOrderItems` uuid **`715ef031-003d-6742-871d-0d20de2fa40f`** (PO **80622**, line **21**).
156
-
157
- | Source | `quantity` | `_qtyReceived` | `_qtyFulfilled` | `_qtyOnHand` | `_qtyAvailable` |
158
- |---|---|---|---|---|---|
159
- | V2 API response | **3270** (correct) | 0 | 0 | 0 | 0 |
160
- | hand-run SQL on `Client_Nychh` | 3270 | **3270** | 0 | — | — |
161
-
162
- `quantity` coming back correct on the very same object is the tell that this is **not** a fetch, route, database or deploy problem. Two further eliminations:
163
- - **The values arrive as a literal `0`, not as `null` and not absent.** That rules out `AclFieldPermissions` **and** an insufficient `calcDepth` in one step — both of those produce a missing/`null` field, never a `0`. (Consistent with the rule now on [v2-rest-query-contract](../../../2.0/apps/api2/features/v2-rest-query-contract.md): a calculated field returning `0` is almost never an ACL problem.)
164
- - **No physical shadowing columns on `PurchaseOrderItems`.** A stored column of the same name would win over the `FIELD_SQL` expression; the blank client template creates only **7** columns on that table, so there is nothing to shadow.
165
-
166
- **The receipts genuinely exist** (3270 received on this line), so the numbers are recoverable — exactly the shape of option **(b)** above. Left **UNRESOLVED and handed to Jeff**; it needs the same integration/data-owner decision, and the evidence narrows it to the override's stock-in source rather than anything in the request path.
167
-
168
- ##### 2026-09-14 — PARTIALLY cleared on sandbox: 4 lines now report availability (blocker still OPEN)
169
- Re-measured on `sandbox-client`: of **1,238** NYCHH purchase-order lines, **4** now report `_qtyAvailable >= 1`. The other **1,234 still report 0.**
170
-
171
- | `PurchaseOrderItems.uuid` | `quantity` | `_qtyAvailable` |
172
- |---|---|---|
173
- | `31c5871e-97da-7838-b3aa-ac8b9a21cf01` | 29 | 29 |
174
- | `38e83217-759d-bf7d-8c27-940501603473` | 7 | 7 |
175
- | `67039882-8aed-d68d-dcd5-cbf089b4ab6b` | 4 | 4 |
176
- | `f574526e-3de1-f182-e8ba-3945ecd36765` | 100 | 100 |
177
-
178
- On all four, availability equals the **full ordered quantity** — i.e. these are newly linked lines with nothing dispatched yet, not partial recoveries. **Not verified on production.**
179
- - **Changed:** "the picker shows nothing for NYCHH" is now stale for `sandbox-client` — it shows 4. Anyone testing the Create Transfer Order flow there now has real selectable rows.
180
- - **NOT changed:** the underlying blocker stands. The unbackfilled `InventoryAdjustmentItems.itemFulfillmentItemId` link is still NULL on ~all rows, and options **(a)** and **(b)** above still need the integration / data-owner decision. Do not close this on the strength of 4 rows.
140
+ History: 2026-09-01→09-14 every PO line read 0 because the old single FK `InventoryAdjustmentItems.itemFulfillmentItemId` was NULL on all rows; superseded by the many-to-many bridge import.
181
141
 
182
142
  #### The role-3 grant migration is NOT the fix for the empty picker
183
143
  `dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql` grants **role 3 (API)** READ (`isWritable = 0`) on all eight `_qty*` `RecordFields` (**1500–1504, 1524–1526**), guarded by `NOT EXISTS` on the `(recordFieldId, roleId)` unique key.
@@ -187,7 +147,7 @@ On all four, availability equals the **full ordered quantity** — i.e. these ar
187
147
  **Keep it anyway.** It closes a real and *separate* gap on the `getFullModelData()` (no-explicit-`fields=`) path, where these fields genuinely are ACL-gated, and 31 of 36 prod tenants have the same hole. It was **applied on sandbox `Client_Nychh`** and is **cleanly reversible** via its eight hardcoded uuid literals (`DELETE … WHERE uuid IN (…)`) — which is why it uses literals rather than generated uuids; see [re-runnable additive migrations](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md). Fleet survey and the "grant explicitly, don't mirror a sibling" rule it produced: [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md).
188
148
 
189
149
  #### Index coverage / schema
190
- No `dbchanges2` file — pure calculated `FIELD_SQL` in PHP. All correlation columns (`InventoryAdjustmentItems.itemFulfillmentItemId`, `ItemFulfillmentItems.salesOrderItemId`, `SalesOrderItems.itemId`, both bridge FKs) are already indexed.
150
+ No `dbchanges2` file — pure calculated `FIELD_SQL` in PHP. All correlation columns (line-bridge FKs, `ItemFulfillmentItems.salesOrderItemId`, `SalesOrderItems.itemId`, both bridge FKs) are already indexed.
191
151
 
192
152
  ### V2 field enablement — how the TransferOrder POST was made to work
193
153
  Posting a NetSuite-sourced transfer order that carries NetSuite custom fields required **every** part of the custom-field recipe on the NYCHH client DB. Each missing part surfaced as a distinct V2 error, peeled in order:
@@ -227,12 +187,12 @@ Discovered 2026-08-28 while standing the Transfer Orders UI up locally: beyond t
227
187
 
228
188
  - **A custom field is writable/searchable ONLY when BOTH the model declares it (via the NetSuite trait on the client-override model) AND the ACL/metadata rows exist.** Declaring without the ACL row, or granting the ACL without the declaration, each fails with its own error — see the peeled sequence above.
229
189
  - **These quantity overrides are NYCHH-only.** They live on `_Model_Nychh_Item` and `_Model_Nychh_PurchaseOrderItem`; do not lift them to the base `_Model_Client_*` — other clients do not have the stocking/transfer two-branch model.
230
- - **A 0.00 quantity for NYCHH is expected today, not a bug.** Stock-in reads inventory-adjustment links that are not backfilled yet.
231
- - 🚨 **Every NYCHH PO line reads `_qtyReceived`/`_qtyOnHand`/`_qtyAvailable` = 0.00 right now**, in sandbox **and** production, because `InventoryAdjustmentItems.itemFulfillmentItemId` is NULL on 100% of rows and the override INNER JOINs on it. Blocks the Create Transfer Order picker; needs an integration/data decision, not code. See the OPEN BLOCKER section.
190
+ - **A 0.00 received on a NYCHH PO line usually means the NetSuite adjustment link is blank** (NetSuite/human issue) — triage before debugging code.
232
191
  - **⚠ `_Model_Nychh_PurchaseOrderItem` is on `_production`/`_sandbox-client` but NOT on `_beta`/`_sandbox-dev`** — so NYCHH PO-line quantities are genuinely different numbers per tier. Establish the deployed `_underscore` branch before comparing environments or trusting a repro.
233
192
  - **Never eliminate a tenant model override by `ls`-ing your working tree.** Use `git cat-file -e origin/<deployed-branch>:Model/<Slug>/<Model>.php`. This exact mistake cost a session — and a *passing* drift check on the base `Model/Client/` file is what made it feel safe.
234
193
 
235
194
  ## Change history
195
+ - 2026-10-02 — Rewrote stock-in for the IA↔IF line bridge; documented PO-line available = received − shipped − committed; removed stale 100%-zero blocker. (jcardinal)
236
196
  - 2026-09-24 — 329 no longer empty; vendor PO lines moved off it to 330. (jcardinal)
237
197
 
238
198
  ## Related
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-24
9
+ updated: 2026-10-02
10
10
  owners: ["bala", "jcardinal"]
11
11
  files:
12
12
  - _underscore/Model/Client/TransferOrder.php
@@ -155,15 +155,12 @@ on 5050. The mechanism, the failed workarounds, and the cross-client -8 list are
155
155
 
156
156
  ## Where the ship-to address comes from — and the location-2 problem
157
157
 
158
- The address is the **destination Location's** `primaryLocationAddress.address`. Until 2026-09-09 all
158
+ Since 2026-10-02 the push sends the TO's own `shipToAddress` (`TransferOrders.shipToAddressId`, set by the import from the NetSuite ship-to — [import](./netsuite-transfer-order-import.md)); fallback = the **destination Location's** `primaryLocationAddress.address`. Until 2026-09-09 all
159
159
  **24** NYCHH shipping locations shared the head-office Accounts Payable billing address, so even a
160
160
  correct push would have shipped everything to 50 Water Street; that backfill and its open questions
161
161
  are in [location shipping addresses + delivery contacts](./location-shipping-addresses.md).
162
162
 
163
- **Still open and still wrong for the push:** `Locations.id = 2` “NYC Health + Hospitals” carries
164
- `Addresses.id 32536` = **1400 Pelham Parkway South**, which is **Jacobi's** address, not a real
165
- delivery site for that location — and location 2 is the destination on the large majority of NYCHH
166
- transfer orders. Any transfer order to location 2 will ship to Jacobi.
163
+ **Superseded:** location 2 no longer holds Jacobi's 1400 Pelham address (now 55 Water St 10041), and TOs carry their own ship-to — see [location shipping addresses](./location-shipping-addresses.md).
167
164
 
168
165
  ### What "no address" actually did — 5 of 6 orders went to the wrong hospital
169
166
 
@@ -228,6 +225,7 @@ email, which must fire *after* this push writes `number` back. The `POST` row ab
228
225
  It may also need record ACL rows for 325 — untested; an `EZ-1` would be the symptom.
229
226
 
230
227
  ## Change history
228
+ - 2026-10-02 — Push uses the TO-owned `shipToAddress` when set; location-2 Jacobi-address note superseded. (jcardinal)
231
229
  - 2026-09-24 — PO # bridge-only (purchaseOrderId column dropped); 326/330 no longer dead (downstream vendor POs). (jcardinal)
232
230
 
233
231
  - 2026-09-17 — Noted that record **312 now carries a second `PUT` interceptor row**
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: nychh
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-10-01
21
+ updated: 2026-10-02
22
22
  owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
23
23
  files:
24
24
  - dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
@@ -66,6 +66,10 @@ TOGA 2.0 client on `_underscore`, prod schema `Client_Nychh`. Supply-chain / ful
66
66
  - **Worker tiers:** `worker2` (2.0) runs the NYCHH asset-tag backfill; `worker`/`library` (1.0) run the NetSuite→TOGa Supply sync and the asset-tag verification/backfill diagnostics.
67
67
 
68
68
  ## Key features (this client)
69
+ - **Prod NetSuite feeds (2026-10-02):** only ITEM_FULFILLMENTS + INVENTORY_ADJUSTMENTS run (worker `511a7fd9`, 2026-09-30), cursors rewound to 2022/2023.
70
+ - **TO destination + own ship-to (2026-10-02, library `8f974d93`, _underscore `6839dd3e`).** NetSuite-entered TOs landed on catch-all Location 2 (text match failed on Queens hyphens + multi-address sites). Destination now = mapped NetSuite address-book entry → text match → Location 2; TOs own `shipToAddressId`; Location auto-create uses default shipping. Mapping: 61 entries → 18 sites. See [TO import](./features/netsuite-transfer-order-import.md), [address-book mapping](../../1.0/apps/worker/workflows/netsuite-addressbook-location-mapping.md).
71
+ - **TO↔customer-PO line matching fixed (2026-10-02, `1bc4de52`).** Old matcher picked unstocked duplicate PO lines and created empty PO lines; now ranks stocked lines and allows reuse. 161 TOs re-processed; 86 bad links over 62 TOs remained at last measure (re-process result pending). See [reconcile](../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md).
72
+ - 🚩 **Two `Countries` rows share `c_netsuiteCountry = '_unitedStates'`** (id 1 US; id 2 stray `_t`, NY only, ~38k addresses). Froze ITEM_FULFILLMENTS 2026-10-02 (issue 935); hotfixed by `findStateUuidForNetsuiteAddress()` fallback. Stray `_t` cleanup OPEN.
69
73
  - **All 24 shipping locations shared ONE address until 2026-09-09 — the head-office Accounts Payable bill-to (50 Water St).** Each location had its own `Addresses` row (261-303) but they held identical values, and locations 1 + 2 shared `Addresses.id = 1`. `Client_Nychh/2026-09-09c - UpdateLocationShippingAddresses.sql` backfilled each from its NetSuite customer's `defaultshipping` address (applied + verified in prod: 22 distinct addresses over 24 locations). It only ever showed on the **transfer-order** screens, because `TransferOrders` has no ship-to column while `SalesOrders` carries its own `shipToAddressId`. Catch-all **location 2 "NYC Health + Hospitals"** now at 55 Water St 10041; its big TO count was mostly an import overwrite bug (fix 2026-09-24, see [NetSuite TO import](./features/netsuite-transfer-order-import.md)), and Bellevue/Jacobi carry NetSuite customer ids (29275 / 31917) that no longer exist. See [location shipping addresses](./features/location-shipping-addresses.md).
70
74
  - 🚨 **sandbox-client (`Core` + `Client_Nychh`) was reset from PRODUCTION again on 2026-09-04** — second reset in two days (first, 2026-09-03, over the stale `purchase-orders` TableView). Every "verified on <date>" sandbox-client claim in this profile dated **before 2026-09-04 now describes whatever prod had**. The 2026-09-01 → 2026-09-03 `dbchanges2` migrations may need re-running on sandbox-client. Re-read the environment before trusting any sandbox state; procedure + re-run order in [Targeting the right database](../../2.0/apps/dbchanges2/workflows/targeting-the-right-database.md).
71
75
  - **Transfer orders push OUT to NetSuite as $0 sales orders - LIVE and verified 2026-09-08.** NYCHH is the first and only client on that path; switched on purely by its `transfer-orders` (record 312) `POST`/`POST` interceptor row. NetSuite SO **289187** (id 7463537) was created from it. **The NetSuite customer is the CLIENT - 4490 / 28908 - on every transfer order, never the destination location** (a first cut sent the destination and put SO 7462031 on sub-customer 33674 "CHS"; `Locations` 1-26 are "Shipping" type mapping to sub-customers under 4490, and the bug hid because almost every order picks location 2, whose id *is* 28908). Origin location is still the NetSuite **warehouse**, and `TransferOrders.description` now goes out as the NetSuite `memo`, shown as **ORDER DESCRIPTION**. See [TransferOrder → NetSuite push](./features/transfer-order-netsuite-push.md).
@@ -105,10 +109,10 @@ TOGA 2.0 client on `_underscore`, prod schema `Client_Nychh`. Supply-chain / ful
105
109
  - 🚨 **The transfer-order ACL + schema on prod/sandbox are UNTRACKED (2026-08-28).** Record and field ACL for records 312/313, their logic-group/expression chain, and the `TransferOrderItems.dtCreated`/`dtUpdated` columns were all hand-applied when the NetSuite TransferOrder POST was enabled — **no `dbchanges2` file creates any of it**, so a fresh environment can reproduce neither the UI nor the importer. Reconstructed in `Client_Nychh/2026-08-28c` (ACL, faithful — role 2 deliberately excluded) and `Client/2026-08-28b` (columns; root cause is the blank client template, which affects **every** tenant). See [transfer-order inventory & quantities](./features/transfer-order-inventory-quantities.md).
106
110
  - **Create Transfer Order links POs (2026-09-23)** — one POST writes header bridge 325 + line bridge 329 (Core `2026-09-23d` inherent children). 325/329 = upstream customer PO, 326/330 = downstream vendor POs (live since 2026-09-24, see [TO↔PO link reconcile](../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md)). `TransferOrders.purchaseOrderId` dropped 2026-09-24 → read `_purchaseOrderNumbers`. See [transfer-order push](./features/transfer-order-netsuite-push.md).
107
111
  - **Two-branch inventory quantities + TransferOrder V2 enablement** — NYCHH-only on-hand/available/fulfilled/backordered model on `_Model_Nychh_Item`, plus the client-override models + ACL rows that let a NetSuite TransferOrder POST succeed. See [transfer-order inventory & quantities](./features/transfer-order-inventory-quantities.md).
108
- - 🚨 **OPEN BLOCKER — every NYCHH PO line reports availability 0, in sandbox AND production (verified 2026-09-01).** The deployed `_Model_Nychh_PurchaseOrderItem` sources stock-in from inventory adjustments and `INNER JOIN`s `InventoryAdjustmentItems.itemFulfillmentItemId`, which is **NULL on 100% of rows** (292/292 sandbox, 317/317 prod). So `_qtyReceived` / `_qtyOnHand` / `_qtyAvailable` are all `0.00` for every purchase-order line, and the **Create Transfer Order item picker has nothing to show**. Needs an **integration / data-owner decision** — backfill the FK, or change the override's stock-in source (base PO item receipts return real numbers today). Not a frontend or ACL defect, and **not** fixed by `Client/2026-09-01a`. See [transfer-order inventory & quantities](./features/transfer-order-inventory-quantities.md).
112
+ - **PO-line availability = received − shipped − committed** (stock-in = adjustments linked to the stocking SO's fulfillment). The 2026-09 "every line reads 0" blocker is superseded by the IA↔IF bridge import. "No available qty" → [PO availability triage](./workflows/po-availability-triage.md); a blank `custbody_stock_adjustment` is a NetSuite/human issue, even pre-2024.
109
113
  - 🚨 **`_Model_Nychh_PurchaseOrderItem` is NOT on every `_underscore` branch (verified 2026-09-01).** `Model/Nychh/PurchaseOrderItem.php` exists on **`_production`** and **`_sandbox-client`** but is **absent from `_beta`** and **`_sandbox-dev`**. Because `_underscore` is pulled at deploy by branch, NYCHH PO-line `_qty*` values are **different numbers per tier**: prod/sandbox-client run the inventory-adjustment formula (0.00 until the NetSuite link is backfilled), beta/sandbox-dev fall back to the base item-receipt formula and report real quantities. **Never eliminate a tenant override by `ls`-ing your working tree** — use `git ls-tree -r --name-only origin/<branch>`. That mistake cost a full session chasing a non-existent "API returns 0" bug. See [transfer-order inventory & quantities](./features/transfer-order-inventory-quantities.md).
110
114
  - **Create Transfer Order modal is scoped by PURCHASE ORDER, not by location (2026-09-01).** Source Location is the one active `allowSending = 1` location, read-only from open; all lines on a transfer must share one PO. `Locations.allowReceiving` is **unmaintained** for this tenant (1 of 31 active locations, and it is the source warehouse), so the target dropdown lists all active locations minus the source. The populated contact join is **`Contacts_Locations`** (record 76), not `Locations_Contacts`; contacts were sparse until 2026-09-09, when 12 test rows were replaced by **19 real delivery contacts** (18 location links). See [Transfer Orders page](../../2.0/apps/toga25-supply/features/transfer-orders-page.md) and [location shipping addresses + delivery contacts](./features/location-shipping-addresses.md).
111
- - **Inventory-adjustment → item-fulfillment linking** — imports NetSuite `custbody_stock_adjustment` (adjustment→fulfillment) into two all-client bridges for adjustment→fulfillment→SO→PO traceability; gated NYCHH-only, best-effort, with on-demand fulfillment import. Go-live toggle (`IS_ENABLED_INTEGRATION_INVENTORY_ADJUSTMENTS`) still off. Line bridge cross-product (IA 1312/1318) fixed 2026-09-30, NOT deployed — re-import both after worker deploy. See [inventory-adjustment → item-fulfillment linking](./features/netsuite-inventory-adjustment-fulfillment-link.md).
115
+ - **Inventory-adjustment → item-fulfillment linking** — imports NetSuite `custbody_stock_adjustment` (adjustment→fulfillment) into two all-client bridges for adjustment→fulfillment→SO→PO traceability; gated NYCHH-only, best-effort, with on-demand fulfillment import. Go-live toggle (`IS_ENABLED_INTEGRATION_INVENTORY_ADJUSTMENTS`) still off. Line-pairing fix `df03dd8f` on `_production` since 2026-09-29. The IA→IF→SO chain imports only missing records, never refreshes existing ones. See [inventory-adjustment → item-fulfillment linking](./features/netsuite-inventory-adjustment-fulfillment-link.md).
112
116
  - **PO links are UPSTREAM (`PurchaseOrders_SalesOrders`), not downstream.** The downstream `sales-order-purchase-orders` route returns an empty array for NYCHH by design, and the shared `_purchaseOrders` field (downstream-only) resolves to `null` for this tenant — open gap. See [PO Number direction](./features/po-number-upstream-direction.md).
113
117
 
114
118
  ## Notes
@@ -0,0 +1,56 @@
1
+ ---
2
+ title: "NYCHH PO-line \"no available qty\" triage (cursor / Toga bug / NetSuite-human)"
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: nychh
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-10-02
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Model/Nychh/PurchaseOrderItem.php
13
+ - library/app/api/toga2.php
14
+ - worker/crons/toga2/netsuite/process_netsuite_order.php
15
+ - worker/crons/toga2/netsuite/process_netsuite_inventory_adjustment.php
16
+ related:
17
+ - ../features/transfer-order-inventory-quantities.md
18
+ - ../features/netsuite-inventory-adjustment-fulfillment-link.md
19
+ - ../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md
20
+ - ../../../1.0/apps/worker/workflows/reprocessing-one-netsuite-order.md
21
+ - ../profile.md
22
+ ---
23
+
24
+ Steps to classify why a NYCHH customer PO line shows no available qty in Toga, and how to write Ops fix steps; open when Ops sends a list of POs that "show 0 available".
25
+
26
+ ## Summary
27
+ - Available = received − shipped − committed ([formula](../features/transfer-order-inventory-quantities.md)).
28
+ - Every line lands in one bucket: **cursor not caught up**, **Toga bug**, or **NetSuite/human**.
29
+ - Rule (developer decision 2026-10-02): a BLANK `custbody_stock_adjustment` on the stock-in adjustment is **NetSuite/human**, even for pre-2024 adjustments.
30
+
31
+ ## How it works
32
+ Per PO line:
33
+ 1. **Stocking SO** — find the stocking SO linked to the PO line (`PurchaseOrderItems_SalesOrderItems`). None / stocking flag unchecked in NetSuite → NetSuite/human.
34
+ 2. **Stocking fulfillment** — unfulfilled stocking lines are usually service items or not shipped yet → not a bug.
35
+ 3. **Stock-in adjustment** — read `custbody_stock_adjustment` raw on each adjustment id. Blank or incomplete → NetSuite/human. Linked in NetSuite but no Toga bridge row → re-run the IA (cursor / missing import), else Toga bug.
36
+ 4. **Shipped** — TO fulfillments on the PO line via 329. A TO that shipped before stock arrived → NetSuite/human (timing). A TO linked to the wrong PO line → Toga bug (re-process the TO).
37
+ 5. **Committed** — `TransferOrderItems.qtyCommitted` vs NetSuite `quantityCommitted`. Stale after ship → TO not re-synced (cursor bucket).
38
+ 6. **Item mismatch** — part numbers are matched exactly; a variant (`210-BKVZ` vs `210-BKVZ_`) on the adjustment vs PO line → NetSuite/human.
39
+ 7. **PO # mismatch** — TO carries a different PO # in `otherRefNum` (memo may hold the real one) → NetSuite/human.
40
+
41
+ Fixes: Toga bug → fix code, then re-process ([process_netsuite_*](../../../1.0/apps/worker/workflows/reprocessing-one-netsuite-order.md)); never data-repair SQL.
42
+
43
+ **Ops steps format:** check each record's CURRENT NetSuite value first, then write "IA #902: `custbody_stock_adjustment` currently blank → set to IF <id>". Never write a change step from memory.
44
+
45
+ ## Gotchas
46
+ - **SuiteQL `custbody_stock_adjustment LIKE '%<id>%'` in WHERE silently returns 0 rows.** Read the field per adjustment id.
47
+ - Worked cases (2026-10-02):
48
+ - Blank links: EIT0010365, EIT0010360, COF0015624 (IA 728/730/787); batch 2 IA #902/#925/#893/#1948.
49
+ - Shipped before stock: EIT0012615 (TO 253655).
50
+ - Stocking flag unchecked: EIT0012426 (stocking order 233366) — Ops fixed.
51
+ - Item variant: IA #1069 `210-BKVZ` vs `210-BKVZ_`.
52
+ - PO # mismatch: EIT0011884 — TOs carry PO # EIT0011933, memo EIT0011884.
53
+ - Wrong PO line picked by the old matcher → [reconcile](../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md) (fixed `1bc4de52`).
54
+
55
+ ## Change history
56
+ - 2026-10-02 — Created from two triage batches (5 + 22 POs). (jcardinal)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.935",
3
+ "version": "1.0.936",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",