toga-ai 1.0.767 → 1.0.768

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.
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-09-03
10
- owners: [dfranks, jcardinal, kyalamarthi]
10
+ owners: [dfranks, jcardinal, kyalamarthi, rgirish]
11
11
  files:
12
12
  - library/app/api/netsuite/rest.php
13
13
  - library/app/netsuite.php
@@ -78,6 +78,22 @@ Forecast2 tables already store.
78
78
  a per-type factor (it double-negates reversals back to positive). The shim flips
79
79
  `amount = -foreignamount`, `costEstimate = -costestimate`.
80
80
 
81
+ - **⚠ `->holdInvoice` on a sales-order shim is NOT a native NetSuite field — it is normalised from
82
+ the custom checkbox `custbody_asi_hold` (internal id 4082), and that field is SHARED by all
83
+ clients.** NetSuite REST exposes no top-level `holdInvoice`. The `"asi"` prefix is **Agilant
84
+ Solutions Inc** (the company), *not* a client prefix — do not read it as client-specific. The read
85
+ lives **inside the shim**, in all three fetch paths: `buildSalesOrderShims()` (~L533, feeding both
86
+ `listSalesOrders()` and `fetchSalesOrderByIdFull()`) and `fetchSalesOrderById()` (~L1537), so
87
+ callers such as `App_Api_Toga2::isTransferOrder()` read an already-normalised boolean. (An earlier
88
+ version read `$nsOrder->holdInvoice` in the caller with a `custbody_hold_invoice` fallback — the
89
+ wrong field name; commit `07c31e24`, 2026-08-19, found the real one and moved the read down here.)
90
+ `worker/crons/toga2/compass/edi/2_create_so_netsuite.php` **writes** the same field.
91
+ **⚠ The SOAP client `library/app/netsuite.php` never sets `holdInvoice` at all**, so any detection
92
+ that depends on it (e.g. the `zero_dollar_hold` transfer-order mode — see
93
+ [per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md)) is **silently
94
+ always-false** on a SOAP path. Verified not a live risk: the togasupply engine calls
95
+ `App_Api_Netsuite_Rest::` on every sales-order/receipt/fulfillment path.
96
+
81
97
  - **`createdFrom` is not a usable SuiteQL column.** `transaction.createdfrom` is **always NULL**
82
98
  in SuiteQL (26/26). It is recoverable from `previoustransactionlinelink.previousdoc` excluding
83
99
  `previoustype = 'Opprtnty'` (an invoice links to both its SO and its opportunity; REST's
@@ -307,6 +323,16 @@ None — uniform across clients (NetSuite is a single shared account).
307
323
 
308
324
  ## Change history
309
325
 
326
+ - 2026-09-03 — Documented that a sales-order shim's **`->holdInvoice` is not a native NetSuite
327
+ field**: it is normalised inside the shim from the custom checkbox **`custbody_asi_hold` (id
328
+ 4082)**, which is **shared across all clients** ("asi" = Agilant Solutions Inc, not a client
329
+ prefix). Set in all three fetch paths (`buildSalesOrderShims()` feeding `listSalesOrders()` /
330
+ `fetchSalesOrderByIdFull()`, plus `fetchSalesOrderById()`); traced to commit `07c31e24`
331
+ (2026-08-19), which corrected the earlier caller-side read of `custbody_hold_invoice`. Recorded the
332
+ trap that the **SOAP client `app/netsuite.php` never sets it**, so hold-based detection is silently
333
+ always-false there — verified harmless today because the togasupply engine is REST-only.
334
+ Read-only session; no change to `rest.php`. (rgirish)
335
+
310
336
  - 2026-09-03 — Recorded that **`fetchItemFulfillmentById` and `fetchInventoryAdjustmentById` omitted
311
337
  `->lastModifiedDate`** (unlike `fetchItemReceiptById`), which froze ITEM_FULFILLMENTS + INVENTORY_ADJUSTMENTS
312
338
  once the 2026-09-02 per-record cursor read that field off the by-reference record these shims replace. GroWrk
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-09-03
10
- owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
10
+ owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
13
13
  - worker/crons/toga2/netsuite/sync_togasupply_canon.php
@@ -463,29 +463,71 @@ the code deploy).
463
463
 
464
464
  ## Client variations
465
465
 
