toga-ai 1.0.889 → 1.0.891

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.
Files changed (21) hide show
  1. package/knowledge/1.0/apps/library/INDEX.md +1 -0
  2. package/knowledge/1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md +63 -0
  3. package/knowledge/1.0/apps/worker/INDEX.md +1 -0
  4. package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +23 -9
  5. package/knowledge/1.0/apps/worker/workflows/reprocessing-one-netsuite-order.md +52 -0
  6. package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
  7. package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md +22 -3
  8. package/knowledge/2.0/apps/toga25-supply/features/transfer-orders-page.md +8 -4
  9. package/knowledge/2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md +10 -3
  10. package/knowledge/INDEX.md +2 -2
  11. package/knowledge/clients/elite/features/netsuite-togasupply-sync.md +7 -1
  12. package/knowledge/clients/elite/profile.md +5 -5
  13. package/knowledge/clients/growrk/features/transfer-order-flow.md +7 -2
  14. package/knowledge/clients/nychh/features/location-shipping-addresses.md +2 -2
  15. package/knowledge/clients/nychh/features/netsuite-transfer-order-import.md +24 -42
  16. package/knowledge/clients/nychh/features/po-number-upstream-direction.md +6 -3
  17. package/knowledge/clients/nychh/features/transfer-order-inventory-quantities.md +6 -3
  18. package/knowledge/clients/nychh/features/transfer-order-netsuite-push.md +9 -6
  19. package/knowledge/clients/nychh/features/transfer-order-notification-emails.md +2 -1
  20. package/knowledge/clients/nychh/profile.md +1 -1
  21. package/package.json +1 -1
@@ -25,3 +25,4 @@
25
25
  | [NetSuite Sync Alert Monitor (App_SystemMonitor_NetSuiteIntegration)](features/netsuite-sync-alert-monitor.md) | 1.0 monitor `App_SystemMonitor_NetSuiteIntegration` — flags stale per-client NetSuite sync checkpoints (>48h) to ClickUp; open when a sync-stale alert is missin |
26
26
  | [Startech PC Matic B2B Sync (library)](features/startech-pcmaticb2b-sync.md) | PC Matic B2B bidirectional ticket sync between TOGaDesk 1.0 and TOGA 2.0 (`App_Api_Toga2`); open for ticket-type maps, StarTech client onboarding, and the V1 th |
