toga-ai 1.0.671 → 1.0.673
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/features/netsuite-suiteql-rest-shim.md +26 -0
- package/knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md +11 -0
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +53 -0
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +35 -0
- package/knowledge/2.0/apps/api2/features/v2-api-error-codes.md +17 -0
- package/knowledge/2.0/apps/saml/architecture.md +10 -1
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -1
- package/knowledge/2.0/apps/worker2/features/alb-target-group-auto-registration.md +64 -12
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/nychh/INDEX.md +3 -1
- package/knowledge/clients/nychh/features/netsuite-transfer-order-import.md +143 -0
- package/knowledge/clients/nychh/features/transfer-order-inventory-quantities.md +116 -0
- package/knowledge/clients/nychh/profile.md +13 -1
- package/package.json +1 -1
|
@@ -92,6 +92,20 @@ Forecast2 tables already store.
|
|
|
92
92
|
- **TO_DATE rejects impossible dates** (e.g. `2026-06-31`) with an opaque HTTP 400 — validate
|
|
93
93
|
calendar dates before building SuiteQL.
|
|
94
94
|
|
|
95
|
+
- **The line SuiteQL must SELECT the line location or it is always NULL — the shim reads it but
|
|
96
|
+
didn't select it.** `listSalesOrders()`'s line query now selects `tl.location` and
|
|
97
|
+
`BUILTIN.DF(tl.location) AS location_name`. The line-shim assembly code already *read* those two
|
|
98
|
+
fields, but the SELECT never included them, so every line's location came back null — which broke
|
|
99
|
+
NYCHH transfer-order origin resolution (`TransferOrders.originLocationId` is NOT NULL → V2 `EV-10`).
|
|
100
|
+
Same class of bug as ShipItem cost above: a field the shim consumes must actually be in the SELECT.
|
|
101
|
+
(Added 2026-08-27.)
|
|
102
|
+
|
|
103
|
+
- **Custom body fields ride in `customFieldList` on the per-id REST GET.** `fetchSalesOrderById()`
|
|
104
|
+
now carries `custbody_stocking_order` (NetSuite internal id 7097) via `customFieldList` so the
|
|
105
|
+
importer can route NYCHH orders (checked = stocking/in → SalesOrders, unchecked = transfer/out →
|
|
106
|
+
TransferOrders). A custom field absent from the record reads as null downstream. See
|
|
107
|
+
[NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
|
|
108
|
+
|
|
95
109
|
## Transaction status sourcing (1.0) — and the 2026.2 REST `status.id` change
|
|
96
110
|
|
|
97
111
|
1.0 reaches NetSuite over **two transports, and neither reads status off a REST record GET
|
|
@@ -136,9 +150,21 @@ None — uniform across clients (NetSuite is a single shared account).
|
|
|
136
150
|
`??=`, or `match`. Lint with `C:\xampp7\php\php.exe -l` before deploying.
|
|
137
151
|
- Field availability varies by NetSuite account **and** record type — probe the live account
|
|
138
152
|
before assuming a column/relationship exists.
|
|
153
|
+
- **`App_NetSuite::getCustomFieldValue()` (`library/app/netsuite.php`) must be warning-safe.** 1.0
|
|
154
|
+
runs on PHP 7.2 in an unguarded path, so any PHP warning → `exit()` (kills the cron). It was
|
|
155
|
+
hardened 2026-08-27 with `?? []` / `?? null` guards on the custom-field-list traversal. Keep that
|
|
156
|
+
discipline when touching it.
|
|
139
157
|
|
|
140
158
|
## Change history
|
|
141
159
|
|
|
160
|
+
- 2026-08-27 — Recorded three field-semantics facts from NYCHH transfer-order support:
|
|
161
|
+
`listSalesOrders()` line SuiteQL now **selects `tl.location` + `BUILTIN.DF(tl.location)`** (the line
|
|
162
|
+
shim read them but never selected them → always null, breaking TO origin resolution / V2 `EV-10`);
|
|
163
|
+
`fetchSalesOrderById()` now carries **`custbody_stocking_order` (id 7097)** via `customFieldList` for
|
|
164
|
+
order routing; and `App_NetSuite::getCustomFieldValue()` was hardened with `?? []`/`?? null` for
|
|
165
|
+
PHP 7.2 warning-safety. See
|
|
166
|
+
[NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
|
|
167
|
+
(jcardinal)
|
|
142
168
|
- 2026-08-25 — Documented **1.0 transaction-status sourcing across both transports** and the
|
|
143
169
|
conclusion that the **NetSuite 2026.2 REST `status.id` text→letter standardization does not
|
|
144
170
|
affect 1.0** (SuiteAnswers 89313). `App_Api_Netsuite_Rest` derives status from the SuiteQL
|
|
@@ -396,6 +396,17 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
396
396
|
|
|
397
397
|
## Change history
|
|
398
398
|
|
|
399
|
+
- 2026-08-27 — `send()`'s throw sites were **reverted to plain `throw new Exception(...)`** (both
|
|
400
|
+
sites) as part of removing the sync engine's swallow-and-skip data-loss bug — the importer must
|
|
401
|
+
propagate a failed write, not catch it (see
|
|
402
|
+
[NetSuite → TOGa Supply per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md),
|
|
403
|
+
2026-08-27). New transfer-order importer methods landed in this class for NYCHH:
|
|
404
|
+
`isTransferOrder()` rewritten + private `isStockingOrder()` (route by NetSuite
|
|
405
|
+
`custbody_stocking_order`), private `linkPurchaseOrderToTransferOrder()`, TO origin resolution in
|
|
406
|
+
`syncSalesOrderFromNetsuite`, `transferOrderItem.uuid` linking in `syncItemFulfillmentFromNetsuite`,
|
|
407
|
+
and an expanded `syncTransferOrderFromNetsuite` status map — all documented on
|
|
408
|
+
[NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
|
|
409
|
+
(jcardinal)
|
|
399
410
|
- 2026-08-17 — Recorded that **depth-4 `GET /sales-orders` omits the `itemFulfillmentItems` and
|
|
400
411
|
`invoiceItems` reverse-hasMany collections** (it does return `salesOrderItems` and the PO-link
|
|
401
412
|
bridges), verified against prod `Logs_Compass.Api` — so code guarding on those collections from
|
|
@@ -120,6 +120,39 @@ client's `Parameters` table back before the lost window. Re-import is **idempote
|
|
|
120
120
|
on `c_netsuiteInternal*Id` and `SalesOrders.c_netsuiteInternalSalesOrderId` carries a unique index —
|
|
121
121
|
so a rewind re-PUTs what is already there and POSTs only what is missing.
|
|
122
122
|
|
|
123
|
+
### ⚠ 2026-08-27: all per-order `try/catch` REMOVED — every section now fails LOUD (was a data-loss bug)
|
|
124
|
+
|
|
125
|
+
The "section-specific isolation" described just above was a **transient, harmful state** and no
|
|
126
|
+
longer exists. On 2026-08-20 a prior dev (Rohan) had wrapped **each order dispatch** in all six
|
|
127
|
+
sections in `try { … } catch (Throwable) { error_log }`, and each section in `try { … } finally {
|
|
128
|
+
finishModeIteration }`. The effect was a **silent data-loss bug**: any non-2XX write was swallowed,
|
|
129
|
+
the record was dropped, **the watermark/checkpoint still advanced past it** (permanent loss), and the
|
|
130
|
+
execution mode was reset to **IDLE** on failure — so the next run had no idea the prior run had
|
|
131
|
+
failed.
|
|
132
|
+
|
|
133
|
+
**Decision + fix (this session), the standing behavior now:**
|
|
134
|
+
|
|
135
|
+
- **All 6 per-order `catch (Throwable)` skips removed** (SALES_ORDERS, PURCHASE_ORDERS, INVOICES,
|
|
136
|
+
ITEM_RECEIPTS, ITEM_FULFILLMENTS, INVENTORY_ADJUSTMENTS). A failed dispatch now **propagates**.
|
|
137
|
+
- **All 6 section-level `try/finally` removed.** `finishModeIteration()` now runs on the **success
|
|
138
|
+
path only**. On failure the cron **aborts**, the watermark PUT is **skipped**, and the execution
|
|
139
|
+
mode is **left as RUNNING** so the next run knows the prior run failed (and the ÷3 window shrink
|
|
140
|
+
fires, as intended).
|
|
141
|
+
- `startModeIteration`'s internal try/catch is **kept** (it re-throws, does not skip).
|
|
142
|
+
- The `send()` throw sites in `library/app/api/toga2.php` were reverted to plain
|
|
143
|
+
`throw new Exception(...)`.
|
|
144
|
+
|
|
145
|
+
**REJECTED alternative (do not re-attempt):** converting the throws into a custom
|
|
146
|
+
`App_Exception_ApiResponse` and catching *that*. The real fix is to **stop catching `Throwable`**, not
|
|
147
|
+
to introduce a catchable wrapper — the `app/exception/apiresponse.php` class created for that approach
|
|
148
|
+
was deleted.
|
|
149
|
+
|
|
150
|
+
**Net:** a failed import halts loudly and preserves `RUNNING` state instead of advancing past dropped
|
|
151
|
+
records. The isolation discussion above now applies uniformly — **no** section steps over a bad
|
|
152
|
+
record; every section behaves the way SALES_ORDERS always did (throw → freeze at RUNNING → same
|
|
153
|
+
poison record re-tried each run → go read the error). This is an application of the universal
|
|
154
|
+
coding-style rule *never catch-and-silently-ignore*, not a new engine-specific policy.
|
|
155
|
+
|
|
123
156
|
### Sections are NOT order-dependent — one cursor can be rewound alone
|
|
124
157
|
|
|
125
158
|
Each section's `startModeIteration()` reads only **its own** `Parameters` key, and the PO section
|
|
@@ -273,6 +306,14 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
273
306
|
|
|
274
307
|
## Client variations
|
|
275
308
|
|
|
309
|
+
- **Transfer-order routing is opt-in per wrapper (default OFF).** A client whose NetSuite
|
|
310
|
+
*transfer orders* arrive as NetSuite *sales orders* can have the importer split them into
|
|
311
|
+
`TransferOrders` vs `SalesOrders` by setting
|
|
312
|
+
`const IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING = true;` before the `require_once`
|
|
313
|
+
(NYCHH's `sync_togasupply_hh.php` is the only one that does). The signal is the NetSuite custom
|
|
314
|
+
checkbox `custbody_stocking_order`, read by `App_Api_Toga2::isTransferOrder()`/`isStockingOrder()`
|
|
315
|
+
— **not** a dollar/hold heuristic. Full mechanics, PO bridges, and origin-location resolution:
|
|
316
|
+
[NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
|
|
276
317
|
- `isParentCustomer` — `true` walks NetSuite child customers (`listChildCustomers`); `false`
|
|
277
318
|
syncs the single customer. Prudential-EDI and the live Quad wrapper use `false`.
|
|
278
319
|
- The original combined-array entries used `App_Api_Toga2::CLIENT_UUID_*` consts for Quad and
|
|
@@ -465,6 +506,18 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
465
506
|
|
|
466
507
|
## Change history
|
|
467
508
|
|
|
509
|
+
- 2026-08-27 — **Removed a silent data-loss bug: all per-order `catch (Throwable)` skips and all
|
|
510
|
+
section-level `try/finally` were deleted, so every section now fails LOUD.** A 2026-08-20 change had
|
|
511
|
+
wrapped each order dispatch in `catch (Throwable) { error_log }` and each section in
|
|
512
|
+
`try/finally { finishModeIteration }` — swallowing any non-2XX write, dropping the record, advancing
|
|
513
|
+
the watermark past it, and resetting mode to IDLE. Now dispatch propagates, `finishModeIteration()`
|
|
514
|
+
runs on the success path only, and a failure aborts with the watermark skipped and mode left
|
|
515
|
+
RUNNING; `send()`'s throw sites in `library/app/api/toga2.php` reverted to plain `throw`. The
|
|
516
|
+
rejected `App_Exception_ApiResponse` wrapper was deleted. The "isolation is section-specific" gotcha
|
|
517
|
+
now applies uniformly — no section steps over a bad record. Also recorded the new opt-in
|
|
518
|
+
transfer-order routing switch (`IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING`, default off,
|
|
519
|
+
NYCHH only) — see [NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
|
|
520
|
+
(jcardinal)
|
|
468
521
|
- 2026-08-26 — Corrected the IF full-reconcile tracking claim and **fixed two bugs** in
|
|
469
522
|
`syncItemFulfillmentFromNetsuite`'s single-tracking-number branch: **318 (item) was never written**
|
|
470
523
|
(`Client_Quad` had 0 rows → blank Tracking # column) and **319 (unit) received only the last
|
|
@@ -222,6 +222,32 @@ then checks the caller's roles against `AclRecordPermissions` (→ `EZ-1` if no
|
|
|
222
222
|
readable/writable fields from `AclFieldPermissions`. `_Model_Core_Page::meta()` reads the same
|
|
223
223
|
tables to return per-page ACL to the frontend.
|
|
224
224
|
|
|
225
|
+
> **⚠ There is NO ACL/metadata caching layer — the load is per-request and there is nothing to bust.**
|
|
226
|
+
> `buildLookups()` re-reads the ACL/metadata tables on **every** request, so an inserted/updated
|
|
227
|
+
> `AclCustomFieldPermissions` / `CustomRecordFields` / `AclFieldPermissions` row takes effect on the
|
|
228
|
+
> **very next request**. There is no cache-invalidation step, and looking for one is a dead end
|
|
229
|
+
> (verified 2026-08-27 wiring NYCHH transfer-order custom fields — a whole debugging pass was wasted
|
|
230
|
+
> on the assumption that a cache had to be cleared). If a freshly-applied grant/metadata row still
|
|
231
|
+
> doesn't take, suspect the row itself (wrong DB/role/id, or a broken cross-schema subselect that
|
|
232
|
+
> inserted zero rows), not staleness.
|
|
233
|
+
|
|
234
|
+
### ⚠ A nested FK resolved BY a custom field needs `CustomRecordFields.isIdentifier = 1`
|
|
235
|
+
|
|
236
|
+
When the API resolves a nested foreign-key reference **through a custom (`c_`) field** — i.e. the
|
|
237
|
+
custom field is the identifier the child is matched on — that field's `CustomRecordFields.isIdentifier`
|
|
238
|
+
must be `1`, or the write fails **`EV-12`** with `searchableIdentifierFields:[id]` (the resolver falls
|
|
239
|
+
back to `id` because no custom field is marked searchable). This is a **fourth** thing to check beyond
|
|
240
|
+
column + `CustomRecordFields` row + `AclCustomFieldPermissions` grant + model declaration. Verified
|
|
241
|
+
2026-08-27 on NYCHH `c_netsuiteInternalTransferOrderStatus` (recordId 351): the field existed and was
|
|
242
|
+
granted, but the nested transfer-order-stage lookup 400'd until `isIdentifier` was set to 1.
|
|
243
|
+
|
|
244
|
+
**The `c_` declaration on a client-override model comes from its NetSuite trait.** The pattern for a
|
|
245
|
+
NetSuite-synced record is `_Model_<Client>_X extends _Model_Client_X { use _Trait_Netsuite_X; }` — the
|
|
246
|
+
trait is what declares the `c_` fields (so a client with only `extends`, no `use`, is missing them and
|
|
247
|
+
its POST 500s `EO-1`). NYCHH's `_Model_Nychh_TransferOrder` / `_Model_Nychh_TransferOrderStage` had to
|
|
248
|
+
be **created** for this reason before their transfer orders could carry NetSuite ids — see
|
|
249
|
+
[NYCHH transfer-order inventory & quantities](../../../clients/nychh/features/transfer-order-inventory-quantities.md).
|
|
250
|
+
|
|
225
251
|
## Navigation / action flags (`Core.AclActions` + `AclActionPermissions`) — a THIRD gate
|
|
226
252
|
|
|
227
253
|
A UI control that is not a record CRUD operation and not a scripted API — a nav item, an "Add" or
|
|
@@ -410,6 +436,15 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
|
|
|
410
436
|
and every repo is on the **same branch** so the generated model matches the DB.
|
|
411
437
|
|
|
412
438
|
## Change history
|
|
439
|
+
- 2026-08-27 — Recorded two facts from wiring NYCHH transfer-order custom fields: **there is NO
|
|
440
|
+
ACL/metadata cache** (`buildLookups()` re-reads per request, so a new grant/metadata row applies on
|
|
441
|
+
the next request — no cache-bust exists; a whole debugging pass was wasted assuming one did), and a
|
|
442
|
+
**nested FK resolved by a custom field needs `CustomRecordFields.isIdentifier = 1`** or the write
|
|
443
|
+
400s `EV-12 searchableIdentifierFields:[id]` (NYCHH `c_netsuiteInternalTransferOrderStatus`, recordId
|
|
444
|
+
351). Also noted that a client-override model's `c_` declarations come from its `_Trait_Netsuite_X`
|
|
445
|
+
(create `_Model_<Client>_X { use _Trait_Netsuite_X; }` or the POST 500s `EO-1`). See
|
|
446
|
+
[NYCHH transfer-order inventory & quantities](../../../clients/nychh/features/transfer-order-inventory-quantities.md).
|
|
447
|
+
(jcardinal)
|
|
413
448
|
- 2026-08-26 - Added the **verify-per-sibling-field, verify-as-the-least-privileged-role** rule:
|
|
414
449
|
columns of the same table often carry different role sets, so adding one more field to a `fields=`
|
|
415
450
|
list is a permission change. `Client_Quad.Currencies` grants `code` (1424) and `symbol` (1425) to
|
|
@@ -55,6 +55,8 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
|
|
|
55
55
|
| **EV-5** | Duplicate `transactionId` | The globally-unique `transactionId` was reused | Send a fresh unique `transactionId` per request |
|
|
56
56
|
| **EV-13** | Validation — page size too large | `recordsPerPage` exceeds the **10000** max | Lower `recordsPerPage` and paginate with `page` |
|
|
57
57
|
| **EV-12** | Record's parent not synced | (Fulfill & Ship) POST `/item-fulfillments` when the Sales Order isn't synced into Toga yet | `GET /sales-orders/syncNetsuiteSalesOrder?netsuiteInternalSalesOrderId=<id>` first |
|
|
58
|
+
| **EV-12** | `invalidSearchableField` / `searchableIdentifierFields:[id]` on a nested write | A nested FK is being resolved **through a custom (`c_`) field**, but that field is not marked searchable, so the resolver has only `id` | Set **`CustomRecordFields.isIdentifier = 1`** on that `c_` field (NYCHH `c_netsuiteInternalTransferOrderStatus`, recordId 351). Also confirm the field is declared on the client model via its `_Trait_Netsuite_*` — see the [ACL chain](../../_underscore/features/acl-permission-chain.md) |
|
|
59
|
+
| **EV-10** | NOT NULL column received NULL on insert (e.g. `originLocationId cannot be null`) | Either the payload never resolved a value for a NOT NULL column, **or** a NOT NULL column that is **not a writable V2 field** was expected to arrive in the payload | Resolve the value upstream (NYCHH `TransferOrders.originLocationId`: select `tl.location` in the SuiteQL + fall back to the first line's location), **or** default the column in the ORM if it is server-owned — see EV-8 below and diagnosis note 15 |
|
|
58
60
|
|
|
59
61
|
## How to diagnose
|
|
60
62
|
|
|
@@ -197,6 +199,15 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
|
|
|
197
199
|
not on matching the response text — see
|
|
198
200
|
[V2 deadlock-retry](./v2-deadlock-retry.md).
|
|
199
201
|
|
|
202
|
+
15. **A NOT NULL column that is NOT a writable V2 field must be DEFAULTED in the ORM — never sent in
|
|
203
|
+
the payload.** Adding the key to the POST body throws **`EV-8`** ("field does not exist") because
|
|
204
|
+
the column has no writable-field registration; but leaving it out throws the DB NOT NULL error on
|
|
205
|
+
insert. The fix is neither — declare a model default so the ORM writes it without a payload key,
|
|
206
|
+
e.g. `public $isActive = [self::FIELD_BOOLEAN, self::FIELDOPT_DEFAULT => true];` on
|
|
207
|
+
`_Model_Client_TransferOrder` (2026-08-27, NYCHH transfer-order POST). Contrast with a NOT NULL
|
|
208
|
+
column that *should* come from the caller/upstream (that is `EV-10` — resolve the value, don't
|
|
209
|
+
default it).
|
|
210
|
+
|
|
200
211
|
## BREAKING (2026-07-30): `error` is now an object, not a bare string
|
|
201
212
|
|
|
202
213
|
The V2 envelope's `error` changed from a bare string (`"EO-1"`) to an **object**:
|
|
@@ -281,6 +292,12 @@ Full mechanics:
|
|
|
281
292
|
- The response-envelope shape and code families: see [api2 architecture](../architecture.md).
|
|
282
293
|
|
|
283
294
|
## Change history
|
|
295
|
+
- 2026-08-27 — Added three codes/notes from the NYCHH transfer-order POST work: **EV-10** (a NOT NULL
|
|
296
|
+
column received NULL — `originLocationId cannot be null`; resolve upstream or default in the ORM), a
|
|
297
|
+
second **EV-12** flavor (`searchableIdentifierFields:[id]` — a nested FK resolved *through* a `c_`
|
|
298
|
+
field needs `CustomRecordFields.isIdentifier = 1`), and **diagnosis note 15**: a NOT NULL column
|
|
299
|
+
that is **not a writable V2 field** must be defaulted in the ORM (`FIELDOPT_DEFAULT`), never sent in
|
|
300
|
+
the payload — sending it is `EV-8`, omitting it is a DB NOT NULL error. (jcardinal)
|
|
284
301
|
- 2026-08-25 — Added the diagnostic that a **malformed bearer token returns EO-1 500 instead of
|
|
285
302
|
EN-3 401**: the header/payload `json_decode` yields `null` and `property_exists()` throws a
|
|
286
303
|
`TypeError` (`V2.php` ~1840); the sibling check at ~826 already guarded with `is_object()`.
|
|
@@ -6,7 +6,7 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: ["rgirish", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- saml/index.php
|
|
@@ -71,6 +71,14 @@ To onboard a new SSO client, add the class in `_underscore` — not in this repo
|
|
|
71
71
|
- **`_underscore` pulled at deploy time** via `.ebextensions/git.php` + prebuild hook — clones branch `_production`. Deploy `saml` after `_underscore` merges to pick up framework changes.
|
|
72
72
|
- **CodePipeline:** Source (GitHub `_production`) → ManualApproval → Deploy (EB).
|
|
73
73
|
- **Composer deps** installed at postdeploy via `015_install_composer.sh`.
|
|
74
|
+
- **Non-prod shared-ALB self-registration (2026-08-27).** Postdeploy hook
|
|
75
|
+
`060_register_instance_to_shared_application_load_balancer.sh` + `ebs/register_instance_to_shared_application_load_balancer.php`
|
|
76
|
+
register a non-production `saml` instance into the target group named exactly its EB env name;
|
|
77
|
+
**skips `production`** (so `saml-production` is unaffected) and always `exit 0`. Requires
|
|
78
|
+
`aws/aws-sdk-php` (now in `composer.json`, lock/vendor to be regenerated) and two IAM actions on
|
|
79
|
+
the instance profile (`elasticloadbalancing:DescribeTargetGroups` region-scoped +
|
|
80
|
+
`RegisterTargets` scoped to the non-prod target-group ARN). See
|
|
81
|
+
[ALB target-group auto-registration](../worker2/features/alb-target-group-auto-registration.md).
|
|
74
82
|
|
|
75
83
|
## Dependencies
|
|
76
84
|
|
|
@@ -88,3 +96,4 @@ To onboard a new SSO client, add the class in `_underscore` — not in this repo
|
|
|
88
96
|
## Change history
|
|
89
97
|
- 2026-06-11 — Added `transactionCommit()` before redirect; wrapped `getAuthenticatedSsoUser()` in try/catch with Sentry; echo+exit on auth failure (rgirish)
|
|
90
98
|
- 2026-06-25 — Sharpened signature-verification gotcha with explicit threat model and flagged it as the repo's top security priority; documented plaintext secrets in `production.ini` + hardcoded Sentry DSN in `Controller/Index.php` with SSM Parameter Store recommendation (jcardinal)
|
|
99
|
+
- 2026-08-27 — Documented non-prod shared-ALB self-registration postdeploy hook (`060_...sh` + `ebs/register_instance_to_shared_application_load_balancer.php`); skips `production`, requires `aws/aws-sdk-php` + two IAM actions; cross-linked worker2 ALB auto-registration feature (jcardinal)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Worker (worker2) Architecture](architecture.md) | Worker (repo `worker2`) is an AWS Elastic Beanstalk **Worker Tier** application that processes background jobs. | worker2/Controller/Index.php, worker2/Worker/, worker2/LambdaFunctions/, _underscore/Worker.php, worker2/composer.json |
|
|
6
|
-
| [Deploy-Time Auto-Registration to the Shared ALB Target Group (non-production)](features/alb-target-group-auto-registration.md) | TOGA does **not** pay for EB-managed load-balancer registration, so an EB instance is normally **not** added to its environment's ALB target group — a fresh or | worker2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, worker2/ebs/register_instance_to_shared_application_load_balancer.php, worker2/.platform/hooks/
|
|
6
|
+
| [Deploy-Time Auto-Registration to the Shared ALB Target Group (non-production)](features/alb-target-group-auto-registration.md) | TOGA does **not** pay for EB-managed load-balancer registration, so an EB instance is normally **not** added to its environment's ALB target group — a fresh or | worker2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, worker2/ebs/register_instance_to_shared_application_load_balancer.php, worker2/.platform/hooks/_shared/040-write-instance-id.sh, worker2/.platform/hooks/_shared/041-write-region.sh, worker2/.platform/hooks/_shared/042-write-eb-environment.sh, worker2/.platform/hooks/postdeploy/015_install_composer.sh, saml/.platform/hooks/_shared/040-write-instance-id.sh, saml/.platform/hooks/_shared/041-write-region.sh, saml/.platform/hooks/_shared/042-write-eb-environment.sh, saml/.platform/hooks/prestart/040-write-instance-id.sh, saml/.platform/hooks/prestart/041-write-region.sh, saml/.platform/hooks/postdeploy/040-write-instance-id.sh, saml/.platform/hooks/postdeploy/041-write-region.sh, saml/.platform/hooks/postdeploy/042-write-eb-environment.sh, saml/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, saml/ebs/register_instance_to_shared_application_load_balancer.php, saml/composer.json, api2/.platform/hooks/prebuild/_shared/042-write-eb-environment.sh, api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, api2/ebs/register_instance_to_shared_application_load_balancer.php |
|
|
7
7
|
| [All-Client Email Queue Monitor (Monitor/Operations/EmailQueue)](features/all-client-email-queue-monitor.md) | `_Worker_Monitor_Operations::EmailQueue()` is the cross-client health check for the 2.0 outbound email queue (`Logs_<Client>.Email` — see [2.0 Email Send Pipeli | worker2/Worker/Monitor/Operations.php, _underscore/Database.php, _underscore/Query.php, dbchanges2/Logs_Client/2026-05-21 - Email.sql |
|
|
8
8
|
| [Automated PR Merger — Concurrent Force-Push Clobber Race](features/automated-pr-merger-force-push-race.md) | The automated PR merger `_Worker_Team_GitHub::Merge` (`worker2` `Worker/Team/Github.php`) merges approved PRs to `_production` by **force-pushing from a clone t | Worker/Team/Github.php |
|
|
9
9
|
| [Callback Scheduling ("call me back") — worker2 AI-BDR](features/callback-scheduling.md) | The **AI-BDR callback path**: what happens between a prospect saying *"call me back later"* on a Vapi call and the dialer actually placing that second call. | worker2/Worker/Vapi.php, worker2/Worker/Ai/Bdr/Vapi.php, worker2/Controller/Index.php |
|
|
@@ -6,20 +6,32 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-27
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh
|
|
13
13
|
- worker2/ebs/register_instance_to_shared_application_load_balancer.php
|
|
14
|
-
- worker2/.platform/hooks/
|
|
15
|
-
- worker2/.platform/hooks/
|
|
16
|
-
- worker2/.platform/hooks/
|
|
17
|
-
- api2/.platform/hooks/prebuild/_shared/042-write-eb-environment.sh
|
|
14
|
+
- worker2/.platform/hooks/_shared/040-write-instance-id.sh
|
|
15
|
+
- worker2/.platform/hooks/_shared/041-write-region.sh
|
|
16
|
+
- worker2/.platform/hooks/_shared/042-write-eb-environment.sh
|
|
18
17
|
- worker2/.platform/hooks/postdeploy/015_install_composer.sh
|
|
18
|
+
- saml/.platform/hooks/_shared/040-write-instance-id.sh
|
|
19
|
+
- saml/.platform/hooks/_shared/041-write-region.sh
|
|
20
|
+
- saml/.platform/hooks/_shared/042-write-eb-environment.sh
|
|
21
|
+
- saml/.platform/hooks/prestart/040-write-instance-id.sh
|
|
22
|
+
- saml/.platform/hooks/prestart/041-write-region.sh
|
|
23
|
+
- saml/.platform/hooks/postdeploy/040-write-instance-id.sh
|
|
24
|
+
- saml/.platform/hooks/postdeploy/041-write-region.sh
|
|
25
|
+
- saml/.platform/hooks/postdeploy/042-write-eb-environment.sh
|
|
26
|
+
- saml/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh
|
|
27
|
+
- saml/ebs/register_instance_to_shared_application_load_balancer.php
|
|
28
|
+
- saml/composer.json
|
|
29
|
+
- api2/.platform/hooks/prebuild/_shared/042-write-eb-environment.sh
|
|
19
30
|
- api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh
|
|
20
31
|
- api2/ebs/register_instance_to_shared_application_load_balancer.php
|
|
21
32
|
related:
|
|
22
33
|
- ../architecture.md
|
|
34
|
+
- ../../saml/architecture.md
|
|
23
35
|
- ../../api2/workflows/codepipeline-codeconnections-deploy.md
|
|
24
36
|
- ../../api2/architecture.md
|
|
25
37
|
---
|
|
@@ -34,8 +46,10 @@ pair automates that step for **non-production** environments: on every deploy th
|
|
|
34
46
|
registers **itself** into the target group whose **name exactly equals the EB environment name**.
|
|
35
47
|
|
|
36
48
|
Originally built in `api2`; ported to `worker2` on 2026-07-28 with three security hardenings
|
|
37
|
-
(instance-profile credentials, native-PHP IMDSv2, fail-closed token handling)
|
|
38
|
-
|
|
49
|
+
(instance-profile credentials, native-PHP IMDSv2, fail-closed token handling), and to `saml`
|
|
50
|
+
(SAML SSO Gateway) on 2026-08-27 as a byte-faithful copy of the hardened `worker2` variant.
|
|
51
|
+
**`worker2` is now the reference implementation — port from it, not from `api2`, whose copy has
|
|
52
|
+
known defects (see below).**
|
|
39
53
|
|
|
40
54
|
Non-prod `worker2` environments need this because they are reached **over HTTP for manual job
|
|
41
55
|
invocation**, not through SQS.
|
|
@@ -50,8 +64,8 @@ invocation**, not through SQS.
|
|
|
50
64
|
2. **`ebs/register_instance_to_shared_application_load_balancer.php`**
|
|
51
65
|
- Reads the instance id and region from
|
|
52
66
|
`/var/app/current/storage/instance-id.txt` and `storage/region.txt` (written earlier by the
|
|
53
|
-
`_shared/040-write-instance-id.sh` / `041-write-region.sh`
|
|
54
|
-
direct **IMDSv2** lookup.
|
|
67
|
+
`_shared/040-write-instance-id.sh` / `041-write-region.sh` hooks — see the layout note
|
|
68
|
+
below), falling back to a direct **IMDSv2** lookup.
|
|
55
69
|
- Reads the EB environment name via `get-config container -k environment_name`.
|
|
56
70
|
- Uses the AWS SDK (`aws/aws-sdk-php`) ELBv2 client to `DescribeTargetGroups` and selects the
|
|
57
71
|
target group whose **`TargetGroupName` is an exact string match** for the environment name —
|
|
@@ -64,13 +78,23 @@ invocation**, not through SQS.
|
|
|
64
78
|
|
|
65
79
|
| Hook | Provides |
|
|
66
80
|
|---|---|
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
81
|
+
| `_shared/040-write-instance-id.sh` (run via a phase wrapper) | `storage/instance-id.txt` |
|
|
82
|
+
| `_shared/041-write-region.sh` (run via a phase wrapper) | `storage/region.txt` |
|
|
83
|
+
| `_shared/042-write-eb-environment.sh` (run via a phase wrapper) | `storage/eb-environment.txt` |
|
|
70
84
|
| `postdeploy/015_install_composer.sh` | `vendor/` (the AWS SDK) |
|
|
71
85
|
|
|
72
86
|
Renumber it below `015` and the SDK autoloader does not exist yet.
|
|
73
87
|
|
|
88
|
+
### Real on-disk hook layout — `_shared` + thin phase wrappers (corrected 2026-08-27)
|
|
89
|
+
|
|
90
|
+
The canonical `040`/`041`/`042` scripts live at **`.platform/hooks/_shared/0xx.sh`** — **not**
|
|
91
|
+
under `prebuild/`, as earlier revisions of this doc stated. Each deploy phase that needs one
|
|
92
|
+
carries a **thin wrapper** in its own hook dir (`.platform/hooks/prestart/0xx.sh`,
|
|
93
|
+
`.platform/hooks/postdeploy/0xx.sh`) that just runs `bash ../_shared/0xx.sh`. In the `worker2`
|
|
94
|
+
and `saml` trees, `040`/`041` have both a `prestart` and a `postdeploy` wrapper, while `042` has
|
|
95
|
+
a `postdeploy` wrapper only (no `prestart` copy). When porting, copy the canonical `_shared`
|
|
96
|
+
script **and** the phase wrappers the tier needs — the wrappers are what actually execute.
|
|
97
|
+
|
|
74
98
|
### `042-write-eb-environment.sh` — the EB environment name, cached to disk (2026-08-04)
|
|
75
99
|
|
|
76
100
|
Added to the same `_shared` hook family (plus a postdeploy hook) in **both `api2` and `worker2`**,
|
|
@@ -95,6 +119,21 @@ an identical `015_install_composer.sh`; and the same `get-config environment -k
|
|
|
95
119
|
convention already used by `.ebextensions/git.php`. Check these four before porting the pair to
|
|
96
120
|
any other 2.0 tier.
|
|
97
121
|
|
|
122
|
+
**`saml` port (2026-08-27) — the two prerequisites a bare tier is missing.** `saml` had *none*
|
|
123
|
+
of the hook family (prebuild was `git.sh` only, postdeploy was `015_install_composer.sh` only,
|
|
124
|
+
no `ebs/` dir), so the port copied the canonical `_shared/040/041/042` scripts, their phase
|
|
125
|
+
wrappers, `060_….sh`, and the hardened `ebs/register_instance_to_shared_application_load_balancer.php`.
|
|
126
|
+
The two prerequisites it did **not** already meet:
|
|
127
|
+
|
|
128
|
+
- **`aws/aws-sdk-php` was absent** from `composer.json` — added `aws/aws-sdk-php: ^3.337`
|
|
129
|
+
(the SDK's ELBv2 client is what the script uses). **`composer.lock` + `vendor/` must be
|
|
130
|
+
regenerated** with `composer require aws/aws-sdk-php:^3.337` before deploy (a developer step;
|
|
131
|
+
not done in the porting session).
|
|
132
|
+
- **Stale `config.platform.php` pin.** `saml` pinned `7.2.33`, but the EB box runs PHP 8.5 (AL2023
|
|
133
|
+
/ platform 4.13.1) and `aws-sdk-php ^3.337` needs PHP ≥ 8.1, so the stale pin would block
|
|
134
|
+
dependency resolution. Corrected to `8.1.0`. **Check the platform pin on any tier before
|
|
135
|
+
porting** — a low pin silently blocks the SDK install.
|
|
136
|
+
|
|
98
137
|
## Credentials — instance profile only (decision, 2026-07-28)
|
|
99
138
|
|
|
100
139
|
**Deploy-time AWS credentials come from the EC2 instance profile. Never from source, never from
|
|
@@ -141,6 +180,12 @@ implementation instead:
|
|
|
141
180
|
group name first when a non-prod env is unreachable after a deploy.
|
|
142
181
|
- **Exit 0 hides failures.** Registration problems will not show in deploy status — read
|
|
143
182
|
`/var/log/eb-hooks.log` on the instance.
|
|
183
|
+
- **`saml` does nothing until it has a non-prod env + matching target group.** The team KB lists
|
|
184
|
+
`saml`'s only environment as `saml-production`, which this hook **self-skips**. The `saml` port
|
|
185
|
+
registers a target only on a **non-prod** `saml` env, and only if (a) that env's EC2 instance
|
|
186
|
+
profile carries the two IAM actions below (Describe scoped by `aws:RequestedRegion`, Register
|
|
187
|
+
scoped to the **non-prod** target-group ARN — never `targetgroup/*/*`), and (b) a target group
|
|
188
|
+
exists named **exactly** the non-prod env name. Naming equality is the whole contract.
|
|
144
189
|
- **The hook turns on reachability; it does not secure it.** The target group and listener already
|
|
145
190
|
exist by convention, but because this hook is what actually puts non-prod instances behind the
|
|
146
191
|
shared ALB, the **listener rules and security groups must restrict non-prod to internal/VPN
|
|
@@ -172,6 +217,13 @@ which lists the other hooks but not `060` — pending an elevated-doc update.
|
|
|
172
217
|
|
|
173
218
|
## Change history
|
|
174
219
|
|
|
220
|
+
- 2026-08-27 — Ported the hardened `worker2` hook pair to **`saml`** (SAML SSO Gateway): copied
|
|
221
|
+
`_shared/040/041/042`, their prestart/postdeploy wrappers, `060_….sh`, and the instance-profile
|
|
222
|
+
`ebs/register_instance_to_shared_application_load_balancer.php`. Added `aws/aws-sdk-php: ^3.337`
|
|
223
|
+
to `saml/composer.json` and corrected its stale `config.platform.php` pin `7.2.33 → 8.1.0`
|
|
224
|
+
(lock/vendor still to be regenerated by a developer). Also **corrected this doc's file paths**:
|
|
225
|
+
the canonical `040/041/042` scripts live at `.platform/hooks/_shared/`, not `prebuild/_shared/`,
|
|
226
|
+
and run via thin per-phase wrappers. (jcardinal)
|
|
175
227
|
- 2026-08-04 — Added `_shared/042-write-eb-environment.sh` (+ a postdeploy hook) to **api2 and
|
|
176
228
|
worker2**, caching the EB environment name to disk from `get-config` → `$EB_ENVIRONMENT_NAME` →
|
|
177
229
|
the IMDS tag. Read from a file rather than shelled out to at use time because its first consumer
|
package/knowledge/INDEX.md
CHANGED
|
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
4
4
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
|
-
- **library** (Library) _(framework core)_ —
|
|
7
|
+
- **library** (Library) _(framework core)_ — 20 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
8
|
- **worker** (Worker) — 28 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)
|
|
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
18
18
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
+
- **_underscore** (_Underscore) _(framework core)_ — 70 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 57 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 13 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
+
| [NYCHH NetSuite → TransferOrders import (stocking-flag routing, PO bridges, origin location)](features/netsuite-transfer-order-import.md) | 1.0 | NYCHH's NetSuite **transfer orders are represented as NetSuite sales orders** (same as the GroWrk pattern), so the shared NetSuite → TOGa Supply importer (`App_ | library/app/api/toga2.php, library/app/api/netsuite/rest.php, library/app/netsuite.php, worker/crons/toga2/netsuite/sync_togasupply_hh.php, worker/crons/toga2/netsuite/common_sync_togasupply.php |
|
|
5
6
|
| [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
6
|
-
| [
|
|
7
|
+
| [NYCHH transfer-order 2.0 model — inventory quantities + V2 field enablement](features/transfer-order-inventory-quantities.md) | 2.0 | The 2.0 (`_underscore`) side of NYCHH transfer-order support: a **two-branch inventory quantity model** on the NYCHH `Item` override, plus the **client-override | _underscore/Model/Nychh/Item.php, _underscore/Model/Nychh/TransferOrder.php, _underscore/Model/Nychh/TransferOrderStage.php, _underscore/Model/Client/TransferOrder.php, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql |
|
|
8
|
+
| [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql |
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "NYCHH NetSuite → TransferOrders import (stocking-flag routing, PO bridges, origin location)"
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: library
|
|
5
|
+
project: Library
|
|
6
|
+
client: nychh
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-27
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- library/app/api/toga2.php
|
|
13
|
+
- library/app/api/netsuite/rest.php
|
|
14
|
+
- library/app/netsuite.php
|
|
15
|
+
- worker/crons/toga2/netsuite/sync_togasupply_hh.php
|
|
16
|
+
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
17
|
+
related:
|
|
18
|
+
- ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
19
|
+
- ../../../1.0/apps/library/features/toga2-api-client-and-bridge.md
|
|
20
|
+
- ../../../1.0/apps/library/features/netsuite-suiteql-rest-shim.md
|
|
21
|
+
- ../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
|
|
22
|
+
- ../../growrk/features/transfer-order-flow.md
|
|
23
|
+
- ./transfer-order-inventory-quantities.md
|
|
24
|
+
- ../profile.md
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Summary
|
|
28
|
+
|
|
29
|
+
NYCHH's NetSuite **transfer orders are represented as NetSuite sales orders** (same as the GroWrk
|
|
30
|
+
pattern), so the shared NetSuite → TOGa Supply importer (`App_Api_Toga2::sync*FromNetsuite`) must
|
|
31
|
+
decide, per order, whether to write it to `Client_Nychh.SalesOrders` or `Client_Nychh.TransferOrders`.
|
|
32
|
+
This doc covers the **1.0-side ingestion**: the routing signal, the PO↔order bridge links, transfer-order
|
|
33
|
+
origin/destination location resolution, the item-fulfillment transfer-order line link, and the
|
|
34
|
+
transfer-order status map. The **2.0-side** (quantity model + the model/ACL work that lets the
|
|
35
|
+
TransferOrder POST succeed) is in
|
|
36
|
+
[NYCHH transfer-order inventory & quantities](./transfer-order-inventory-quantities.md).
|
|
37
|
+
|
|
38
|
+
**End-to-end verified this session:** a transfer order imported into `Client_Nychh.TransferOrders`
|
|
39
|
+
after deploy. One open edge case remains (order 128829, below).
|
|
40
|
+
|
|
41
|
+
## Routing signal — the NetSuite stocking flag, NOT the dollar amount
|
|
42
|
+
|
|
43
|
+
Route by the custom NetSuite checkbox **`custbody_stocking_order`** (internal id **7097**), not by
|
|
44
|
+
total/hold:
|
|
45
|
+
|
|
46
|
+
- **CHECKED** = a *stocking order* (how stock comes **IN**) → `Client_Nychh.SalesOrders`.
|
|
47
|
+
- **UNCHECKED** = a *transfer order* (how stock goes **OUT**) → `Client_Nychh.TransferOrders`.
|
|
48
|
+
|
|
49
|
+
`App_Api_Toga2::isTransferOrder()` was rewritten to read this flag through a new private
|
|
50
|
+
`isStockingOrder()` that normalizes NetSuite's `'T'`/`'F'` / bool / int representations. A new
|
|
51
|
+
private `linkPurchaseOrderToTransferOrder()` mirrors the existing sales-order PO linker (below).
|
|
52
|
+
|
|
53
|
+
> **⚠ The old heuristic — `total === 0` + `custbody_asi_hold` / `holdInvoice` — was WRONG for NYCHH
|
|
54
|
+
> and is gone.** NYCHH does **not** set the hold flag, so under the old rule every order matched as
|
|
55
|
+
> a sales order and `TransferOrders` stayed empty. The dollar amount is **ignored entirely** now.
|
|
56
|
+
> (GroWrk's own `holdInvoice + total===0` signal — see
|
|
57
|
+
> [GroWrk transfer order flow](../../growrk/features/transfer-order-flow.md) — is client-specific and
|
|
58
|
+
> not universal; the TO-detection signal must be read per client.)
|
|
59
|
+
|
|
60
|
+
**Routing is opt-in per client, default OFF.** The shared engine only routes to TransferOrders when
|
|
61
|
+
the wrapper enables it. NYCHH's wrapper sets
|
|
62
|
+
`const IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING = true;` in
|
|
63
|
+
`worker/crons/toga2/netsuite/sync_togasupply_hh.php`; every other client inherits the default (off)
|
|
64
|
+
and behaves exactly as before.
|
|
65
|
+
|
|
66
|
+
## PO ↔ order bridge links (header + line)
|
|
67
|
+
|
|
68
|
+
A single NetSuite PO is bridged to whichever order table the order landed in, at both header and
|
|
69
|
+
line level; links are idempotent (line matching is by shared item `uuid`):
|
|
70
|
+
|
|
71
|
+
| Order routed to | Header bridge | Line bridge |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| stocking → SalesOrders | `PurchaseOrders_SalesOrders` | `PurchaseOrderItems_SalesOrderItems` |
|
|
74
|
+
| transfer → TransferOrders | `PurchaseOrders_TransferOrders` | `PurchaseOrderItems_TransferOrderItems` |
|
|
75
|
+
|
|
76
|
+
`linkPurchaseOrderToTransferOrder()` is the new transfer-order counterpart to the existing
|
|
77
|
+
sales-order linker. See the shared
|
|
78
|
+
[SO↔PO bridge direction](../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md)
|
|
79
|
+
doc for the bridge naming convention.
|
|
80
|
+
|
|
81
|
+
## Transfer-order origin/destination location resolution
|
|
82
|
+
|
|
83
|
+
`TransferOrders.originLocationId` is **NOT NULL**, so a missing origin fails the V2 insert with
|
|
84
|
+
**EV-10** ("originLocationId cannot be null"). Two fixes were needed:
|
|
85
|
+
|
|
86
|
+
1. **The line SuiteQL never selected the line location.** `App_Api_Netsuite_Rest::listSalesOrders()`
|
|
87
|
+
line query now selects `tl.location` and `BUILTIN.DF(tl.location) AS location_name`. The line
|
|
88
|
+
shim already *read* these fields, but they were never *selected*, so they were always null — see
|
|
89
|
+
the [SuiteQL/REST shim](../../../1.0/apps/library/features/netsuite-suiteql-rest-shim.md).
|
|
90
|
+
2. **Header-location fallback.** In `toga2.php`, origin resolution falls back to the **first
|
|
91
|
+
line-item location** when the NetSuite header location is empty.
|
|
92
|
+
|
|
93
|
+
> **OPEN edge case (pending developer decision):** NetSuite sales order **128829** has **no
|
|
94
|
+
> location** on the header *or* on any SuiteQL line, so neither rule resolves an origin. A
|
|
95
|
+
> default-origin rule is proposed — candidate: NYCHH warehouse **Location 75** /
|
|
96
|
+
> `c_netsuiteInternalLocationId` **117** — but not yet applied. Until decided, 128829 cannot import.
|
|
97
|
+
|
|
98
|
+
## Item-fulfillment transfer-order line link
|
|
99
|
+
|
|
100
|
+
`ItemFulfillmentItems.transferOrderItemId` must populate so the 2.0 stock-out quantity calc can tie
|
|
101
|
+
a fulfillment back to its transfer-order line. In `syncItemFulfillmentFromNetsuite`:
|
|
102
|
+
|
|
103
|
+
- added `itemFulfillmentItems.transferOrderItem.uuid` to the GET `fields` allowlist,
|
|
104
|
+
- null-guarded the dedup lookup (`?? null`),
|
|
105
|
+
- the header IF POST sets `transferOrder` when applicable.
|
|
106
|
+
|
|
107
|
+
## Transfer-order status map
|
|
108
|
+
|
|
109
|
+
`syncTransferOrderFromNetsuite`'s status switch was expanded to cover the real NetSuite statuses:
|
|
110
|
+
**Billed**, **Pending Billing**, **Pending Billing-Partially Fulfilled** → `received` / `inTransit`.
|
|
111
|
+
The **default** now **logs and defaults to `'pending'`** instead of throwing. An unmapped "Billed"
|
|
112
|
+
status was previously throwing (and being swallowed), which is why order 128829 never imported.
|
|
113
|
+
|
|
114
|
+
`App_NetSuite::getCustomFieldValue()` (`library/app/netsuite.php`) was hardened with `?? []` / `?? null`
|
|
115
|
+
guards: 1.0 targets PHP 7.2 and any PHP warning on that path terminates the cron.
|
|
116
|
+
|
|
117
|
+
## Gotchas / known issues
|
|
118
|
+
|
|
119
|
+
- **Order 128829 (no location anywhere) is unresolved** — see the origin-resolution section. Do not
|
|
120
|
+
assume the importer is broken if this single order is stuck; every located order imports.
|
|
121
|
+
- **Routing is gated by the wrapper constant.** If a future client should route transfer orders,
|
|
122
|
+
add `const IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING = true;` before its
|
|
123
|
+
`require_once 'common_sync_togasupply.php';` — and confirm that client actually sets
|
|
124
|
+
`custbody_stocking_order` in NetSuite. Absent the flag, everything lands in SalesOrders.
|
|
125
|
+
- **`custbody_stocking_order` must reach the shim.** `fetchSalesOrderById()` now carries the field
|
|
126
|
+
via `customFieldList`; a missing custom field read returns null → the order routes as a transfer
|
|
127
|
+
order (unchecked = transfer). Verify the field is populated in NetSuite before trusting the split.
|
|
128
|
+
|
|
129
|
+
## Change history
|
|
130
|
+
|
|
131
|
+
- 2026-08-27 — Built NYCHH transfer-order ingestion end-to-end. **Routing by `custbody_stocking_order`
|
|
132
|
+
(id 7097), not dollar amount** (`isTransferOrder()` rewritten + new `isStockingOrder()` normalizer);
|
|
133
|
+
the old `total===0 + hold-flag` heuristic was wrong for NYCHH (no hold flag set → everything landed
|
|
134
|
+
in SalesOrders) and is removed. Routing is opt-in per client via
|
|
135
|
+
`IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING` (default off; NYCHH true). Added the
|
|
136
|
+
`PurchaseOrders_TransferOrders` / `PurchaseOrderItems_TransferOrderItems` bridge linker; resolved TO
|
|
137
|
+
origin (line SuiteQL now selects `tl.location`, header-empty falls back to first line location);
|
|
138
|
+
linked item fulfillments to their transfer-order line (`transferOrderItem.uuid`); expanded the TO
|
|
139
|
+
status map (Billed / Pending Billing → received/inTransit, default `pending` instead of throwing);
|
|
140
|
+
hardened `getCustomFieldValue()` for PHP 7.2. A transfer order imported into
|
|
141
|
+
`Client_Nychh.TransferOrders` after deploy. **Open:** order 128829 has no location on header or any
|
|
142
|
+
line — default-origin rule (candidate Location 75 / `c_netsuiteInternalLocationId` 117) pending the
|
|
143
|
+
developer's decision. (jcardinal)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "NYCHH transfer-order 2.0 model — inventory quantities + V2 field enablement"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: nychh
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-27
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Nychh/Item.php
|
|
13
|
+
- _underscore/Model/Nychh/TransferOrder.php
|
|
14
|
+
- _underscore/Model/Nychh/TransferOrderStage.php
|
|
15
|
+
- _underscore/Model/Client/TransferOrder.php
|
|
16
|
+
- dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql
|
|
17
|
+
- dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql
|
|
18
|
+
related:
|
|
19
|
+
- ../../../2.0/apps/_underscore/features/acl-permission-chain.md
|
|
20
|
+
- ../../../2.0/apps/api2/features/v2-api-error-codes.md
|
|
21
|
+
- ../../growrk/features/transfer-order-flow.md
|
|
22
|
+
- ./netsuite-transfer-order-import.md
|
|
23
|
+
- ../profile.md
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
The 2.0 (`_underscore`) side of NYCHH transfer-order support: a **two-branch inventory quantity
|
|
29
|
+
model** on the NYCHH `Item` override, plus the **client-override model declarations and ACL rows**
|
|
30
|
+
that let a NetSuite-sourced TransferOrder actually POST through the V2 API. The 1.0 ingestion that
|
|
31
|
+
feeds these POSTs is in
|
|
32
|
+
[NYCHH NetSuite → TransferOrders import](./netsuite-transfer-order-import.md).
|
|
33
|
+
|
|
34
|
+
The general, framework-level lessons from the field-exposure work here are recorded on the shared
|
|
35
|
+
[ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md) and
|
|
36
|
+
[V2 API error codes](../../../2.0/apps/api2/features/v2-api-error-codes.md) docs; this doc keeps the
|
|
37
|
+
NYCHH-specific composition.
|
|
38
|
+
|
|
39
|
+
## NYCHH-only quantity calculations (two-branch inventory model)
|
|
40
|
+
|
|
41
|
+
Per the 2026-08-17 "TOGa Supply" meeting. **NYCHH ONLY** — the overrides live on the client model
|
|
42
|
+
`_Model_Nychh_Item` (`_underscore/Model/Nychh/Item.php`), not the base. Stocking orders bring stock
|
|
43
|
+
**in**, transfer orders send it **out**, so the quantities reflect that direction:
|
|
44
|
+
|
|
45
|
+
| Quantity | Definition |
|
|
46
|
+
|---|---|
|
|
47
|
+
| **on hand** | `received` (PO item receipts) − `fulfilled` (transfer-order item fulfillments) |
|
|
48
|
+
| **available** | `on hand` − `committed` |
|
|
49
|
+
| **fulfilled** | Σ shipped transfer-order fulfillments |
|
|
50
|
+
| **backordered** | `ordered` − `received` |
|
|
51
|
+
|
|
52
|
+
`_qtyOnOrder` was added alongside these. The stock-in side reads PO receipts; the stock-out side
|
|
53
|
+
reads transfer orders (which is why the 1.0 importer must populate
|
|
54
|
+
`ItemFulfillmentItems.transferOrderItemId` — see the import doc).
|
|
55
|
+
|
|
56
|
+
## V2 field enablement — how the TransferOrder POST was made to work
|
|
57
|
+
|
|
58
|
+
Posting a NetSuite-sourced transfer order that carries NetSuite custom fields required **every** part
|
|
59
|
+
of the custom-field recipe on the NYCHH client DB. Each missing part surfaced as a distinct V2 error,
|
|
60
|
+
peeled in order:
|
|
61
|
+
|
|
62
|
+
1. **Client-override model must DECLARE the custom field.** The framework resolves
|
|
63
|
+
`_Model_Nychh_X extends _Model_Client_X`, and the NetSuite `c_` declarations come from
|
|
64
|
+
`_Trait_Netsuite_X`. Created:
|
|
65
|
+
- `_underscore/Model/Nychh/TransferOrder.php` —
|
|
66
|
+
`class _Model_Nychh_TransferOrder extends _Model_Client_TransferOrder { use _Trait_Netsuite_TransferOrder; }`
|
|
67
|
+
(declares `c_netsuiteInternalSalesOrderId`) → fixes the **EO-1** 500.
|
|
68
|
+
- `_underscore/Model/Nychh/TransferOrderStage.php` —
|
|
69
|
+
`class _Model_Nychh_TransferOrderStage extends _Model_Client_TransferOrderStage { use _Trait_Netsuite_TransferOrderStage; }`
|
|
70
|
+
(declares `c_netsuiteInternalTransferOrderStatus`) → fixes **EV-12** `invalidSearchableField`.
|
|
71
|
+
2. **ACL write grant** — an `AclCustomFieldPermissions` row (roleId 1, `isWritable 1`) for
|
|
72
|
+
transfer-orders `c_netsuiteInternalSalesOrderId` (record 312) → fixes **EV-9**.
|
|
73
|
+
`dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql`.
|
|
74
|
+
3. **Searchable identifier** — for a nested FK resolved *by a custom field*, that field's
|
|
75
|
+
`CustomRecordFields.isIdentifier` must be `1`. Set `isIdentifier=1` on
|
|
76
|
+
`c_netsuiteInternalTransferOrderStatus` (recordId 351) → fixes **EV-12**
|
|
77
|
+
`searchableIdentifierFields:[id]`.
|
|
78
|
+
`dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql`.
|
|
79
|
+
|
|
80
|
+
> **There is NO ACL/metadata caching layer in the 2.0 V2 API.** Applying these ACL/metadata rows
|
|
81
|
+
> takes effect on the **very next request** — there is no cache to invalidate and no cache-bust step.
|
|
82
|
+
> (This corrects an earlier assumption that a cache had to be busted, which sent debugging down a
|
|
83
|
+
> dead end.) Recorded framework-wide on the
|
|
84
|
+
> [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md).
|
|
85
|
+
|
|
86
|
+
## `isActive` is defaulted in the ORM, not sent in the payload (shared model)
|
|
87
|
+
|
|
88
|
+
`TransferOrders.isActive` is **NOT NULL**, but `isActive` is **not a registered/writable V2 field** —
|
|
89
|
+
adding `'isActive' => 1` to the POST payload throws **EV-8** ("field does not exist"). The correct
|
|
90
|
+
fix is to default it in the ORM: `_underscore/Model/Client/TransferOrder.php` now declares
|
|
91
|
+
`public $isActive = [self::FIELD_BOOLEAN, self::FIELDOPT_DEFAULT => true];`, so the ORM writes `1` on
|
|
92
|
+
insert without a payload key. This is a **shared** (`_Model_Client_TransferOrder`) change — the
|
|
93
|
+
pattern (default a NOT NULL non-writable column in the model, never send it) is on the
|
|
94
|
+
[V2 API error codes](../../../2.0/apps/api2/features/v2-api-error-codes.md) map under EV-8.
|
|
95
|
+
|
|
96
|
+
## Gotchas / known issues
|
|
97
|
+
|
|
98
|
+
- **A custom field is writable/searchable ONLY when BOTH the model declares it (via the NetSuite
|
|
99
|
+
trait on the client-override model) AND the ACL/metadata rows exist.** Declaring without the ACL
|
|
100
|
+
row, or granting the ACL without the declaration, each fails with its own error — see the peeled
|
|
101
|
+
sequence above.
|
|
102
|
+
- **These quantity overrides are NYCHH-only.** They live on `_Model_Nychh_Item`; do not lift them to
|
|
103
|
+
the base `_Model_Client_Item` — other clients do not have the stocking/transfer two-branch model.
|
|
104
|
+
|
|
105
|
+
## Change history
|
|
106
|
+
|
|
107
|
+
- 2026-08-27 — Built the NYCHH two-branch inventory quantity model on `_Model_Nychh_Item` (on hand =
|
|
108
|
+
received − fulfilled; available = on hand − committed; fulfilled = Σ shipped TO fulfillments;
|
|
109
|
+
backordered = ordered − received; added `_qtyOnOrder`), per the 2026-08-17 TOGa Supply meeting.
|
|
110
|
+
Enabled the NetSuite-sourced TransferOrder POST by creating the NYCHH client-override models
|
|
111
|
+
`_Model_Nychh_TransferOrder` / `_Model_Nychh_TransferOrderStage` (each `use`s its
|
|
112
|
+
`_Trait_Netsuite_*` to declare the `c_` field), adding the `AclCustomFieldPermissions` write grant
|
|
113
|
+
(2026-08-27a) and setting `CustomRecordFields.isIdentifier=1` on
|
|
114
|
+
`c_netsuiteInternalTransferOrderStatus` (2026-08-27b). Defaulted the shared
|
|
115
|
+
`_Model_Client_TransferOrder::$isActive` in the ORM (NOT NULL, non-writable → EV-8 if sent).
|
|
116
|
+
Confirmed there is **no ACL/metadata cache** — rows apply on the next request. (jcardinal)
|
|
@@ -15,15 +15,19 @@ project: _Underscore
|
|
|
15
15
|
client: nychh
|
|
16
16
|
type: profile
|
|
17
17
|
status: active
|
|
18
|
-
updated: 2026-08-
|
|
18
|
+
updated: 2026-08-27
|
|
19
19
|
owners: ["jcardinal", "apeterson", "bala"]
|
|
20
20
|
files:
|
|
21
21
|
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
22
22
|
- dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql
|
|
23
|
+
- dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql
|
|
24
|
+
- dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql
|
|
23
25
|
related:
|
|
24
26
|
- ../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
25
27
|
- ../../2.0/apps/_underscore/features/tableview-joins.md
|
|
26
28
|
- ./features/po-number-upstream-direction.md
|
|
29
|
+
- ./features/netsuite-transfer-order-import.md
|
|
30
|
+
- ./features/transfer-order-inventory-quantities.md
|
|
27
31
|
- ../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
|
|
28
32
|
---
|
|
29
33
|
|
|
@@ -53,6 +57,14 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
53
57
|
`macAddress` synced from the NetSuite serial (inventory-number) record, closing the gap where a
|
|
54
58
|
tag added after the ItemShip never bumps the fulfillment sync. See
|
|
55
59
|
[NYCHH Asset-Tag Backfill](../../2.0/apps/worker2/features/nychh-asset-tag-backfill.md).
|
|
60
|
+
- **Transfer orders (NetSuite sales orders → `TransferOrders`)** — NYCHH's NetSuite transfer orders
|
|
61
|
+
arrive as sales orders; the 1.0 importer routes them by the `custbody_stocking_order` flag (not the
|
|
62
|
+
dollar amount), bridges the PO, and resolves origin/destination locations. See
|
|
63
|
+
[NYCHH NetSuite → TransferOrders import](./features/netsuite-transfer-order-import.md).
|
|
64
|
+
- **Two-branch inventory quantities + TransferOrder V2 enablement** — NYCHH-only on-hand/available/
|
|
65
|
+
fulfilled/backordered model on `_Model_Nychh_Item`, plus the client-override models + ACL rows that
|
|
66
|
+
let a NetSuite TransferOrder POST succeed. See
|
|
67
|
+
[NYCHH transfer-order inventory & quantities](./features/transfer-order-inventory-quantities.md).
|
|
56
68
|
|
|
57
69
|
- **PO links are UPSTREAM (`PurchaseOrders_SalesOrders`), not downstream.** The downstream
|
|
58
70
|
`sales-order-purchase-orders` route returns an empty array for NYCHH by design, and the shared
|
package/package.json
CHANGED