466
- - **Transfer-order routing is opt-in per wrapper (default OFF).** A client whose NetSuite
467
- *transfer orders* arrive as NetSuite *sales orders* can have the importer split them into
468
- `TransferOrders` vs `SalesOrders` by setting
469
- `const IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING = true;` before the `require_once`
470
- (NYCHH's `sync_togasupply_hh.php` is the only one that does). The signal is the NetSuite custom
471
- checkbox `custbody_stocking_order`, read by `App_Api_Toga2::isTransferOrder()`/`isStockingOrder()`
472
- — **not** a dollar/hold heuristic. Full mechanics, PO bridges, and origin-location resolution:
473
- [NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
474
- - **Transfer-order integration is governed by TWO DISTINCT wrapper flags — a master enable switch and a
475
- NYCHH-only classification-mode switch (do not conflate them).**
466
+ - **Transfer-order integration is governed by TWO DISTINCT wrapper constants — a master enable switch
467
+ and an explicit per-client detection MODE (do not conflate them).** These orders **masquerade as
468
+ NetSuite sales orders** and are split into `TransferOrders` vs `SalesOrders` by
469
+ `App_Api_Toga2::isTransferOrder()`.
476
470
  - **`IS_ENABLED_INTEGRATION_TRANSFER_ORDERS`** — the master "do transfer-order integration at all"
477
- switch, defined in every `sync_togasupply_*.php` launcher. `true` for **GroWrk, Adyen, Elite, and
478
- NYCHH**; `false` for the rest. These are the clients exercised by the shared
479
- `syncTransferOrderFromNetsuite` / `getTransferOrderItemsByUuid` fixes (2026-08-30 above).
480
- - **`IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`** — a **separate classification-mode** switch,
481
- **NYCHH-only** (`sync_togasupply_hh.php`), that makes `App_Api_Toga2::isTransferOrder()` classify by
482
- the NetSuite `custbody_stocking_order` checkbox instead of the legacy rule.
483
-
484
- These orders **masquerade as NetSuite sales orders** and are split into `TransferOrders` vs
485
- `SalesOrders` by `isTransferOrder()`. The classification **SIGNAL is per-client**: **NYCHH** routes by
486
- the `custbody_stocking_order` stocking flag (above); **GroWrk / Elite / Adyen** use the **legacy rule**
487
- (`holdInvoice AND total === 0`). Do **not** assume one universal signal — the master enable is shared,
488
- the classification mode is not.
471
+ switch, defined in every `sync_togasupply_*.php` launcher. `true` for **GroWrk, Adyen, and
472
+ NYCHH**; `false` for Elite (pre-wired, not on) and the rest. **Every one of the 7
473
+ `isTransferOrder()` call sites is guarded by this**, so the mode throw below can never fire for a
474
+ client with transfer orders off.
475
+ - **`TRANSFER_ORDER_DETECTION_MODE`** — the classification mode, one of two `App_Api_Toga2` class
476
+ constants (see next bullet). Required whenever the master switch is on.
477
+
478
+ ### ⚠ `TRANSFER_ORDER_DETECTION_MODE` — explicit per client, NO default, throws if unset (2026-09-03)
479
+
480
+ `isTransferOrder()` used to branch on a boolean `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`
481
+ (defined only in NYCHH's launcher) and **otherwise fall through to GroWrk's legacy `$0 total +
482
+ holdInvoice` rule**. That made the legacy rule an **implicit, silent default**: a new client turning
483
+ transfer orders on without the constant silently got another client's classification applied to its
484
+ orders, on a green-looking run. That boolean is **removed everywhere**.
485
+
486
+ Each launcher now declares an explicit mode before its `require_once`:
487
+
488
+ ```php
489
+ const TRANSFER_ORDER_DETECTION_MODE = App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__STOCKING_FLAG;
490
+ // or
491
+ const TRANSFER_ORDER_DETECTION_MODE = App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD;
492
+ ```
493
+
494
+ | Mode constant | Value | Rule | Clients |
495
+ |---|---|---|---|
496
+ | `TRANSFER_ORDER_DETECTION_MODE__STOCKING_FLAG` | `'stocking_flag'` | NetSuite checkbox `custbody_stocking_order` (id 7097) — **unchecked = transfer order** | NYCHH |
497
+ | `TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD` | `'zero_dollar_hold'` | `holdInvoice === true` **AND** `total === 0` | GroWrk, Adyen, Elite (pre-set, unused — its master switch is off) |
498
+
499
+ `App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODES` is the allowlist array. **`isTransferOrder()` THROWS
500
+ when the mode is undefined or not in the allowlist** — a deliberate fail-loud, chosen over defaulting
501
+ to the legacy rule, consistent with the 2026-08-27 "every section fails loud" policy. Consequence,
502
+ and it is not optional: **`library` and `worker` MUST deploy together.** A worker deploy without the
503
+ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
504
+
505
+ - **A separate NYCHH-only flag, `IS_ENABLED_INVENTORY_ADJUSTMENT_FULFILLMENT_LINK`,** now guards
506
+ `attachInventoryAdjustmentToItemFulfillment()` (the `custbody_stock_adjustment` link). The removed
507
+ routing boolean had been doing double duty for it; it is an unrelated feature that merely has NYCHH
508
+ as its only client today. See
509
+ [NYCHH inventory-adjustment → item-fulfillment linking](../../../clients/nychh/features/netsuite-inventory-adjustment-fulfillment-link.md).
510
+
511
+ - **`->holdInvoice` is NOT a native NetSuite field** — it is normalised by the REST shim from the
512
+ custom checkbox `custbody_asi_hold` (internal id **4082**), which is **shared across all clients**
513
+ ("asi" = Agilant Solutions Inc, the company, not a client prefix). `App_Api_Netsuite_Rest` sets it
514
+ in `buildSalesOrderShims()` (feeding `listSalesOrders()` + `fetchSalesOrderByIdFull()`) and in
515
+ `fetchSalesOrderById()`. **`app/netsuite.php` (SOAP) never sets it at all** — ZERO_DOLLAR_HOLD would
516
+ be silently always-false on a SOAP path. Not a live risk: `common_sync_togasupply.php` uses
517
+ `App_Api_Netsuite_Rest::` on **every** sales-order / receipt / fulfillment path, and all
518
+ transfer-order clients are on REST. Field detail:
519
+ [SuiteQL/REST shim](../../library/features/netsuite-suiteql-rest-shim.md).
520
+
521
+ - **⚠ OPEN (pre-existing, not fixed): an absent NetSuite `total` collapses to `0.0` and can
522
+ misclassify.** The ZERO_DOLLAR_HOLD branch reads
523
+ `$total = isset($nsOrder->total) ? (float) $nsOrder->total : 0.0;`. A REST response with a
524
+ missing/blank `total` (transient API failure — not a real $0 order) becomes `0.0`, and combined with
525
+ the hold flag set, a **non-zero order classifies as a transfer order**. This line predates the mode
526
+ refactor and was carried across unchanged; it goes against the fail-loud rule for cron/sync code.
527
+ Raised with the developer, left as-is pending a decision.
528
+ - **Full mechanics of the stocking-flag mode** (PO bridges, origin-location resolution):
529
+ [NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
530
+ Legacy-rule background: [GroWrk transfer order flow](../../../clients/growrk/features/transfer-order-flow.md).
489
531
  - `isParentCustomer` — `true` walks NetSuite child customers (`listChildCustomers`); `false`
490
532
  syncs the single customer. Prudential-EDI and the live Quad wrapper use `false`.
491
533
  - The original combined-array entries used `App_Api_Toga2::CLIENT_UUID_*` consts for Quad and
@@ -1212,6 +1254,23 @@ the code deploy).
1212
1254
 
1213
1255
  ## Change history
1214
1256
 
1257
+ - 2026-09-03 — **Transfer-order detection is now an explicit per-client MODE with no default; the old
1258
+ `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING` boolean is removed from both repos.**
1259
+ `isTransferOrder()` previously branched on that boolean (NYCHH-only) and otherwise fell through to
1260
+ GroWrk's `$0 total + holdInvoice` rule — an implicit silent default that would apply one client's
1261
+ classification to another's orders on a green-looking run. Launchers now set
1262
+ `TRANSFER_ORDER_DETECTION_MODE` to one of two new `App_Api_Toga2` constants
1263
+ (`__STOCKING_FLAG` = NYCHH, `__ZERO_DOLLAR_HOLD` = GroWrk/Adyen/Elite), validated against a
1264
+ `TRANSFER_ORDER_DETECTION_MODES` allowlist; an undefined/unknown mode **throws**. All 7 call sites
1265
+ are guarded by `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS`, so the throw can't fire for a client with
1266
+ TOs off. The `custbody_stock_adjustment` adjustment→fulfillment link, which the removed boolean was
1267
+ also guarding, moved to its own `IS_ENABLED_INVENTORY_ADJUSTMENT_FULFILLMENT_LINK` (NYCHH only).
1268
+ Also recorded: `->holdInvoice` is the REST shim's normalisation of the **shared** custom checkbox
1269
+ `custbody_asi_hold` (id 4082, "asi" = Agilant Solutions Inc, not a client), never set on the SOAP
1270
+ path — safe today because this engine is REST-only; and the pre-existing `total ?: 0.0` default in
1271
+ the ZERO_DOLLAR_HOLD branch can misclassify on a transient NetSuite failure (open, unchanged).
1272
+ **`library` + `worker` must deploy together** or GroWrk and Adyen throw on their next run. (rgirish)
1273
+
1215
1274
  - 2026-09-03 — **By-reference shim wrote the cursor on the WRONG CLOCK → ITEM_RECEIPTS + ITEM_FULFILLMENTS
1216
1275
  re-matched the same record forever (fixed, `library` only).** Follow-on to the same-day by-reference
1217
1276
  fix below: the shims set `lastModifiedDate` from the REST GET (UTC), `date()`-formatted in
@@ -45,6 +45,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
45
45
 
46
46
  ## Clients
47
47
 
48
+ - **Adyen** (`adyen`) → [clients/adyen/INDEX.md](clients/adyen/INDEX.md)
48
49
  - **AIG (Staples Protection Plan)** (`aig`) → [clients/aig/INDEX.md](clients/aig/INDEX.md)
49
50
  - **Canon** (`canon`) → [clients/canon/INDEX.md](clients/canon/INDEX.md)
50
51
  - **Compass Canada** (`compass-canada`) → [clients/compass-canada/INDEX.md](clients/compass-canada/INDEX.md)
@@ -0,0 +1,5 @@
1
+ # Client: Adyen `adyen`
2
+
3
+ | Doc | Framework | Summary | Files |
4
+ |-----|-----------|---------|-------|
5
+ | [Adyen](profile.md) | 2.0 | Adyen is a live TOGa Supply client (`Client_Adyen` database) on the shared **NetSuite → TOGa Supply** importer, with its own 1.0 launcher `worker/crons/toga2/ne | worker/crons/toga2/netsuite/sync_togasupply_adyen.php, library/app/api/toga2.php |
@@ -0,0 +1,59 @@
1
+ ---
2
+ title: Adyen
3
+ framework: "2.0"
4
+ apps:
5
+ - worker
6
+ - library
7
+ - _underscore
8
+ - api2
9
+ - dbchanges2
10
+ project: Worker
11
+ client: adyen
12
+ type: profile
13
+ status: draft
14
+ updated: 2026-09-03
15
+ owners: ["rgirish"]
16
+ files:
17
+ - worker/crons/toga2/netsuite/sync_togasupply_adyen.php
18
+ - library/app/api/toga2.php
19
+ related:
20
+ - ../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
21
+ - ../growrk/features/transfer-order-flow.md
22
+ - ../../2.0/apps/_underscore/features/acl-permission-chain.md
23
+ ---
24
+
25
+ ## Summary
26
+
27
+ Adyen is a live TOGa Supply client (`Client_Adyen` database) on the shared **NetSuite → TOGa Supply**
28
+ importer, with its own 1.0 launcher `worker/crons/toga2/netsuite/sync_togasupply_adyen.php`.
29
+
30
+ **This profile was created as a stub (2026-09-03).** Adyen had no `clients/` folder despite being a
31
+ live, scheduled client — it surfaced while refactoring transfer-order detection, because Adyen is one
32
+ of the clients whose launcher carries `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS = true`. It is
33
+ deliberately marked `status: draft`: the entries below are what is verifiable from code and existing
34
+ docs, not a curated client picture. **Anyone doing Adyen work should flesh this out.** Whether Adyen
35
+ was previously undocumented intentionally has not been confirmed.
36
+
37
+ ## What is known
38
+
39
+ - **NetSuite → TOGa Supply sync.** A per-client thin wrapper over the shared engine — full mechanics
40
+ on [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
41
+ - **Transfer orders are ENABLED** (`IS_ENABLED_INTEGRATION_TRANSFER_ORDERS = true`). Adyen's NetSuite
42
+ transfer orders arrive **as NetSuite sales orders** and are classified by the
43
+ `TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD` mode — `holdInvoice === true` **AND**
44
+ `total === 0`, the same rule as GroWrk (see
45
+ [GroWrk transfer order flow](../growrk/features/transfer-order-flow.md)). This mode is now declared
46
+ explicitly in Adyen's launcher; `App_Api_Toga2::isTransferOrder()` throws if it is missing, so
47
+ **`library` and `worker` must deploy together** for Adyen.
48
+ - ⚠ **Not independently verified against Adyen's NetSuite account.** The mode preserves the
49
+ behavior Adyen had under the old implicit default; nobody has confirmed with the client that $0 +
50
+ hold is the right signal *for Adyen*. Worth checking before trusting the split.
51
+ - **`Client_Adyen` is one of the client DBs with surface-layer / ACL drift** noted on the
52
+ [ACL permission chain](../../2.0/apps/_underscore/features/acl-permission-chain.md) and
53
+ [surface-layer schema](../../2.0/apps/dbchanges2/features/surface-layer-schema.md) docs.
54
+
55
+ ## Open questions
56
+
57
+ - Formal/legal client name and account owner — recorded here simply as **Adyen**.
58
+ - Which repos beyond the `apps:` list above are genuinely in Adyen's scope.
59
+ - Whether Adyen has client-specific models/features that deserve their own docs.
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-02
10
- owners: ["snaredla", "jcardinal"]
9
+ updated: 2026-09-03
10
+ owners: ["snaredla", "jcardinal", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/sync_togasupply_elite.php
13
13
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -88,6 +88,13 @@ the trait declares must exist as a column, a `CustomRecordFields` row **and** an
88
88
  so it always trails.)
89
89
  - **`MIN_DATETIME_TO_CHECK_FOR_NETSUITE_DATA = '2018-01-01'`**, with the real start controlled by the
90
90
  seeded `Parameters` cursors.
91
+ - **`TRANSFER_ORDER_DETECTION_MODE` is pre-set but NOT in use (2026-09-03).** Elite's wrapper declares
92
+ `App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD` (`holdInvoice && total === 0`, the
93
+ GroWrk rule) so that the constant exists if transfer orders are ever switched on — but Elite's
94
+ master switch `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` is **`false`**, so `isTransferOrder()` is
95
+ never reached today. Before flipping the master switch, confirm Elite's NetSuite actually represents
96
+ transfer orders as $0 held sales orders; do not assume the pre-set mode is correct for them. Mode
97
+ mechanics: [per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
91
98
 
92
99
  ## Gotchas / known issues
93
100
 
@@ -125,6 +132,13 @@ the trait declares must exist as a column, a `CustomRecordFields` row **and** an
125
132
 
126
133
  ## Change history
127
134
 
135
+ - 2026-09-03 — Elite's wrapper now declares `TRANSFER_ORDER_DETECTION_MODE =
136
+ App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD` as part of the shared
137
+ transfer-order detection-mode refactor (the old `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`
138
+ boolean was removed and there is no longer a default mode). **Dormant for Elite** —
139
+ `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` is `false`, so `isTransferOrder()` is never reached; verify
140
+ the rule against Elite's NetSuite before enabling. (rgirish)
141
+
128
142
  - 2026-09-02 — **Elite PURCHASE_ORDERS froze on a dropped PO line that still had a receipt** (shared
129
143
  fix, `library` only — now DEPLOYED to `_production`, PO import confirmed unfrozen on Elite). Elite was
130
144
  the confirmed prod case: `NETSUITE_EXECUTION_MODE_PURCHASE_ORDERS='1-RUNNING'`, watermark frozen at **2025-10-13**,
@@ -3,6 +3,6 @@
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
5
  | [GroWrk — shipping-carrier cleanup, UPS visibility, and the NetSuite Fulfill-and-Ship punch-out URL](features/shipping-carrier-cleanup.md) | 2.0 | GroWrk (`Core.Clients` id **33**, DB `Client_Growrk`) is where the runaway `ShippingCarriers` duplication was found and fixed (TRUE-81576). | _underscore/Trait/Netsuite/SalesOrder.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts |
6
- | [Transfer Order Flow (NetSuite SalesOrder workaround → dedicated TransferOrders)](features/transfer-order-flow.md) | 2.0 | GroWrk moves inventory between their own locations (warehouses, distribution points). | _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemReceipt.php, _underscore/Model/Client/ItemReceiptItem.php, _underscore/Trait/Netsuite/TransferOrder.php, dbchanges2/Client_Growrk/, dbchanges2/Client/ |
6
+ | [Transfer Order Flow (NetSuite SalesOrder workaround → dedicated TransferOrders)](features/transfer-order-flow.md) | 2.0 | GroWrk moves inventory between their own locations (warehouses, distribution points). | _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemReceipt.php, _underscore/Model/Client/ItemReceiptItem.php, _underscore/Trait/Netsuite/TransferOrder.php, dbchanges2/Client_Growrk/, dbchanges2/Client/, worker/crons/toga2/netsuite/sync_togasupply_growrk.php, library/app/api/toga2.php |
7
7
  | [GroWrk NetSuite-module custom fields — the never-executed 2026-05-28 commit (Units + ServiceRequests 500s)](features/units-netsuite-custom-fields.md) | 2.0 | GroWrk is on the dbchanges2 **`netsuite`** module (`dbchanges2/Client_Growrk/_modules.txt`), so its Units carry the module's seven `c_` custom fields. | _underscore/Trait/Netsuite/Unit.php, _underscore/Model/Growrk/Unit.php, _underscore/Model/Growrk/ItemFulfillment.php, dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/Client_Growrk/_modules.txt |
8
8
  | [GroWrk](profile.md) | 2.0 | GroWrk is a 2.0 client using TOGA for inventory and order management. | |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: growrk
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-06-19
9
+ updated: 2026-09-03
10
10
  owners: ["rgirish"]
11
11
  files:
12
12
  - _underscore/Model/Client/TransferOrder.php
@@ -17,9 +17,14 @@ files:
17
17
  - _underscore/Trait/Netsuite/TransferOrder.php
18
18
  - dbchanges2/Client_Growrk/
19
19
  - dbchanges2/Client/
20
+ - worker/crons/toga2/netsuite/sync_togasupply_growrk.php
21
+ - library/app/api/toga2.php
20
22
  related:
21
23
  - 2.0/apps/_underscore/features/recursive-item-fulfillments.md
22
24
  - 2.0/apps/worker2/architecture.md
25
+ - ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
26
+ - ../../../1.0/apps/library/features/netsuite-suiteql-rest-shim.md
27
+ - ../../nychh/features/netsuite-transfer-order-import.md
23
28
  ---
24
29
 
25
30
  ## Summary
@@ -40,8 +45,28 @@ GroWrk moves inventory between their own locations (warehouses, distribution poi
40
45
 
41
46
  ## How it works
42
47
 
43
- ### Identification signal
44
- A NetSuite SalesOrder is a TransferOrder if and only if: `holdInvoice === true` AND `total === 0`. Both conditions must hold — `holdInvoice` alone is not sufficient.
48
+ ### Identification signal — now a NAMED, EXPLICIT mode (2026-09-03)
49
+
50
+ A NetSuite SalesOrder is a TransferOrder if and only if: `holdInvoice === true` **AND** `total === 0`.
51
+ Both conditions must hold — `holdInvoice` alone is not sufficient.
52
+
53
+ This rule is **GroWrk's**, not universal (NYCHH uses the `custbody_stocking_order` stocking flag), and
54
+ it is no longer an implicit fallback. It is now the named mode
55
+ `App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD` (`'zero_dollar_hold'`), declared
56
+ explicitly in GroWrk's 1.0 launcher `worker/crons/toga2/netsuite/sync_togasupply_growrk.php`:
57
+
58
+ ```php
59
+ const TRANSFER_ORDER_DETECTION_MODE = App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD;
60
+ ```
61
+
62
+ `App_Api_Toga2::isTransferOrder()` **throws** if the mode is missing — so GroWrk's launcher and the
63
+ library must deploy together. Adyen and Elite use the same mode. Full mechanics:
64
+ [per-client sync — transfer-order detection mode](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
65
+
66
+ **`holdInvoice` is not a native NetSuite field** — the REST shim normalises it from the custom
67
+ checkbox `custbody_asi_hold` (id **4082**), which is shared by every client ("asi" = Agilant Solutions
68
+ Inc, the company). The SOAP client never sets it. See the
69
+ [SuiteQL/REST shim](../../../1.0/apps/library/features/netsuite-suiteql-rest-shim.md).
45
70
 
46
71
  ### Two-stage closure
47
72
  - **Outbound:** An `ItemFulfillment` with `transferOrderId` set (not `salesOrderId`) closes the outbound side — items leave the origin location.
@@ -113,7 +138,24 @@ Full plan saved at `/Applications/AMPPS/www/transfer-order-plan.md`.
113
138
  - **Lazy transaction gotcha.** `_Database::register()` auto-starts a lazy transaction since Apr 2026. The worker must call `_Database::transactionCommit(DB_CLIENT)` before any SQS dispatch or the TO insert rolls back silently.
114
139
  - **Bridge table scope confirmed as all-clients.** The 8 bridge tables go in `dbchanges2/Client/` (not `Client_Growrk/`). Schema fixes (dtCreated etc.) stay in `Client_Growrk/` — other clients don't need them.
115
140
  - **NS identification: both conditions required.** `holdInvoice=true` alone is not a safe signal — must also confirm `total === 0`.
141
+ - **⚠ An ABSENT NetSuite `total` reads as `0.0` and can misclassify a real order (open, unfixed).**
142
+ The 1.0 detection branch does `$total = isset($nsOrder->total) ? (float) $nsOrder->total : 0.0;`, so
143
+ a NetSuite REST response with a missing/blank total — a transient API failure, not a genuine $0
144
+ order — collapses to `0.0`; combined with the hold flag set, a **non-zero order is classified as a
145
+ transfer order**. Pre-existing, carried unchanged through the 2026-09-03 mode refactor, and raised
146
+ with the developer. If you see an unexpected TransferOrder for GroWrk, check this first.
147
+ - **The mode is now mandatory in the launcher.** Removing/forgetting
148
+ `TRANSFER_ORDER_DETECTION_MODE` in `sync_togasupply_growrk.php` makes `isTransferOrder()` **throw**
149
+ — GroWrk's sync will fail loud, by design, rather than silently reclassify.
116
150
 
117
151
  ## Change history
118
152
 
153
+ - 2026-09-03 — GroWrk's `holdInvoice && total === 0` rule was **formalised as the named mode**
154
+ `App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD`, declared explicitly in
155
+ `worker/crons/toga2/netsuite/sync_togasupply_growrk.php`. It had been the *implicit fallback* in
156
+ `isTransferOrder()`, which silently applied GroWrk's rule to any other client that enabled transfer
157
+ orders; there is now no default and an unset mode throws. Recorded that `holdInvoice` is the REST
158
+ shim's normalisation of the shared custom checkbox `custbody_asi_hold` (id 4082) and that the SOAP
159
+ client never sets it, plus the open `total ?: 0.0` misclassification risk. **library + worker must
160
+ deploy together** or GroWrk throws on its next sync run. (rgirish)
119
161
  - 2026-06-19 — Designed full transfer order flow; confirmed DB state in prod; produced 5-phase implementation plan pending team review (rgirish)
@@ -6,8 +6,8 @@ project: Library
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-01
10
- owners: [jcardinal]
9
+ updated: 2026-09-03
10
+ owners: [jcardinal, rgirish]
11
11
  files:
12
12
  - library/app/api/toga2.php
13
13
  - library/app/api/netsuite/rest.php
@@ -46,7 +46,13 @@ on-demand fulfillment/sales-order import it triggers — was **migrated from SOA
46
46
 
47
47
  Like [NYCHH transfer-order import](./netsuite-transfer-order-import.md), the FK columns and models
48
48
  are **all-client infrastructure** but the **attach is gated NYCHH-only** by
49
- `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`.
49
+ `IS_ENABLED_INVENTORY_ADJUSTMENT_FULFILLMENT_LINK` (defined `true` in `sync_togasupply_hh.php`).
50
+
51
+ > **Renamed 2026-09-03.** This used to piggyback on the transfer-order routing boolean
52
+ > `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`, which was doing double duty. That constant is
53
+ > gone; the adjustment→fulfillment link now has its **own** flag and is fully independent of
54
+ > transfer-order detection (which is now a per-client *mode*, not a boolean — see
55
+ > [per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)).
50
56
 
51
57
  ## The two FK links (replaced the header/line bridge tables)
52
58
 
@@ -63,7 +69,7 @@ link is now a plain PUT of the FK uuid onto the parent record, not a bridge-row
63
69
  ## Attach code — `App_Api_Toga2::attachInventoryAdjustmentToItemFulfillment`
64
70
 
65
71
  Lives in `library/app/api/toga2.php`, called from `syncInventoryAdjustmentFromNetsuite` **after the
66
- header upsert**, gated on `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`. It:
72
+ header upsert**, gated on `IS_ENABLED_INVENTORY_ADJUSTMENT_FULFILLMENT_LINK`. It:
67
73
 
68
74
  1. reads `custbody_stock_adjustment` off the adjustment (the related Item Fulfillment),
69
75
  2. resolves that fulfillment by `ItemFulfillments.c_netsuiteInternalItemFulfillmentId`,
@@ -221,6 +227,11 @@ this feature on the scheduled cron.
221
227
 
222
228
  ## Change history
223
229
 
230
+ - 2026-09-03 — **Gate flag split off onto its own constant.** The attach was guarded by the
231
+ transfer-order routing boolean `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`, which was doing
232
+ double duty; that constant was removed when transfer-order detection became a per-client *mode*.
233
+ The link is now gated by `IS_ENABLED_INVENTORY_ADJUSTMENT_FULFILLMENT_LINK` (NYCHH launcher only) —
234
+ same behavior, no longer coupled to routing. **library + worker deploy together.** (rgirish)
224
235
  - 2026-09-01 — **Unfroze NYCHH `INVENTORY_ADJUSTMENTS` — serial-CASE mismatch in the `/units` lookup
225
236
  (shared `library`, working-tree, not yet committed/deployed).** `syncInventoryAdjustmentFromNetsuite`
226
237
  keyed the units lookup by the returned serial (`'n/a'`) but probed with the uppercased NetSuite serial
@@ -6,8 +6,8 @@ project: Library
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-02
10
- owners: [jcardinal, bala]
9
+ updated: 2026-09-03
10
+ owners: [jcardinal, bala, rgirish]
11
11
  files:
12
12
  - library/app/api/toga2.php
13
13
  - library/app/api/netsuite/rest.php
@@ -65,11 +65,16 @@ private `linkPurchaseOrderToTransferOrder()` mirrors the existing sales-order PO
65
65
  > [GroWrk transfer order flow](../../growrk/features/transfer-order-flow.md) — is client-specific and
66
66
  > not universal; the TO-detection signal must be read per client.)
67
67
 
68
- **Routing is opt-in per client, default OFF.** The shared engine only routes to TransferOrders when
69
- the wrapper enables it. NYCHH's wrapper sets
70
- `const IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING = true;` in
71
- `worker/crons/toga2/netsuite/sync_togasupply_hh.php`; every other client inherits the default (off)
72
- and behaves exactly as before.
68
+ **Routing mode is declared per client — there is NO default (2026-09-03).** The old boolean
69
+ `IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING` is **gone**. NYCHH's wrapper now sets
70
+
71
+ ```php
72
+ const TRANSFER_ORDER_DETECTION_MODE = App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__STOCKING_FLAG;
73
+ ```
74
+
75
+ in `worker/crons/toga2/netsuite/sync_togasupply_hh.php`. `isTransferOrder()` **throws** if the mode
76
+ is undefined or not in the `TRANSFER_ORDER_DETECTION_MODES` allowlist — see the shared
77
+ [per-client sync — transfer-order detection mode](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
73
78
 
74
79
  ## PO ↔ order bridge links (header + line)
75
80
 
@@ -246,16 +251,29 @@ refused on the prod client cluster):
246
251
  broken.
247
252
  - **Order 128829 (no location anywhere) is unresolved** — see the origin-resolution section. Do not
248
253
  assume the importer is broken if this single order is stuck; every located order imports.
249
- - **Routing is gated by the wrapper constant.** If a future client should route transfer orders,
250
- add `const IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING = true;` before its
251
- `require_once 'common_sync_togasupply.php';` — and confirm that client actually sets
252
- `custbody_stocking_order` in NetSuite. Absent the flag, everything lands in SalesOrders.
254
+ - **Routing mode is declared by the wrapper and there is NO fallback.** If a future client should
255
+ route transfer orders by the stocking flag, add
256
+ `const TRANSFER_ORDER_DETECTION_MODE = App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__STOCKING_FLAG;`
257
+ before its `require_once 'common_sync_togasupply.php';` — and confirm that client actually sets
258
+ `custbody_stocking_order` in NetSuite. Omitting the mode no longer lands everything in
259
+ SalesOrders; it **throws** (deliberate — see the shared engine doc).
260
+ - **The `custbody_stock_adjustment` inventory-adjustment link is on its OWN flag now.**
261
+ `attachInventoryAdjustmentToItemFulfillment()` used to piggyback on the old routing boolean; it is
262
+ now gated by `IS_ENABLED_INVENTORY_ADJUSTMENT_FULFILLMENT_LINK` (NYCHH-only) — see
263
+ [NYCHH inventory-adjustment → item-fulfillment linking](./netsuite-inventory-adjustment-fulfillment-link.md).
253
264
  - **`custbody_stocking_order` must reach the shim.** `fetchSalesOrderById()` now carries the field
254
265
  via `customFieldList`; a missing custom field read returns null → the order routes as a transfer
255
266
  order (unchecked = transfer). Verify the field is populated in NetSuite before trusting the split.
256
267
 
257
268
  ## Change history
258
269
 
270
+ - 2026-09-03 — **`IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING` removed.** NYCHH now declares
271
+ `const TRANSFER_ORDER_DETECTION_MODE = App_Api_Toga2::TRANSFER_ORDER_DETECTION_MODE__STOCKING_FLAG;`
272
+ in `sync_togasupply_hh.php`; `isTransferOrder()` throws on an undefined/unknown mode instead of
273
+ silently falling back to GroWrk's `$0 + hold` rule. The `custbody_stock_adjustment`
274
+ inventory-adjustment → item-fulfillment link, which the old boolean was also guarding, moved to its
275
+ own NYCHH-only `IS_ENABLED_INVENTORY_ADJUSTMENT_FULFILLMENT_LINK`. **library and worker must deploy
276
+ together.** (rgirish)
259
277
  - 2026-09-02 — Recorded **why** the importer can never resolve an origin from the NetSuite header:
260
278
  on a NYCHH transfer order the **destination** location is the NetSuite customer and the **origin**
261
279
  is the NetSuite warehouse, so `entity` is always the destination (verified in prod across all 1,930
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.767",
3
+ "version": "1.0.768",
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",