27
27
  | [App_Api_Toga2 — TOGa2 API Client & 1.0↔2.0 Sync Bridge](features/toga2-api-client-and-bridge.md) | `App_Api_Toga2` — the 1.0-side client for the TOGa2/api2 REST API + cross-framework sync routines (`send`/`authenticate`, options DSL, `syncWithToga`/`syncWithT |
28
+ | [Transfer order ↔ PO link reconcile (both directions, header + line, from NetSuite)](features/transfer-order-purchase-order-link-reconcile.md) | `App_Api_Toga2::reconcileTransferOrderPurchaseOrderLinks()` makes one TO's PO links (upstream + downstream, header + line) match NetSuite; open before touching |
@@ -0,0 +1,63 @@
1
+ ---
2
+ title: "Transfer order ↔ PO link reconcile (both directions, header + line, from NetSuite)"
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-24
10
+ owners: [jcardinal]
11
+ files:
12
+ - library/app/api/toga2.php
13
+ - library/app/api/netsuite/rest.php
14
+ - dbchanges2/_modules/netsuite/2026-09-24a - TransferOrderDownstreamPoBridgeAcl.sql
15
+ - worker/crons/toga2/netsuite/process_netsuite_order.php
16
+ related:
17
+ - ../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
18
+ - ../../worker/features/netsuite-togasupply-per-client-sync.md
19
+ - ../../worker/workflows/reprocessing-one-netsuite-order.md
20
+ - ../../../clients/nychh/features/netsuite-transfer-order-import.md
21
+ - ../../../clients/growrk/features/transfer-order-flow.md
22
+ - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
23
+ ---
24
+
25
+ `App_Api_Toga2::reconcileTransferOrderPurchaseOrderLinks()` makes one TO's PO links (upstream + downstream, header + line) match NetSuite; open before touching any TO↔PO bridge write or when TO PO links look wrong.
26
+
27
+ ## Summary
28
+ - One public method owns every TO↔PO bridge write in the 1.0 NetSuite sync. For one TO it makes both directions match NetSuite: creates missing rows, deletes wrong rows, throws on ACL gaps.
29
+ - Direction rule + table/record ids: [bridge direction](../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md). Short form: upstream (customer PO → TO) = 325/329; downstream (TO → vendor PO) = 326/330.
30
+ - Constant `App_Api_Toga2::TRANSFER_ORDER_PO_LINK_TABLES[<direction>]` holds the table/route names. Use it; never hardcode.
31
+ - Prod-verified 2026-09-24 (all clients with NetSuite TOs: NYCHH, GroWrk, Elite, Adyen).
32
+
33
+ ## How it works
34
+ Signature: `reconcileTransferOrderPurchaseOrderLinks(&$nsOrder, array &$cfg, array &$lookupCustomer = []): void`.
35
+
36
+ Callers:
37
+ - `syncTransferOrderFromNetsuite()` — after `retireStaleSalesOrderForTransferOrder()`, for **every** `TRANSFER_ORDER_DETECTION_MODE`, not only STOCKING_FLAG.
38
+ - `syncPurchaseOrderFromNetsuite()` — when a PO's `createdFrom` is a TO (re-runs the reconcile for that TO).
39
+ - `linkTransferOrderPurchaseOrders()` — kept as a thin public wrapper (old callers).
40
+
41
+ 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
+ - **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
+ 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.
50
+
51
+ Deletes are logged: grep `reconcileTransferOrderPurchaseOrderLinks` in the worker log.
52
+
53
+ `retireStaleSalesOrderForTransferOrder()` moves the old SO's links by direction: upstream SO links → 325/329, downstream (`SalesOrders_PurchaseOrders` / `SalesOrderItems_PurchaseOrderItems`) → 326/330.
54
+
55
+ ## Gotchas
56
+ - **Direction comes from the NetSuite relationship, never the vendor name.** GroWrk raises real vendor POs on a vendor named "Growrk" (NS PO 7522688 `createdfrom` SO 7515828).
57
+ - **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
+ - **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
+ - **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.
60
+ - 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
+
62
+ ## Change history
63
+ - 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,4 +21,5 @@
21
21
  | [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 |
22
22
  | [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 |
23
23
  | [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 |
24
+ | [Re-processing one NetSuite order (process_netsuite_order.php operator CLI)](workflows/reprocessing-one-netsuite-order.md) | Run the real SO/TO sync + its vendor POs for ONE NetSuite order on ONE client, without touching cursors; open to heal or debug a single order. |
24
25
  | [Tracing a 1.0 worker cron run in production (no stdout, silent skips)](workflows/tracing-a-worker-cron-run-in-production.md) | How to answer "did this cron run, and what did it do?" on the 1.0 worker tier (no usable stdout, skipped runs leave no trace); open before concluding a cron "ne |
@@ -820,15 +820,15 @@ order number, line count *and* total quantity.
820
820
 
821
821
  1. move `ItemFulfillmentItems`: `salesOrderItemId` → `transferOrderItemId`,
822
822
  2. move `PurchaseOrderItems_SalesOrderItems` → `PurchaseOrderItems_TransferOrderItems` (upstream),
823
- 3. **(b2)** move `SalesOrderItems_PurchaseOrderItems` → `PurchaseOrderItems_TransferOrderItems`
824
- (downstream — added 2026-09-18),
823
+ 3. **(b2)** move `SalesOrderItems_PurchaseOrderItems` → `TransferOrderItems_PurchaseOrderItems`
824
+ (downstream → downstream 330; before 2026-09-24 it wrongly went to upstream 329),
825
825
  4. `DELETE` the `SalesOrderItem`;
826
826
 
827
827
  then at header level:
828
828
 
829
- 5. **(d2)** collapse **both** `SalesOrders_PurchaseOrders` (downstream) **and**
830
- `PurchaseOrders_SalesOrders` (upstream) onto `PurchaseOrders_TransferOrders`
831
- — upstream added 2026-09-18,
829
+ 5. **(d2)** move `PurchaseOrders_SalesOrders` (upstream) → `PurchaseOrders_TransferOrders` (325) and
830
+ `SalesOrders_PurchaseOrders` (downstream) → `TransferOrders_PurchaseOrders` (326; before 2026-09-24
831
+ it wrongly went to 325),
832
832
  6. **(e)** repoint `ItemFulfillments.salesOrderId` → `transferOrderId` (the fulfillment **header**,
833
833
  added 2026-09-18),
834
834
  7. `DELETE` the `SalesOrder`.
@@ -844,10 +844,11 @@ returns no `totalRecordCount` (see the ACL shape below). Consistent with the 202
844
844
  policy. **A stale transfer-order Invoice no longer freezes the run — it is now DELETED up front
845
845
  (2026-09-21, below);** the old per-line `InvoiceItems` throw is kept only as a safety net.
846
846
 
847
- **Bridge direction is PO-FIRST, confirmed from NYCHH data rather than guessed.** The naming rule is
848
- `<source>_<created from it>`. `Client_Nychh` populates `PurchaseOrderItems_TransferOrderItems` (98
849
- rows) and `PurchaseOrders_TransferOrders` (45); the TO-first siblings are **0 in every client**. Same
850
- rule as the [SO↔PO bridge direction doc](../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md).
847
+ **Both TO↔PO directions are real (corrected 2026-09-24).** Naming rule `<source>_<created from it>`:
848
+ PO-first 325/329 = upstream customer PO; TO-first 326/330 = downstream vendor POs. The old "TO-first
849
+ is dead" reading came from the bug that wrote vendor POs upstream. After retirement,
850
+ `syncTransferOrderFromNetsuite` runs the [TO↔PO link reconcile](../../library/features/transfer-order-purchase-order-link-reconcile.md),
851
+ which fixes both directions. Rule: [bridge direction](../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md).
851
852
 
852
853
  **⚠ It needs the three-layer ACL grant first.** Without `AclFieldPermissions` on records 29/325/329
853
854
  the bridge existence checks return **200 + `WZ-1` with no `meta.totalRecordCount`** and the method
@@ -1087,6 +1088,11 @@ discovered per-client.
1087
1088
 
1088
1089
  ### Manual operator scripts (built for Elite, reusable pattern)
1089
1090
 
1091
+ **One order, any client:** [process_netsuite_order.php](../workflows/reprocessing-one-netsuite-order.md)
1092
+ (`<client> --tranid=|--id=`) — real SO/TO sync + its vendor POs, no cursor writes, refuses while the
1093
+ scheduled wrapper runs. Uses `NETSUITE_SYNC_LOOKUPS_ONLY` (early return after LOOKUPS in
1094
+ `common_sync_togasupply.php`).
1095
+
1090
1096
  Two CLI scripts written for the Elite backfill live under
1091
1097
  `test/@srija/Elite Testing/Service Requests/` (they are operator tools, **not** scheduled crons —
1092
1098
  do not add them to `cron.worker.sync.json`):
@@ -1202,6 +1208,13 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
1202
1208
 
1203
1209
  ## Gotchas / known issues
1204
1210
 
1211
+ - **⚠ Sales-order downstream LINE links were deleted on every PO re-sync (fixed 2026-09-24).** In
1212
+ `syncPurchaseOrderFromNetsuite` `$incomingSalesOrderItemUuid` was initialized null and never
1213
+ assigned, so the existing-line branch deleted every `SalesOrderItems_PurchaseOrderItems` link and
1214
+ never re-created it (Elite prod: 55 of 97 SO-linked vendor POs had no line links). Now assigned; the
1215
+ nested link is stripped from the existing-line PUT; deletes are logged
1216
+ (`syncPurchaseOrderFromNetsuite: DELETE`). Any cursor-reset replay before this fix lost line links.
1217
+
1205
1218
  - **PLATFORM FACT: NO TOGA client fulfills NetSuite item groups (bundles).** Every client fulfills the
1206
1219
  **member items individually**. This is the load-bearing safety assumption behind the IA dedup's
1207
1220
  empty-serials delete branch (see [NYCHH IA→fulfillment link](../../../clients/nychh/features/netsuite-inventory-adjustment-fulfillment-link.md))
@@ -2071,6 +2084,7 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
2071
2084
  [NetSuite Sync Alert Monitor](../../library/features/netsuite-sync-alert-monitor.md).
2072
2085
 
2073
2086
  ## Change history
2087
+ - 2026-09-24 — TO↔PO links: two-direction reconcile (vendor POs no longer upstream; retireStale moves downstream SO links to 326/330); fixed SO downstream line-link deletes on PO re-sync; new `process_netsuite_order.php` + `NETSUITE_SYNC_LOOKUPS_ONLY`. (jcardinal)
2074
2088
  - 2026-09-22 — **The `Logs.Issue` 589 doom-loop's framework root cause is now fully explained and has
2075
2089
  tooling, but this sync is NOT yet patched.** Three independent reasons the 2006 reconnect was dead
2076
2090
  (warning path, `$connections` cache never cleared, `$link` captured above the retry loop); prod
@@ -0,0 +1,52 @@
1
+ ---
2
+ title: "Re-processing one NetSuite order (process_netsuite_order.php operator CLI)"
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-09-24
10
+ owners: [jcardinal]
11
+ files:
12
+ - worker/crons/toga2/netsuite/process_netsuite_order.php
13
+ - worker/crons/toga2/netsuite/common_sync_togasupply.php
14
+ - library/app/api/netsuite/rest.php
15
+ related:
16
+ - ../features/netsuite-togasupply-per-client-sync.md
17
+ - ../../library/features/transfer-order-purchase-order-link-reconcile.md
18
+ - ../../../clients/nychh/features/netsuite-transfer-order-import.md
19
+ ---
20
+
21
+ Run the real SO/TO sync + its vendor POs for ONE NetSuite order on ONE client, without touching cursors; open to heal or debug a single order.
22
+
23
+ ## Summary
24
+ - Manual CLI, **not scheduled** — never add to `cron.worker.*.json`.
25
+ - Writes no cursor, no execution mode. Fail-loud: any error stops the run.
26
+ - Preferred repair model: fix the code so re-processing heals, then re-run (one order here, or a cursor rewind for history — [per-client sync](../features/netsuite-togasupply-per-client-sync.md) "Replaying one order"). No SQL data repair.
27
+ - Supersedes the NYCHH-only `probe_import_single_order_hh.php` / `relink_transfer_order_pos_hh.php` (not in the repo).
28
+
29
+ ## How it works
30
+ From the `worker/` directory:
31
+ ```
32
+ php crons/toga2/netsuite/process_netsuite_order.php <client> --tranid=<NS order #>
33
+ php crons/toga2/netsuite/process_netsuite_order.php <client> --id=<NS internal id>
34
+ ```
35
+ `<client>` = the wrapper suffix of `sync_togasupply_<client>.php` (e.g. `hh`, `growrk`, `elite`). Pass exactly one of `--tranid` / `--id`.
36
+
37
+ Steps the script runs:
38
+ 1. Refuses if `sync_togasupply_<client>.php` is running (`ps -ef`) — two writers can duplicate links.
39
+ 2. Defines `NETSUITE_SYNC_LOOKUPS_ONLY` and requires the wrapper → `common_sync_togasupply.php` builds config + lookups, then returns before any section.
40
+ 3. `--tranid` → `App_Api_Netsuite_Rest::findSalesOrderIdByTranId()` (format-validated).
41
+ 4. Refuses an order whose customer / end-user customer does not map to this client.
42
+ 5. Closed → `deleteClosedOrderFromNetsuite`; TO (detection mode) → `syncTransferOrderFromNetsuite` (includes the [two-direction PO link reconcile](../../library/features/transfer-order-purchase-order-link-reconcile.md)); else → `syncSalesOrderFromNetsuite`.
43
+ 6. `listPurchaseOrderIdsCreatedFromOrder()` → `syncPurchaseOrderFromNetsuite` for every vendor PO created from the order.
44
+
45
+ Verify: grep the PHP error log for `reconcileTransferOrderPurchaseOrderLinks` and `syncPurchaseOrderFromNetsuite: DELETE`.
46
+
47
+ ## Gotchas
48
+ - A TO skipped by the sync (out-of-scope customer) prints `NOT imported` — read the error log, not a script bug.
49
+ - Needs `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` true in the wrapper for TO routing; otherwise the order syncs as a sales order.
50
+
51
+ ## Change history
52
+ - 2026-09-24 — Created; prod-verified on NYCHH TO 11. (jcardinal)
@@ -48,7 +48,7 @@
48
48
  | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | Engine that mirrors a downstream ItemFulfillment up the SO→PO→SO chain onto every customer-facing SO; open when touching IF creation, bundle scaling, or upstrea |
49
49
  | [Sales-order denial reason (`ApprovalDecisions.note`) — capture, the denial-details card, and the read-only workflow mode](features/sales-order-denial-reason.md) | Map of the SO approval denial reason (`ApprovalDecisions.note`) — where it is written, read, and displayed; open when building any denial-reason display, banner |
50
50
  | [Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath](features/sales-order-po-number-sourcing.md) | How the SO modal "PO Number" field is sourced (one surface element → `_purchaseOrders` calc field → tenant SQL); open before touching PO-number display, the ups |
51
- | [SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | The two SO↔PO bridge tables mean opposite things (upstream = customer PO, downstream = vendor PO); open when an SO↔PO query returns an empty array or before cho |
51
+ | [SO↔PO and TO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | The two SO↔PO (and two TO↔PO) bridge tables mean opposite things (upstream = customer PO, downstream = vendor PO); open when an SO/TO↔PO query returns an empty |
52
52
  | [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | Per-client status-filter dropdown on the sales-orders table, driven by a Surface `FILTER_SET`; open when the offered statuses are wrong for a tenant or when onb |
53
53
  | [Shipping Carriers & Carrier Account Numbers (records, matching, duplication, deletion)](features/shipping-carriers-and-accounts.md) | Per-client `ShippingCarriers` / `ShippingCarrierAccountNumbers` model, how the syncs match/create carrier rows, and the duplication + blank-carrier + pagination |
54
54
  | [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` static string utilities: use `encodeToAsciiHtmlEntities` for any dynamic text going into a 2.0 email; open when escaping email text or parsing substri |
@@ -1,12 +1,12 @@
1
1
  ---
2
- title: "SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)"
2
+ title: "SO↔PO and TO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)"
3
3
  framework: "2.0"
4
4
  repo: _underscore
5
5
  project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-18
9
+ updated: 2026-09-24
10
10
  owners: [apeterson, bala, jcardinal, rgirish]
11
11
  files:
12
12
  - _underscore/Model/Client/PurchaseOrders/SalesOrder.php
@@ -15,7 +15,9 @@ files:
15
15
  - _underscore/Model/Client/SalesOrder.php
16
16
  - _underscore/Trait/Netsuite/SalesOrder.php
17
17
  - library/app/api/toga2.php
18
+ - _underscore/Model/Client/TransferOrder.php
18
19
  related:
20
+ - ../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md
19
21
  - ./sales-order-po-number-sourcing.md
20
22
  - ./recursive-item-fulfillments.md
21
23
  - ./fulfillable-item-propagation.md
@@ -24,7 +26,7 @@ related:
24
26
  - ../../../clients/nychh/features/po-number-upstream-direction.md
25
27
  ---
26
28
 
27
- The two SO↔PO bridge tables mean opposite things (upstream = customer PO, downstream = vendor PO); open when an SO↔PO query returns an empty array or before choosing a bridge/child key.
29
+ The two SO↔PO (and two TO↔PO) bridge tables mean opposite things (upstream = customer PO, downstream = vendor PO); open when an SO/TO↔PO query returns an empty array or before choosing a bridge/child key.
28
30
 
29
31
  ## Summary
30
32
 
@@ -44,6 +46,17 @@ Read the name as **"`<source>`\_`<thing created from it>`"**. The *first* table
44
46
  | `PurchaseOrderItems_SalesOrderItems` | **UPSTREAM** | an SO line created **FROM** a customer PO line | `purchase-order-item-sales-order-items` |
45
47
  | `SalesOrderItems_PurchaseOrderItems` | **DOWNSTREAM** | a vendor PO line created **FROM** an SO line | `sales-order-item-purchase-order-items` |
46
48
 
49
+ **Transfer orders follow the same rule (live in both directions since 2026-09-24):**
50
+
51
+ | Header table | Line table | Direction | Meaning | `Core.Records` |
52
+ |---|---|---|---|---|
53
+ | `PurchaseOrders_TransferOrders` | `PurchaseOrderItems_TransferOrderItems` | **UPSTREAM** | TO created FROM the customer PO (NetSuite `otherRefNum`) | 325 / 329 |
54
+ | `TransferOrders_PurchaseOrders` | `TransferOrderItems_PurchaseOrderItems` | **DOWNSTREAM** | vendor PO created FROM the TO (NetSuite PO line `createdfrom`) | 326 / 330 |
55
+
56
+ The only writer is the 1.0 [TO↔PO link reconcile](../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md). Reading side: `_Model_Client_TransferOrder._purchaseOrderNumbers` (FIELD_SQL, `GROUP_CONCAT` of upstream PO numbers, Core RecordField 2633) — the `TransferOrders.purchaseOrderId` column (RecordField 2143) was dropped platform-wide 2026-09-24. A TO can have several POs, so read the calc field; a bridge join multiplies list rows.
57
+
58
+ **⚠ Pick the direction from the NetSuite relationship, never from the vendor name.** GroWrk raises real vendor POs on a vendor named "Growrk" (NS PO 7522688 `createdfrom` SO 7515828). Customer PO = `otherRefNum`; vendor PO = a PO whose line `createdfrom` is the order.
59
+
47
60
  The 1.0 Compass NetSuite sync deletes these **item-level** link rows in **both** directions when it prunes a NetSuite-removed SO line — see [NetSuite → TOGa Supply per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md) (2026-09-15).
48
61
 
49
62
  ## How it works
@@ -83,7 +96,13 @@ All **35** prod client schemas contain all four tables (`SalesOrders`, `Purchase
83
96
  - **⚠ Do NOT paper over the direction with an `ojoin` on a shared single-record fetch.** Tempting because it works in one place: the TOGa Supply NYCHH orders **list** joins the upstream bridge and is correct — but only because NYCHH's upstream bridge happens to be **1:1** (823 rows / 823 distinct sales orders, max 1 PO per SO). The same join in the shared `fetchOrdersDetails` (`toga2-supply/src/pages/Orders/api/OrdersApi.ts:397`) breaks three tenants: **Compass** has up to **6,440** downstream POs on one sales order (row multiplication on a single-record fetch), **Compass Canada's upstream bridge is empty** (735 of 788 would drop to **zero**), and **Prudential** has only **1,868 of 26,843** orders upstream (92% populated would become 7%). A scalar `GROUP_CONCAT` calculated field is the right mechanism precisely because it collapses many POs without multiplying rows.
84
97
  - **⚠ When you DELETE a sales order, you must move BOTH directions of BOTH bridges — fixing one and missing its sibling is the default failure (2026-09-18).** Elite's stale-SalesOrder retirement shipped handling only `SalesOrders_PurchaseOrders` (downstream header) and `PurchaseOrderItems_SalesOrderItems` (upstream line), and failed on prod against the two siblings it skipped — `PurchaseOrders_SalesOrders` (upstream header) and `SalesOrderItems_PurchaseOrderItems` (downstream line). **Neither direction is "the main one":** on the same 218 orders, upstream dominated at header level (**218 vs 123**) while downstream was the rare one at line level (**7 vs 669**), so you cannot skip a direction because it looked empty on another tenant. The bridge FKs are `RESTRICT`, so a missed sibling is a hard FK error rather than silent — but you only find it one blocker at a time. **Enumerate instead:** query `information_schema.KEY_COLUMN_USAGE` for every FK referencing the parent, then count rows on the affected records, before writing any delete code. Details on the [per-client sync engine doc](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
85
98
 
99
+ - **⚠ Before 2026-09-24 the 1.0 sync wrote vendor POs into the UPSTREAM TO tables** (325/329) — from `syncPurchaseOrderFromNetsuite` (PO `createdFrom` a TO) and from `retireStaleSalesOrderForTransferOrder`. Same class of bug as a transposed child key. Fixed by the reconcile, which deletes the wrong rows on re-process.
100
+
101
+ ## Change history
102
+ - 2026-09-24 — Added TO↔PO bridges (325/329 upstream, 326/330 downstream now live), the name-vs-relationship rule, `_purchaseOrderNumbers`, and the vendor-PO-upstream bug. (jcardinal)
103
+
86
104
  ## Related
105
+ - [TO↔PO link reconcile (1.0)](../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md)
87
106
  - [Sales-order PO Number sourcing](./sales-order-po-number-sourcing.md)
88
107
  - [Recursive Item Fulfillments](./recursive-item-fulfillments.md)
89
108
  - [isFulfillable propagation](./fulfillable-item-propagation.md)
@@ -55,6 +55,7 @@ files:
55
55
  - toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
56
56
  - toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx
57
57
  - _underscore/Model/Client/TransferOrderItem.php
58
+ - toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/helpers/resolveDetailSlot.ts
58
59
  related:
59
60
  - ./surface-frontend.md
60
61
  - ./meta-driven-table-data.md
@@ -227,7 +228,7 @@ Three params, all addressing Inventory grouping `posByItems` level 0 (`fetchSlug
227
228
  - **The filter chip is set through `URLSearchParams.set`, with the value `"includes:" + encodeURIComponent(number)`** — mirroring the serializer in `hooks/useServerTableUrlState.ts` exactly. Hand-writing the query string double-encodes, or breaks a value containing `:` or `,`. The key's middle segment is the **column id from the table meta** (`customerPurchaseOrder`), not the record field name — renaming that `TableViewFields` column breaks the link with nothing to warn you.
228
229
  - **`purchase-orders=<uuid>` is the active-row param**, named **exactly** the table slug (`activeRowUuid = searchParams.get(slug)`). That level's `rowInteraction` is the default `"expand"`, so setting it **opens the child sub-table inline on page load** — the same thing a row click does. Omitted when the uuid is unknown; the filter alone still lands on the row, collapsed.
229
230
 
230
- The uuid is read off the record (`transferOrder.purchaseOrder.uuid`, already in the record fetch — no API change), **not** off the surface: the surface says *which number to show*; the route it navigates to is FE wiring (the config/logic boundary, [frontend standard](../../../standards/frontend.md) §20).
231
+ Since 2026-09-24 the number is `_purchaseOrderNumbers` (calc field, upstream bridge, may be several joined) and the uuid is `purchaseOrdersTransferOrders.purchaseOrder.{uuid,number}` (`hooks/useTransferOrderRecord.ts`, `helpers/resolveDetailSlot.ts`). **api2 returns a lone child as an object, not an array** — normalize both shapes. The link renders **only when exactly 1 PO**. The uuid is read off the record, **not** off the surface: the surface says *which number to show*; the route it navigates to is FE wiring (the config/logic boundary, [frontend standard](../../../standards/frontend.md) §20).
231
232
 
232
233
  > ⚠ **Never `onClose()` before `navigate()`.** See the gotcha below — it races two navigations and silently strips the params the link is made of.
233
234
 
@@ -335,8 +336,8 @@ This closes both **required POST changes** recorded on 2026-09-02: `number` is n
335
336
  - header: `purchaseOrdersTransferOrders: [{ purchaseOrder: { uuid } }]` → bridge `PurchaseOrders_TransferOrders` (record **325**; inherent child of 312 via row 151).
336
337
  - each `transferOrderItems[]` line: `purchaseOrderItemsTransferOrderItems: [{ purchaseOrderItem: { uuid } }]` → bridge `PurchaseOrderItems_TransferOrderItems` (record **329**; inherent child of 313 via `dbchanges2/Core/2026-09-23d`). Source: `TransferOrderDraftLine.purchaseOrderItemUuid` ← `PurchaseOrderItemRow.purchaseOrderItemUuid`.
337
338
  - V2 auto-sets each bridge's parent-side FK; payload supplies only the other side. Engine: [nested-relationship-writes](../../api2/features/nested-relationship-writes.md).
338
- - Direct `purchaseOrder: { uuid }` (`TransferOrders.purchaseOrderId`) is **kept** — the record modal's PO deep link reads `transferOrder.purchaseOrder.uuid`.
339
- - Use 325/329, never the reverse-named twins 326 `TransferOrders_PurchaseOrders` / 330 `TransferOrderItems_PurchaseOrderItems` (0 rows in prod; dead).
339
+ - No direct `purchaseOrder` in the POST — `TransferOrders.purchaseOrderId` (RecordField 2143) was **dropped platform-wide 2026-09-24**.
340
+ - Write 325/329 (upstream: TO created FROM the customer PO). 326/330 are the **downstream** twins (vendor POs created FROM the TO), written only by the 1.0 NetSuite sync — [bridge direction](../../_underscore/features/sales-order-purchase-order-bridge-direction.md).
340
341
 
341
342
  **FKs go out as nested `{ uuid }` objects under the field name WITHOUT the `Id` suffix** — `contact: { uuid }`, `createdByUser: { uuid }`. Each is omitted entirely when absent rather than sent as `null`: a location can have no contact, and an SSO session can lack a user uuid.
342
343
 
@@ -523,7 +524,7 @@ Money (Unit Value / Total / Total Value) and Target Contact are **dropped for v1
523
524
 
524
525
  ~~⚠ `TransferOrders` carries **no contact FK at all** today, so the Target Contact element's `valueKey` is a **placeholder**~~ — **closed 2026-09-01.** `TransferOrders.contactId` (FK `Contacts`) exists and RecordField **2561** is granted to roles 1 and 3, so the element has a real binding and enabling it is now genuinely a one-row client override.
525
526
 
526
- The items table's **PO column is order-level**: `TransferOrderItems` has no line-level PO column (line PO link lives in bridge 329 since 2026-09-23, not bound to the column), so the column binds the parent order's `purchaseOrder.number` and repeats on every row. For NYCHH it resolves NULL because their PO links are **upstream** — see [NYCHH PO direction](../../../../clients/nychh/features/po-number-upstream-direction.md). It is left visible in Core (neutral default; other tenants populate `TransferOrders.purchaseOrderId` normally). **The header's PO value is now a link into Inventory** (above); for NYCHH it is the same null, so nothing renders there for this tenant yet.
527
+ The items table's **PO column is order-level**: `TransferOrderItems` has no line-level PO column (line PO link lives in bridge 329, not bound to the column), so the column (`columnKey po`) binds the parent's `_purchaseOrderNumbers` and repeats on every row. Core `SurfaceElements` valueKeys (transfer-order-details role `purchaseOrder`, transfer-order-items `po`) → `_purchaseOrderNumbers` since 2026-09-24. NYCHH transfer-orders TableView column repointed to RecordField 2633 (join 185 on 2143 deleted). **The header's PO value links into Inventory** (above) when there is exactly one PO.
527
528
 
528
529
  ### Entry point #2 — the Inventory page's "Create Transfer Order" button (2026-09-03)
529
530
 
@@ -623,3 +624,6 @@ Library-side detail: [blox API client](../../toga-blox/features/api-client.md).
623
624
  - **`_Model_Client_Item` has no `name` field** — the item title/description live in `partNumber`, `title`, `description`, `longDescription` (`Item.php:39-45`). Binding `name` yields nothing.
624
625
  - **`Client_Nychh` `AclFieldPermissions` on the Location address fields (Core RecordFields 70–74) exist for roles 1 (Base) and 2 (Developer) but NOT role 3 (API).** Irrelevant to the UI, relevant to any integration reading those fields.
625
626
  - **Core `RecordFields` 2499/2503 (record 351) and 2504 (record 312) have `name` NULL** — added by an earlier migration without names, so a name-based lookup on them finds nothing.
627
+
628
+ ## Change history
629
+ - 2026-09-24 — `TransferOrders.purchaseOrderId` dropped; PO read from `_purchaseOrderNumbers` + upstream bridge; Create POST no longer sends `purchaseOrder`; deep link only for exactly 1 PO. (jcardinal)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
9
+ updated: 2026-09-25
10
10
  owners: ["bala", "jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Netsuite/SalesOrder.php
@@ -79,12 +79,13 @@ Nothing beyond this list. A transfer order has no shipping cost, ship method, te
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
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. |
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. |
82
83
  | `externalId` | `toga-to-<transfer order uuid>` |
83
84
  | line `item` / `quantity` | each transfer-order item |
84
85
  | line `rate` | **0** — the zero-value part |
85
86
  | line `location` | the **origin** warehouse. **`location` is NOT a ship-to** — NetSuite never uses a line's location for delivery. |
86
87
 
87
- **Missing data ABORTS the order — never sends a partial one.** Each throws with a named message: **no origin location, no destination location, no lines, no PO number**. Mirrors the sales-order push's missing-mapping rule. **PO # is mandatory on the NetSuite order form**, which is why a missing PO number is fatal rather than omitted. `resolvePurchaseOrderNumber()` reads the PO number from the **`purchaseOrdersTransferOrders` header bridge first** (written by the Create Transfer Order modal since 2026-09-23), falling back to the direct `purchaseOrderId` column. See [SO↔PO bridge direction](../../_underscore/features/sales-order-purchase-order-bridge-direction.md).
88
+ **Missing data ABORTS the order — never sends a partial one.** Each throws with a named message: **no origin location, no destination location, no lines, no PO number**. Mirrors the sales-order push's missing-mapping rule. **PO # is mandatory on the NetSuite order form**, which is why a missing PO number is fatal rather than omitted. `resolvePurchaseOrderNumber()` reads the PO number **only** from the upstream `purchaseOrdersTransferOrders` header bridge (325; the `purchaseOrderId` column was dropped 2026-09-24) and joins several POs with `' + '` — the same separator the inbound importer splits `otherRefNum` on. See [SO↔PO bridge direction](../../_underscore/features/sales-order-purchase-order-bridge-direction.md).
88
89
 
89
90
  **`description` → `memo` cannot leak into a plain sales order.** `memo` is sent only from the transfer-order shape. There is **no `memo` RecordField and no `memo` column anywhere** in the sales-order path.
90
91
 
@@ -193,12 +194,14 @@ Stub detail: `_Model_True` is stubbed with **only** the `FIELD_*` / `FIELDOPT_*`
193
194
  - **The Create Transfer Order modal must (a) drop `"number"` from the POST and (b) send `transferOrderItems` NESTED in the same POST.** If it posts lines in separate calls afterwards, `postPost` reads **zero lines** and the push aborts — the trigger would then have to move. See [Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
194
195
  - **dev-sandbox metadata is incomplete** (record 325 + its RecordFields + both inherent-child rows).
195
196
  - **The -8 tax-group list is a snapshot (2026-09-14).** Re-run the check before onboarding any new client; **34283 Yale New Haven Health** and **2661 Endeavor Health** will fail on their first order.
196
- - **Undecided: should the delivery contact go into the NetSuite `attention` field?** NetSuite ship-to addresses carry a person on the first line (order **289193** shows *Frederick Roberts*, the Bellevue delivery contact). `TransferOrders.contactId` exists as an FK on `_Model_Client_TransferOrder` but **nothing in worker2 or toga2-supply reads it**.
197
+ - **Delivery contact → `attention`: shipped 2026-09-25** (row above). In `Core.WorkerJobs.parameters` the FK arrives expanded as `transferOrder.contact` {firstName, lastName} — **no `contactId` key, `contact._name` never present**; no ACL/depth change was needed. toga25-supply's Create Transfer Order modal sets `TransferOrders.contactId` on every order it places. No length cap on attention (NetSuite ~99 chars) — same as other fields.
197
198
  - **Undecided: disable the customer name-search fallback for transfer orders?** `resolveNetSuiteCustomerId()` falls back to a SuiteQL search by company **name** when the destination has no customer id, and NYCHH's `Health & Hospitals` vs `NYC Health + Hospitals` are close enough to attach an order to the wrong NetSuite account.
198
199
  - **Decided and shipped 2026-09-08:** `description` goes out as `memo`, renders as "ORDER DESCRIPTION" on the NYCHH form. Still undecided: the TA number / `customerIdentificationNumber` into custom fields. **`contact` and `createdByUser` cannot be sent** — they need a NetSuite employee/contact id, which NYCHH `Contacts` do not carry.
199
200
 
200
201
  ## Gotchas
201
202
 
203
+ - **Stale test (2026-09-24):** `test/@Bala/tests/netsuite_transferorder_payload_tests.php` "PO number from the column" case now fails — the column is gone; rewrite it against the bridge.
204
+
202
205
  ### ⚠ The customer's NetSuite tax item must be AVATAX, or every create 400s
203
206
 
204
207
  **Symptom.** Every transfer order create fails with NetSuite HTTP **400**:
@@ -256,6 +259,10 @@ When the payload omits the ship-to address the create **succeeds**, so it looks
256
259
  - **A NetSuite HTTP 400 naming a tax code is a CUSTOMER data problem, not a payload problem.** Check the customer's `taxitem` first; do not pin the tax fields in the payload (ignored) and do not drop the address to make the error go away (silently ships to the wrong site).
257
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).
258
261
 
262
+ ## Change history
263
+ - 2026-09-24 — `resolvePurchaseOrderNumber()` bridge-only (column 2143 dropped); multiple POs joined with `' + '`. (jcardinal)
264
+ - 2026-09-25 — delivery contact name sent as `shippingAddress.attention`. (jcardinal)
265
+
259
266
  ## Related
260
267
 
261
268
  - [Toga → NetSuite Sales-Order Push](./netsuite-salesorder-outbound-push.md) — the shared outbound worker this reuses; read first.
@@ -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)_ — 27 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 38 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 28 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
+ - **worker** (Worker) — 39 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) — 13 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-18
9
+ updated: 2026-09-24
10
10
  owners: ["snaredla", "jcardinal", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/sync_togasupply_elite.php
@@ -339,6 +339,11 @@ ships to **all 22 clients** carrying `netsuite` in `_modules.txt` and is written
339
339
  [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md). **Run so far
340
340
  on `Client_Elite` only.**
341
341
 
342
+ > **2026-09-24:** `linkPurchaseOrderToTransferOrder` is gone — replaced by the two-direction
343
+ > [TO↔PO link reconcile](../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md).
344
+ > Some of the 325/329 rows counted above were **vendor** POs (wrong direction); re-processing moves
345
+ > them to downstream 326/330 (ACL: `_modules/netsuite/2026-09-24a`).
346
+
342
347
  ### Elite's stale SalesOrder backfill — RUNNING, 218 → 187 (2026-09-18)
343
348
 
344
349
  Enabling transfer-order detection left **218** Elite `SalesOrders` that duplicate a `TransferOrder`
@@ -445,6 +450,7 @@ Elite's PO backfill just did. **NOT FIXED** (shared file, 22 clients). Full root
445
450
  rewinding `NETSUITE_LAST_SYNC_DATETIME_SALES_ORDERS`.
446
451
 
447
452
  ## Change history
453
+ - 2026-09-24 — TO↔PO links now via the two-direction reconcile; some upstream rows were vendor POs; SO downstream line links were being deleted on every PO re-sync (55 of 97 SO-linked vendor POs had none) — fixed. (jcardinal)
448
454
  - 2026-09-18 — **Stale-SalesOrder backfill is live: 218 → 187.** The retirement method needed
449
455
  four more FK dependencies before it would run (fulfillment **header** `salesOrderId`, the
450
456
  upstream `PurchaseOrders_SalesOrders`, the downstream `SalesOrderItems_PurchaseOrderItems`, and
@@ -17,8 +17,8 @@ project: Worker
17
17
  client: elite
18
18
  type: profile
19
19
  status: active
20
- updated: 2026-09-21
21
- owners: [snaredla, apeterson, tcox, bala, rgirish]
20
+ updated: 2026-09-24
21
+ owners: [snaredla, apeterson, tcox, bala, rgirish, jcardinal]
22
22
  files:
23
23
  - worker2/Worker/Elite.php
24
24
  - worker2/Worker/Sync/ServiceRequest.php
@@ -74,9 +74,9 @@ Elite's app scope. Elite-specific config, the still-owed `Countries` cleanup, an
74
74
  `Apis` rows hazard are in [netsuite-togasupply-sync](features/netsuite-togasupply-sync.md).
75
75
  **As of 2026-09-08 Elite carries all 22 `_Trait_Netsuite_*` trait models** (`_Model_Elite_*`), the
76
76
  last three being Contact, TransferOrder and TransferOrderStage — code-only, no migration needed,
77
- because the `_modules/netsuite` opt-in had already seeded the columns. **Transfer orders remain off**
78
- (`IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` false, 0 rows in prod), and the shared sync method carries
79
- GroWrk-specific rules that must be verified before Elite enables it.
77
+ because the `_modules/netsuite` opt-in had already seeded the columns. **Transfer orders are on in the 1.0 sync** — prod `Client_Elite` has **218 TransferOrders**
78
+ (verified 2026-09-24; the earlier "0 rows" note was stale). The 2.0 `_Trait_Netsuite_TransferOrder`
79
+ method still carries GroWrk-specific rules. TO↔PO links: [TO↔PO link reconcile](../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md).
80
80
 
81
81
  **⚠ 2026-09-08: Elite's sales-order sync was blocked for 4 days** by a transfer-order 400 `EV-12` —
82
82
  `Client_Elite.CustomRecordFields` id 72 had `isIdentifier = 0` on
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: growrk
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: ["rgirish"]
9
+ updated: 2026-09-24
10
+ owners: [rgirish, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/TransferOrder.php
13
13
  - _underscore/Model/Client/TransferOrderItem.php
@@ -142,8 +142,13 @@ Full plan saved at `/Applications/AMPPS/www/transfer-order-plan.md`.
142
142
  - **NS identification: both conditions required.** `holdInvoice=true` alone is not a safe signal — must also confirm `total === 0`.
143
143
  - **⚠ An ABSENT NetSuite `total` reads as `0.0` and can misclassify a real order (open, unfixed).** The 1.0 detection branch does `$total = isset($nsOrder->total) ? (float) $nsOrder->total : 0.0;`, so a NetSuite REST response with a missing/blank total — a transient API failure, not a genuine $0 order — collapses to `0.0`; combined with the hold flag set, a **non-zero order is classified as a transfer order**. Pre-existing, carried unchanged through the 2026-09-03 mode refactor, and raised with the developer. If you see an unexpected TransferOrder for GroWrk, check this first.
144
144
  - **The mode is now mandatory in the launcher.** Removing/forgetting `TRANSFER_ORDER_DETECTION_MODE` in `sync_togasupply_growrk.php` makes `isTransferOrder()` **throw** — GroWrk's sync will fail loud, by design, rather than silently reclassify.
145
+ - **⚠ Bridge direction (2026-09-24).** `PurchaseOrders_TransferOrders` / `PurchaseOrderItems_TransferOrderItems` = UPSTREAM (TO created FROM the customer PO); `TransferOrders_PurchaseOrders` / `TransferOrderItems_PurchaseOrderItems` = DOWNSTREAM (vendor PO created FROM the TO). Written by the 1.0 [TO↔PO link reconcile](../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md). **Never infer direction from the vendor name** — GroWrk raises real vendor POs on a vendor named "Growrk" (NS PO 7522688 `createdfrom` SO 7515828). Prod `Client_Growrk` had 701 TransferOrders and a few vendor POs wrongly upstream before the fix.
146
+
147
+ ## Change history
148
+ - 2026-09-24 — Added bridge-direction gotcha + the "Growrk" vendor trap. (jcardinal)
145
149
 
146
150
  ## Related
151
+ - [TO↔PO bridge direction](../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md)
147
152
 
148
153
  - [Recursive item fulfillments](../../../2.0/apps/_underscore/features/recursive-item-fulfillments.md)
149
154
  - [worker2 architecture](../../../2.0/apps/worker2/architecture.md)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-24
9
+ updated: 2026-09-25
10
10
  owners: [bala, jcardinal]
11
11
  files:
12
12
  - dbchanges2/Client_Nychh/2026-09-09c - UpdateLocationShippingAddresses.sql
@@ -58,7 +58,7 @@ The Create Transfer Order screen picks a **Target Contact** from `Contacts` thro
58
58
  ## Gotchas
59
59
 
60
60
  - **🚩 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.
61
- - **🚩 OPEN — nothing reads the chosen contact.** `TransferOrders.contactId` is a real FK on `_Model_Client_TransferOrder`, but **no code in worker2 or toga25-supply reads it**. NetSuite ship-to addresses do carry a person on the first line (order **289193** shows *Frederick Roberts*, the Bellevue delivery contact), so wiring the contact into the NetSuite `attention` field is the obvious next step — **undecided, not implemented**. See [transfer-order → NetSuite push](../../../2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md).
61
+ - **Chosen contact → NetSuite `attention` (2026-09-25).** The Create Transfer Order modal sets `TransferOrders.contactId`; the worker2 push sends the contact's name as ship-to `attention` (as order **289193** shows *Frederick Roberts*). This is a *person* on the ship-to, not the AP bill-to tell below. See [transfer-order → NetSuite push](../../../2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md).
62
62
  - **Location 2 "NYC Health + Hospitals" = client-level catch-all; address now 55 Water Street, 10041** (was Jacobi's 1400 Pelham). Its 1,832-of-1,939 TO count is largely an import bug (overwrote user destinations) + NetSuite-entered orders under 28908; fix + ship-to matching in [the NetSuite transfer-order import](./netsuite-transfer-order-import.md).
63
63
  - **⚠ Two locations carry NetSuite customer ids that no longer exist.** **Bellevue Hospital Center → 29275** and **Jacobi Medical Center → 31917**; neither id is in the NetSuite `customer` table, so their addresses came from other sources rather than a `defaultshipping` row. The ids need correcting; until then any NetSuite lookup keyed on them returns nothing.
64
64
  - **⚠ A location's address surfaces ONLY on transfer orders for this client.** Sales orders carry their own `shipToAddressId`, so "the address is fine on the order screens" says nothing about the location record.
@@ -16,10 +16,13 @@ files:
16
16
  - _underscore/Model/Nychh/TransferOrderItem.php
17
17
  - worker/crons/toga2/netsuite/sync_togasupply_hh.php
18
18
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
19
- - worker/crons/toga2/netsuite/relink_transfer_order_pos_hh.php
19
+ - worker/crons/toga2/netsuite/process_netsuite_order.php
20
20
  - dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql
21
21
  - dbchanges2/Client_Nychh/2026-09-16a - TransferOrderBridgeAcl.sql
22
+ - dbchanges2/_modules/netsuite/2026-09-24a - TransferOrderDownstreamPoBridgeAcl.sql
22
23
  related:
24
+ - ../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md
25
+ - ../../../1.0/apps/worker/workflows/reprocessing-one-netsuite-order.md
23
26
  - ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
24
27
  - ../../../1.0/apps/library/features/toga2-api-client-and-bridge.md
25
28
  - ../../../1.0/apps/library/features/netsuite-suiteql-rest-shim.md
@@ -47,7 +50,7 @@ Route by the custom NetSuite checkbox **`custbody_stocking_order`** (internal id
47
50
  - **CHECKED** = a *stocking order* (stock comes **IN**) → `Client_Nychh.SalesOrders`.
48
51
  - **UNCHECKED** = a *transfer order* (stock goes **OUT**) → `Client_Nychh.TransferOrders`.
49
52
 
50
- `App_Api_Toga2::isTransferOrder()` was rewritten to read this flag through a new private `isStockingOrder()` that normalizes NetSuite's `'T'`/`'F'` / bool / int representations. A new private `linkPurchaseOrderToTransferOrder()` mirrors the existing sales-order PO linker (below).
53
+ `App_Api_Toga2::isTransferOrder()` was rewritten to read this flag through a new private `isStockingOrder()` that normalizes NetSuite's `'T'`/`'F'` / bool / int representations.
51
54
 
52
55
  **Routing mode is declared per client — there is NO default (2026-09-03).** The old boolean `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING` is **gone**. NYCHH's wrapper now sets
53
56
 
@@ -58,33 +61,21 @@ const TRANSFER_ORDER_DETECTION_MODE = App_Api_Toga2::TRANSFER_ORDER_DETECTION_MO
58
61
  in `worker/crons/toga2/netsuite/sync_togasupply_hh.php`. `isTransferOrder()` **throws** if the mode is undefined or not in the `TRANSFER_ORDER_DETECTION_MODES` allowlist — see the shared [per-client sync — transfer-order detection mode](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
59
62
 
60
63
  ### PO ↔ order bridge links (header + line)
61
- A single NetSuite PO is bridged to whichever order table the order landed in, at both header and line level; links are idempotent (line matching is by shared item `uuid`):
64
+ Owned by the shared [TO↔PO link reconcile](../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md) (2026-09-24; replaced `linkPurchaseOrderToTransferOrder`). Direction rule: [bridge direction](../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md).
62
65
 
63
- | Order routed to | Header bridge | Line bridge |
66
+ | Order routed to | Upstream (customer PO) | Downstream (vendor PO) |
64
67
  |---|---|---|
65
- | stocking → SalesOrders | `PurchaseOrders_SalesOrders` | `PurchaseOrderItems_SalesOrderItems` |
66
- | transfer → TransferOrders | `PurchaseOrders_TransferOrders` | `PurchaseOrderItems_TransferOrderItems` |
67
-
68
- `linkPurchaseOrderToTransferOrder()` is the new transfer-order counterpart to the existing sales-order linker. See [SO↔PO bridge direction](../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md) for the bridge naming convention.
69
-
70
- #### The bridge links were never being created — three stacked causes, fixed + prod-verified 2026-09-16
71
- The PO↔TO bridge links (header **and** line) were silently never forming for NYCHH. Three independent faults, peeled in order; the last was the real blocker:
72
- 1. **Linking only ever ran from the `PURCHASE_ORDERS` section, which never runs for NYCHH.** The links depended on the PO section, so a transfer-order import created none. **Fix:** a new **public** `App_Api_Toga2::linkTransferOrderPurchaseOrders()` now runs on **every** transfer-order import/retry from inside `syncTransferOrderFromNetsuite()`. It is **idempotent** — re-running an import repairs any missing link. In `TRANSFER_ORDER_DETECTION_MODE__STOCKING_FLAG` mode it finds the customer PO by the NetSuite **PO #** (`otherRefNum`, split on `' + '`) under the `clientConfiguration['importCustomerPurchaseOrdersOnVendor']` vendor (the synthetic **Agilant** vendor), then calls `linkPurchaseOrderToTransferOrder` for the header (`PurchaseOrders_TransferOrders`) and line (`PurchaseOrderItems_TransferOrderItems`) links.
73
- 2. **`linkPurchaseOrderToTransferOrder` read the TO line items from the wrong place, so line links never formed.** It read `$transferOrder->transferOrderItems`, but **api2 NEVER inlines that collection at any depth** (`transfer-orders` has no `InherentRecordChildren` row — same root cause as the idempotency bug), so it was always empty. **Fix:** read TO items via `getTransferOrderItemsByUuid` (the reliable direct `GET /transfer-order-items` query). Also lowered the TO existence GET to `depth=1` and added `recordsPerPage=1000` to the PO-items GET.
74
- 3. **ROOT CAUSE — an ACL gap, not code.** The NYCHH **API** role had **no write ACL** on the two bridge records **325** (`PurchaseOrders_TransferOrders`) and **329** (`PurchaseOrderItems_TransferOrderItems`), so the link POST failed api2 **EZ-3 / WZ-2** ("no fields in which you are authorized to write"). The working **sales-order** bridge (record **276**, `PurchaseOrders_SalesOrders`) already had the grant, which is why SO links worked and TO links did not. **Also key: on CREATE, api2 `isPermitted()` DENIES a record permission that has no `AclLogicGroups` chain — a bare `AclRecordPermissions` row is inert** (the standard four-table chain rule — see [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md)).
75
-
76
- **Fix migration:** `dbchanges2/Client_Nychh/2026-09-16a - TransferOrderBridgeAcl.sql` seeds the full four-table chain (`AclRecordExpressions` `'all'` → `AclRecordPermissions` full CRUD → `AclLogicGroups` AND → `AclLogicGroupExpressions`) **plus** `AclFieldPermissions isWritable = 1`, for roles **Base** and **API**, on records **325** and **329**, mirroring the working record 276. Guarded/re-runnable, roles resolved by name, fresh random v4 uuids, `Core` ids hardcoded (no `Client_*`→`Core` JOIN — the cross-cluster rule). The only writable fields a bridge POST touches are the row `uuid` + the two FK columns. **Data facts:** Core bridge record ids **276**=`PurchaseOrders_SalesOrders`, **325**=`PurchaseOrders_TransferOrders`, **329**=`PurchaseOrderItems_TransferOrderItems`; Core `RecordFields` **2210**=uuid/**2211**=purchaseOrderId/**2212**=transferOrderId (rec 325), **2226**=uuid/**2227**=purchaseOrderItemId/**2228**=transferOrderItemId (rec 329); `Client_Nychh` roles **1**=Base, **2**=Developer, **3**=API, **4**=Public. Reviews: `cto` AGREE (four-table chain), `sql-reviewer` SAFE TO MERGE.
77
-
78
- **ACL failures are now LOUD in the linker.** Both bridge existence checks **THROW** when their GET returns no `meta->totalRecordCount` (an ACL **read** gap) instead of silently skipping the create; the create POST already throws on a write-fail (EZ-3/WZ-2). So any future ACL gap stops the run rather than quietly producing zero links.
79
-
80
- **Verified in prod:** link `PurchaseOrderItems.id = 589` ↔ `TransferOrderItems.id = 7155` created for PO **EIT0012548** ↔ TO **284624** (NetSuite SO **7326440**).
81
-
82
- #### PO discovery for a NYCHH transfer order — `otherRefNum` + Agilant vendor, NEVER `createdPo`
83
- For NYCHH the SO **line-level `createdPo` is NULL**; the PurchaseOrder is the **customer PO** (`otherRefNum`, the NetSuite "PO #" field) imported against the synthetic **Agilant** vendor. So PO discovery from a transfer order must use `otherRefNum` + the Agilant vendor — never `createdPo`. NetSuite proof: SO **7326440** `tranid` 284624, `otherrefnum` EIT0012548; its lines' `createdpo` are all null; in NetSuite it links only to an invoice + item fulfillment (there is **no** PurchaseOrder record on the SO itself).
84
-
85
- #### Operator tools for this path (manual CLI, NOT scheduled)
86
- - **`worker/crons/toga2/netsuite/probe_import_single_order_hh.php`** — imports/relinks **one** NetSuite order by internal id for NYCHH. Builds lookups by requiring `common_sync_togasupply.php` with all `IS_ENABLED_INTEGRATION_*` false, fetches via `fetchSalesOrderByIdFull`, routes via `isTransferOrder`, runs `syncTransferOrderFromNetsuite`, then prints diagnostics (`[diag]` PO lookup, `[trace]` header-link POST response) and verifies header + line links. No cursor writes / no process lock.
87
- - **`worker/crons/toga2/netsuite/relink_transfer_order_pos_hh.php`** — backfills links for **all** existing `Client_Nychh` transfer orders. Pages every `TransferOrders` row, fetches each NetSuite order for its PO #, runs only the link step (`App_Api_Toga2::linkTransferOrderPurchaseOrders` — no re-import), idempotent, per-order `try/catch` that logs + collects failures (a sanctioned operator-tool exception to fail-loud), with a live progress bar + counts + end summary. Optional first arg = limit.
68
+ | stocking → SalesOrders | `PurchaseOrders_SalesOrders` / `PurchaseOrderItems_SalesOrderItems` | `SalesOrders_PurchaseOrders` / `SalesOrderItems_PurchaseOrderItems` |
69
+ | transfer → TransferOrders | 325 `PurchaseOrders_TransferOrders` / 329 `PurchaseOrderItems_TransferOrderItems` | 326 `TransferOrders_PurchaseOrders` / 330 `TransferOrderItems_PurchaseOrderItems` |
70
+
71
+ NYCHH specifics:
72
+ - 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
+ - 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
+ - api2 never inlines `transfer-orders.transferOrderItems` (no `InherentRecordChildren` row) → read TO lines via `getTransferOrderItemsByUuid`.
76
+ - 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
+ - Missing ACL is loud: existence GETs throw on no `meta->totalRecordCount`.
78
+ - One-order repair: [process_netsuite_order.php](../../../1.0/apps/worker/workflows/reprocessing-one-netsuite-order.md) `hh --tranid=<n>`. Old `probe_import_single_order_hh.php` / `relink_transfer_order_pos_hh.php` are superseded.
88
79
 
89
80
  ### Transfer-order origin/destination location resolution
90
81
  `TransferOrders.originLocationId` is **NOT NULL**, so a missing origin fails the V2 insert with **EV-10** ("originLocationId cannot be null"). Two fixes were needed:
@@ -199,21 +190,11 @@ Two NYCHH-specific gates, both of which bit during this work:
199
190
 
200
191
  WARNING: NYCHH's `NETSUITE_LAST_SYNC_CURSOR_ITEM_RECEIPTS` is stale at **2024-08-06** - re-enabling item receipts replays about two years unless it is rewound forward first.
201
192
 
202
- ## ⚠ OPEN (2026-09-18) — NYCHH sync is 2.5 YEARS behind on a 403 EZ-1 from records 326/330
203
-
204
- Found by a fleet sweep of `NETSUITE_EXECUTION_MODE_*` across all 22 netsuite clients. NYCHH was the worst: `NETSUITE_LAST_SYNC_CURSOR_PURCHASE_ORDERS` at **2024-02-16** and `NETSUITE_LAST_SYNC_CURSOR_INVOICES` at **2024-02-22**.
205
-
206
- **⚠ The mode read `864000-RUNNING` the whole time and was taken as "healthy, in progress".** That reading was wrong — a full window with a 2.5-year-old cursor is **broken, not busy**. Always pair the execution mode with the cursor; neither is a health signal alone.
207
-
208
- **Error:** `GET /v2/transfer-order-items-purchase-order-items` → **403 `EZ-1`**.
209
-
210
- **Root cause — a different ACL gap from Elite's.** Records **326** (`transfer-orders-purchase-orders`) and **330** (`transfer-order-items-purchase-order-items`) — the **TO-first** direction — have **zero** `AclRecordPermissions` rows. The `2026-09-17a` module migration only covered the PO-first siblings **325/329**. The error shape differs from Elite's silent `200 + WZ-1`: `EZ-1` means the **record** layer is missing, `WZ-1` means the record layer is fine and the **field** layer is empty — use the code to pick which layer to fix.
211
-
212
- **Red herring:** the envelope reports `api: "Agilant"`, which looks like a different API needing different grants. It is not — NYCHH's Agilant API carries `roleId` **1 and 3**, so roleId 3 is still correct.
213
-
214
- **Fix prepared, ⚠ NOT YET RUN:** client-scoped SQL, 5 statements, same four-layer chain and `NOT EXISTS` guards as `2026-09-17a` — permissions for 326/330, `'all'` expressions, logic groups, logic-group expressions, and 6 field grants (**2214/2215/2216** for 326; **2230/2231/2232** for 330; `id` **2213/2229** deliberately excluded, matching 325/329). Chain mechanics on the [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md).
215
-
216
- **Not NYCHH-only.** 326/330 were never in the module migration, so any client whose sync touches the TO-first bridges will hit the same 403.
193
+ ## RESOLVED (2026-09-24) — 403 `EZ-1` on records 326/330 froze PO + INVOICES (cursors 2024-02)
194
+ - `GET /v2/transfer-order-items-purchase-order-items` → 403 `EZ-1`: 326/330 had no `AclRecordPermissions` (only 325/329 were seeded). Fixed by `_modules/netsuite/2026-09-24a` (all NetSuite clients).
195
+ - `EZ-1` = record layer missing; `WZ-1` (200) = field layer missing. Use the code to pick the layer.
196
+ - `864000-RUNNING` with a years-old cursor is **broken, not busy**. Always pair execution mode with the cursor.
197
+ - Envelope `api: "Agilant"` is a red herring: that API carries roleIds 1 and 3.
217
198
 
218
199
  ## Gotchas / known issues
219
200
 
@@ -225,6 +206,7 @@ Found by a fleet sweep of `NETSUITE_EXECUTION_MODE_*` across all 22 netsuite cli
225
206
  - **`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.
226
207
 
227
208
  ## Change history
209
+ - 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)
228
210
  - 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)
229
211
 
230
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)
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [apeterson, bala]
9
+ updated: 2026-09-24
10
+ owners: [apeterson, bala, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/SalesOrder.php
13
13
  - _underscore/Trait/Netsuite/SalesOrder.php
@@ -42,9 +42,12 @@ Symptom that looked like a bug and was not: the sales-order modal's PO Number ca
42
42
  - ✅ **The open consequence is addressed on `_beta` — and NOT with a per-tenant override.** The shared `_purchaseOrders` field read the **downstream** table only (`_underscore/Model/Client/SalesOrder.php:349`), so once the modal bound to `_purchaseOrders` NYCHH resolved it without an `EV-8` and got `null`. Rather than a NYCHH-specific field, the **base** field became a `COALESCE` of downstream-then-upstream, which fixed every upstream tenant at once: NYCHH went from 64 to **819 of 819** orders resolving a PO, with **no existing value changed** for any tenant. It sits on `_beta` / the sandbox branches, **not `_production`** — see [PO Number sourcing](../../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md) for the exact commits. NYCHH is also not the only upstream tenant: all 22 clients on `_Trait_Netsuite_SalesOrder` write upstream.
43
43
  - 🚨 **Still open for NYCHH specifically: the 64 both-direction orders.** Downstream-first makes the modal show the **vendor** PO while the list keeps showing the **customer** PO. A `_Model_Nychh_SalesOrder` override preferring upstream was offered and not built. Decide which one NYCHH should see before treating either surface as authoritative.
44
44
  - **The NYCHH orders list is not a template.** Its table-view join through the upstream bridge is safe **only** because NYCHH's upstream bridge is coincidentally 1:1 (823 rows / 823 distinct sales orders). Copying that join into a shared single-record fetch breaks Compass (up to 6,440 POs on one order), Compass Canada (empty upstream bridge) and Prudential (1,868 of 26,843 upstream).
45
- - **The transfer-order items table inherits the same blank.** `TransferOrderItems` has no line-level PO, so that column binds the **parent order's** `purchaseOrder.number` and repeats on every row — and for NYCHH it resolves NULL, because their PO links are upstream and `TransferOrders.purchaseOrderId` is not populated for this tenant. The column is deliberately left visible in the Core surface (neutral default; other tenants do populate it). See [Transfer Orders page](../../../2.0/apps/toga25-supply/features/transfer-orders-page.md).
45
+ - **Transfer orders (fixed 2026-09-24).** `TransferOrders.purchaseOrderId` was dropped platform-wide; TO PO number = `_purchaseOrderNumbers` (upstream bridge 325, `GROUP_CONCAT`). The Supply header + items `po` column read it. See [Transfer Orders page](../../../2.0/apps/toga25-supply/features/transfer-orders-page.md).
46
46
  - Do not "fix" this by adding downstream rows. The direction is correct for NYCHH's business model; the shared field is what was incomplete.
47
47
 
48
+ ## Change history
49
+ - 2026-09-24 — TO PO number now `_purchaseOrderNumbers` (column 2143 dropped). (jcardinal)
50
+
48
51
  ## Related
49
52
  - [SO↔PO bridge direction](../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md)
50
53
  - [Sales-order PO Number sourcing](../../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-09-24
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - _underscore/Model/Nychh/Item.php
@@ -79,7 +79,7 @@ The item-level bridges mirror the header-level SO↔PO direction split documente
79
79
 
80
80
  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
81
 
82
- - **⚠ PO-level outbound reads 0 until a separate backfill.** `_qtyCommitted` depends on `PurchaseOrderItems_TransferOrderItems`, which is currently **empty (0 rows)** in prod — so PO-level outbound reads 0 until the transfer-order import populates that bridge. Separate import gap, not this change.
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.
83
83
 
84
84
  #### The override is NOT on every branch — NYCHH PO-line quantities differ per environment
85
85
  Verified 2026-09-01 with `git ls-tree` against each `origin/` branch of `_underscore`:
@@ -227,11 +227,14 @@ Discovered 2026-08-28 while standing the Transfer Orders UI up locally: beyond t
227
227
 
228
228
  - **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
229
  - **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, and PO-level outbound reads a still-empty `PurchaseOrderItems_TransferOrderItems` bridge (see above).
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
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.
232
232
  - **⚠ `_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
233
  - **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
234
 
235
+ ## Change history
236
+ - 2026-09-24 — 329 no longer empty; vendor PO lines moved off it to 330. (jcardinal)
237
+
235
238
  ## Related
236
239
  - [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md)
237
240
  - [V2 API error codes](../../../2.0/apps/api2/features/v2-api-error-codes.md)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-23
9
+ updated: 2026-09-24
10
10
  owners: ["bala", "jcardinal"]
11
11
  files:
12
12
  - _underscore/Model/Client/TransferOrder.php
@@ -203,11 +203,13 @@ email, which must fire *after* this push writes `number` back. The `POST` row ab
203
203
 
204
204
  ## Data + metadata state (2026-09-02)
205
205
 
206
- - **Prod 2026-09-23:** `PurchaseOrders_TransferOrders` (325) = **1,592 rows**; `TransferOrders.purchaseOrderId`
207
- set on 10 rows. The Create Transfer Order modal now writes 325 (header) + 329 (lines) in its single POST
208
- ([transfer-orders-page](../../../2.0/apps/toga25-supply/features/transfer-orders-page.md)). An order
209
- with no PO link still makes `resolvePurchaseOrderNumber()` return null → push aborts by design.
210
- - Reverse-named twins 326 / 330 have 0 rows — dead; never read or write them.
206
+ - PO number comes **only** from upstream bridge 325 (customer PO). `TransferOrders.purchaseOrderId` was
207
+ dropped 2026-09-24. The Create Transfer Order modal writes 325 (header) + 329 (lines) in its single POST
208
+ ([transfer-orders-page](../../../2.0/apps/toga25-supply/features/transfer-orders-page.md)). No 325 row →
209
+ `resolvePurchaseOrderNumber()` returns null → push aborts by design. Several POs → joined with `' + '`.
210
+ - 326 / 330 (downstream, vendor POs created FROM the TO) are **live** since 2026-09-24 — written by the
211
+ 1.0 [TO↔PO link reconcile](../../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md).
212
+ Never read them for the PO # sent to NetSuite; that is the customer PO (325).
211
213
  - Prod `InherentRecordChildren` ids: 150 = 312→313, 151 = 312→325, 152 = 312→357; added 2026-09-23
212
214
  (`Core/2026-09-23d`): 17→325, 18→329, 313→329.
213
215
  - RecordFields: 325 = 2209 id / 2210 uuid / 2211 purchaseOrderId / 2212 transferOrderId; 329 = 2225 / 2226 /
@@ -226,6 +228,7 @@ email, which must fire *after* this push writes `number` back. The `POST` row ab
226
228
  It may also need record ACL rows for 325 — untested; an `EZ-1` would be the symptom.
227
229
 
228
230
  ## Change history
231
+ - 2026-09-24 — PO # bridge-only (purchaseOrderId column dropped); 326/330 no longer dead (downstream vendor POs). (jcardinal)
229
232
 
230
233
  - 2026-09-17 — Noted that record **312 now carries a second `PUT` interceptor row**
231
234
  (`2026-09-15a - TransferOrderEmails.sql`), because the Submitted notification email must fire
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-23
9
+ updated: 2026-09-24
10
10
  owners: ["bala", "rgirish", "jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Client/Nychh/TransferOrderEmails.php
@@ -266,6 +266,7 @@ loaded out of the dbchanges2 file so an unresolved `{placeholder}` fails a test.
266
266
  unescaped values and both detail-box defects were all found there, not in production.
267
267
 
268
268
  ## Change history
269
+ - 2026-09-24 — `TransferOrderEmails` reads PO numbers from upstream bridge 325 (column `purchaseOrderId` dropped). (jcardinal)
269
270
  - 2026-09-23 — Submitted template put on review hold; missing REVIEW row → job failure note. (jcardinal)
270
271
 
271
272
  - 2026-09-23 — **The greeting now addresses whoever raised the order, not the site contact.**
@@ -103,7 +103,7 @@ TOGA 2.0 client on `_underscore`, prod schema `Client_Nychh`. Supply-chain / ful
103
103
  - 🚨 **`Client_Nychh.TransferOrders` HAS production data — 1,927 rows (verified 2026-08-31 on `reader1.client.database.togahub.com`).** The transfer-order *tables and data* have been live in prod for a while (the 1.0 NetSuite importer writes them); only the **2026-08-28 UI vertical** (TableView, Core surfaces, nav override) is new. An earlier version said transfer orders were "local only … not yet run on sandbox-client or production", and that stale line produced a wrong recommendation — that a prod-to-local restore would *wipe* the transfer-orders feature. **Never infer prod state from this repo or from a migration's run status; read prod.** Restoring or overwriting `Client_Nychh` destroys real transfer-order rows.
104
104
  - **The Transfer Orders SCREEN is NYCHH-only and new (2026-08-28).** NYCHH is the first — and today the only — tenant with a `transfer-orders` TableView (`Client_Nychh/2026-08-28a`, which also seeds the ACL chain for record 351 that the Status column silently depends on) and the one `SurfaceOverrides` row that reveals the nav item (`2026-08-28b`, client-wide, matching how this tenant grants Sales Orders and Inventory — NYCHH does not scope navigation by role). The Core surfaces and stage colour tokens are **shared/neutral**, hidden for every other client until they opt in. The seven migration files were **committed 2026-08-31** (`dbchanges2` commit `1c3fe2e`, branch `TRUE-80852`, pushed); run order is **`Core/` → `Client/` fan-out → `Client_Nychh/`**, because the client files reference Core record ids and the NYCHH TableView/ACL need the columns the fan-out files add. Run state per environment must be **verified against that environment**, never assumed from the repo. **beta is blocked** because it lacks the `TransferOrderStages` table. The **Create Transfer Order** modal was reworked 2026-09-01 and its data layer is wired, but 🚨 **the item picker shows zero selectable lines for NYCHH** — see the availability blocker below. See [Transfer Orders page](../../2.0/apps/toga25-supply/features/transfer-orders-page.md).
105
105
  - 🚨 **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
- - **Create Transfer Order links POs (2026-09-23)** — one POST writes header bridge 325 + line bridge 329 (Core `2026-09-23d` inherent children). Prod 325 has 1,592 rows; 326/330 twins are dead. See [transfer-order push](./features/transfer-order-netsuite-push.md).
106
+ - **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
107
  - **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
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).
109
109
  - 🚨 **`_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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.889",
3
+ "version": "1.0.891",
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",