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.
- package/knowledge/1.0/apps/library/INDEX.md +1 -0
- package/knowledge/1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md +63 -0
- package/knowledge/1.0/apps/worker/INDEX.md +1 -0
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +23 -9
- package/knowledge/1.0/apps/worker/workflows/reprocessing-one-netsuite-order.md +52 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md +22 -3
- package/knowledge/2.0/apps/toga25-supply/features/transfer-orders-page.md +8 -4
- package/knowledge/2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md +10 -3
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/elite/features/netsuite-togasupply-sync.md +7 -1
- package/knowledge/clients/elite/profile.md +5 -5
- package/knowledge/clients/growrk/features/transfer-order-flow.md +7 -2
- package/knowledge/clients/nychh/features/location-shipping-addresses.md +2 -2
- package/knowledge/clients/nychh/features/netsuite-transfer-order-import.md +24 -42
- package/knowledge/clients/nychh/features/po-number-upstream-direction.md +6 -3
- package/knowledge/clients/nychh/features/transfer-order-inventory-quantities.md +6 -3
- package/knowledge/clients/nychh/features/transfer-order-netsuite-push.md +9 -6
- package/knowledge/clients/nychh/features/transfer-order-notification-emails.md +2 -1
- package/knowledge/clients/nychh/profile.md +1 -1
- 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` → `
|
|
824
|
-
(downstream
|
|
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)**
|
|
830
|
-
`
|
|
831
|
-
|
|
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
|
-
**
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
|
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 |
|
package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
-
|
|
339
|
-
-
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
- **
|
|
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.
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ —
|
|
8
|
-
- **worker** (Worker) —
|
|
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-
|
|
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
|
-
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
|
|
78
|
-
(
|
|
79
|
-
GroWrk-specific rules
|
|
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-
|
|
10
|
-
owners: [
|
|
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-
|
|
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
|
-
-
|
|
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/
|
|
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.
|
|
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
|
-
|
|
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 |
|
|
66
|
+
| Order routed to | Upstream (customer PO) | Downstream (vendor PO) |
|
|
64
67
|
|---|---|---|
|
|
65
|
-
| stocking → SalesOrders | `PurchaseOrders_SalesOrders`
|
|
66
|
-
| transfer → TransferOrders | `PurchaseOrders_TransferOrders`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
##
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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-
|
|
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
|
-
- **
|
|
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-
|
|
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
|
-
-
|
|
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
|
|
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-
|
|
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
|
-
- **
|
|
207
|
-
|
|
208
|
-
([transfer-orders-page](../../../2.0/apps/toga25-supply/features/transfer-orders-page.md)).
|
|
209
|
-
|
|
210
|
-
-
|
|
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-
|
|
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).
|
|
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