toga-ai 1.0.700 → 1.0.702
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 +52 -2
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +121 -1
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +11 -0
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +20 -1
- package/knowledge/clients/nychh/INDEX.md +1 -1
- package/knowledge/clients/nychh/features/netsuite-inventory-adjustment-fulfillment-link.md +18 -1
- package/knowledge/clients/nychh/features/netsuite-transfer-order-import.md +88 -1
- package/knowledge/standalone/standards/session-priming.md +12 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-30
|
|
10
10
|
owners: [dfranks, jcardinal, kyalamarthi]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/netsuite/rest.php
|
|
@@ -135,7 +135,40 @@ Forecast2 tables already store.
|
|
|
135
135
|
vendorless PO still throws. Same class as the `tl.location` regression below and the missing-entity
|
|
136
136
|
guard was correct — the SELECT was the defect. **When adding a shim, every field the guard/consumer
|
|
137
137
|
reads must actually be in the SELECT.**
|
|
138
|
-
|
|
138
|
+
- **The entity fix was only the first of a chain — the real fix completed the shim to MATCH
|
|
139
|
+
`listPurchaseOrders`' FULL shape (deploy/verify session, 2026-08-29).** `syncPurchaseOrderFromNetsuite`
|
|
140
|
+
(reached via the item-receipt path) reads a whole set of PO properties **unguarded**, so each one the
|
|
141
|
+
`fetchPurchaseOrderById` shim omitted threw a 1.0 warning → `App_Error` `exit()` → froze the section —
|
|
142
|
+
an "entity → createdFrom → shippingAddress → customForm → tranId → tranDate → line/description/rate"
|
|
143
|
+
whack-a-mole, one missing field surfacing the next. Fixed by making the per-id shim reproduce the
|
|
144
|
+
sibling **list** shape exactly: added `entity` + `trandate` to the header SuiteQL, `tl.rate` + `tl.memo`
|
|
145
|
+
to the line query, and populated `createdFrom` / `shippingAddress` / `customForm` from the per-id REST
|
|
146
|
+
GET. **Lesson (sharper than "a field the consumer reads must be in the SELECT"): a REST shim consumed
|
|
147
|
+
by a SHARED sync method must reproduce the FULL sibling-`list*` shape — every header field, every line
|
|
148
|
+
field, every nested object — not just the fields the one path you are testing happens to read.** This
|
|
149
|
+
cleared Canon(12)/Endeavor(11) ITEM_RECEIPTS to IDLE.
|
|
150
|
+
|
|
151
|
+
- **`buildSalesOrderShims()` / `fetchSalesOrderByIdFull()` SO-item shims now carry `createdPo` + `poRate`
|
|
152
|
+
(2026-08-29).** `syncPurchaseOrderFromNetsuite`'s SO-reconciliation loop reads
|
|
153
|
+
`$nsSalesOrderItem->createdPo` ({internalId}|null) and `$nsSalesOrderItem->poRate` (float|null)
|
|
154
|
+
**unguarded** — an `Undefined property $createdPo` → `exit()`. This loop was **newly reached** by the
|
|
155
|
+
`createdFrom` completion above, which is exactly how the next-missing-field surfaced. Both are now read
|
|
156
|
+
defensively from the per-id REST `salesOrder` sublist already fetched, so the SO-item shim matches what
|
|
157
|
+
the loop consumes. Same "reproduce the full sibling shape" class as the PO-shim completion.
|
|
158
|
+
|
|
159
|
+
- **⚠ `fetchPurchaseOrderById` must number PO lines by the NetSuite `line`, not `linesequencenumber` —
|
|
160
|
+
and the PO record sublist has NO `lineUniqueKey` to join on (fixed 2026-08-30).** Item receipts match
|
|
161
|
+
their PO line by the receipt's `orderLine` (the NetSuite **`line`** number). `fetchPurchaseOrderById`
|
|
162
|
+
numbered `PurchaseOrderItems.lineNumber` by physical `linesequencenumber` (1..N), so when a middle PO
|
|
163
|
+
line was deleted in NetSuite the real lines are e.g. 1,2,4 and a receipt for line 4 failed `EV-12`
|
|
164
|
+
"unable to find a unique match" (GroWrk item receipts, PO **168223**). `listPurchaseOrders` already did
|
|
165
|
+
this right via `lineUniqueKey`; the per-id path did not. **GOTCHA:** the REST `purchaseOrder` record
|
|
166
|
+
line sublist (`expandSubResources=true`) exposes **`line` but NO `lineUniqueKey`/`uniquekey`** to join
|
|
167
|
+
on (unlike `salesOrder`). **Fix:** fetch the record with `expandSubResources=true` and **ZIP the REST
|
|
168
|
+
sublist (physical line order) positionally against the SuiteQL rows (`ORDER BY linesequencenumber`)**,
|
|
169
|
+
cross-checking item id, then number each line by the NetSuite `line` (fall back to `linesequencenumber`
|
|
170
|
+
if the item ids disagree). Self-heals on the next receipt sync (it re-imports the PO). Same "join a
|
|
171
|
+
NetSuite line on `line`, not `linesequencenumber`" lesson as the `tl.id` semantics above.
|
|
139
172
|
- **A shim must produce the SOAP object's SHAPE, not just its scalar fields — an absent nested list
|
|
140
173
|
crashes 1.0 (2026-08-29).** `fetchCustomerById()`'s `addressbookList` was where the AIG lookup phase
|
|
141
174
|
crashed: a REST customer with no/odd address made the shim omit the nested `addressbook` structure the
|
|
@@ -229,6 +262,23 @@ None — uniform across clients (NetSuite is a single shared account).
|
|
|
229
262
|
|
|
230
263
|
## Change history
|
|
231
264
|
|
|
265
|
+
- 2026-08-30 — Recorded that **`fetchPurchaseOrderById` numbered PO lines by `linesequencenumber` instead
|
|
266
|
+
of the NetSuite `line`** — a receipt for a post-gap line (PO with a deleted middle line, real lines
|
|
267
|
+
1,2,4) failed `EV-12` "unable to find a unique match" (GroWrk PO 168223). The PO record line sublist
|
|
268
|
+
exposes `line` but **no `lineUniqueKey`** (unlike `salesOrder`), so the fix zips the REST sublist
|
|
269
|
+
positionally against the `ORDER BY linesequencenumber` SuiteQL rows (item-id cross-checked) and numbers
|
|
270
|
+
by `line`. `library`-only; self-heals on next receipt sync. (jcardinal)
|
|
271
|
+
- 2026-08-29 — **Completed `fetchPurchaseOrderById` to the full `listPurchaseOrders` shape** and added
|
|
272
|
+
the SO-item `createdPo`/`poRate` shim fields (same deploy/verify session, after the entity fix). The
|
|
273
|
+
entity SELECT was only the first missing field: `syncPurchaseOrderFromNetsuite` reads `createdFrom`,
|
|
274
|
+
`shippingAddress`, `customForm`, `tranId`, `tranDate`, and line `rate`/`description` unguarded, so each
|
|
275
|
+
omission threw in turn (whack-a-mole). Fixed by matching the sibling **list** shape exactly (header:
|
|
276
|
+
`entity`+`trandate`; line: `tl.rate`+`tl.memo`; populated `createdFrom`/`shippingAddress`/`customForm`
|
|
277
|
+
from the per-id REST GET). Completing `createdFrom` newly reached the SO-reconciliation loop, exposing
|
|
278
|
+
the next unguarded reads `$nsSalesOrderItem->createdPo`/`poRate` — now carried on the
|
|
279
|
+
`buildSalesOrderShims`/`fetchSalesOrderByIdFull` SO-item shims (read from the per-id REST `salesOrder`
|
|
280
|
+
sublist). Lesson: a shim consumed by a shared sync method must reproduce the FULL sibling-list shape,
|
|
281
|
+
not the fields the tested path happens to read. (jcardinal)
|
|
232
282
|
- 2026-08-29 — Recorded `fetchPurchaseOrderById()`'s **missing `entity` SELECT** (deploy/verify
|
|
233
283
|
session): its header query omitted `entity`/`BUILTIN.DF(entity)` and the shim never set
|
|
234
284
|
`$shim->entity` (unlike `listPurchaseOrders`), so the PO vendor was always null and the
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-30
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -314,6 +314,16 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
314
314
|
checkbox `custbody_stocking_order`, read by `App_Api_Toga2::isTransferOrder()`/`isStockingOrder()`
|
|
315
315
|
— **not** a dollar/hold heuristic. Full mechanics, PO bridges, and origin-location resolution:
|
|
316
316
|
[NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md).
|
|
317
|
+
- **Transfer-order integration clients (beyond NYCHH's stocking-flag routing).** GroWrk, Adyen, and Elite
|
|
318
|
+
run transfer-order integration (wrapper toggle reported as `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS=true`)
|
|
319
|
+
and are the clients exercised by the shared `syncTransferOrderFromNetsuite` / `getTransferOrderItemsByUuid`
|
|
320
|
+
fixes (2026-08-30 above). These orders **masquerade as NetSuite sales orders** ($0, `holdInvoice` for
|
|
321
|
+
GroWrk) and are split into `TransferOrders` vs `SalesOrders` by `App_Api_Toga2::isTransferOrder()`.
|
|
322
|
+
The **routing SIGNAL is per-client**: NYCHH routes by the `custbody_stocking_order` stocking flag (above),
|
|
323
|
+
while GroWrk's own signal is `holdInvoice + total === 0` — do **not** assume one universal rule.
|
|
324
|
+
(⚠ Confirm the exact enable-flag name for GroWrk/Adyen/Elite against the wrappers: the stocking-flag const
|
|
325
|
+
`IS_ENABLED_TRANSFER_ORDER_STOCKING_FLAG_ROUTING` is documented as NYCHH-only, so
|
|
326
|
+
`IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` is either a distinct toggle or a loose name for it.)
|
|
317
327
|
- `isParentCustomer` — `true` walks NetSuite child customers (`listChildCustomers`); `false`
|
|
318
328
|
syncs the single customer. Prudential-EDI and the live Quad wrapper use `false`.
|
|
319
329
|
- The original combined-array entries used `App_Api_Toga2::CLIENT_UUID_*` consts for Quad and
|
|
@@ -640,8 +650,118 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
640
650
|
onboarding effort.
|
|
641
651
|
Both are pre-existing and unrelated to this session's fixes.
|
|
642
652
|
|
|
653
|
+
- **⚠ A serialized unit's OWN location can differ from its receipt line's — auto-create it before the
|
|
654
|
+
`/units` write or api2 400s EV-12 (fixed 2026-08-29, `library/app/api/toga2.php`
|
|
655
|
+
`syncItemReceiptFromNetsuite`).** The unit carries its own `custitemnumber_location` (referenced on the
|
|
656
|
+
`/units` write by `c_netsuiteInternalLocationId`); when that location differs from the receipt line's
|
|
657
|
+
and isn't provisioned in the client DB, api2 rejects the unit write **EV-12**. The fix now **POSTs
|
|
658
|
+
`/locations` for the unit's own location before the write**, mirroring the existing item-line location
|
|
659
|
+
create one level up. Shared/general fix (any client running serialized item receipts), surfaced by
|
|
660
|
+
**GroWrk** location **1569**. Deployed.
|
|
661
|
+
|
|
662
|
+
- **⚠ The exact api2 rejection detail lives in `Logs_<Client>.Api`, not `Logs.Event` (which TRUNCATES
|
|
663
|
+
it).** When a section throws on an api2 non-success, `Logs.Event` records the error envelope but
|
|
664
|
+
**truncates** the useful detail (the `EV-9`/`EV-12` field/`searchCriteria` specifics). The full
|
|
665
|
+
request/response is in the **per-client** `Logs_<Client>.Api` table — **join on the `transactionId`
|
|
666
|
+
carried in the error envelope**; it has `method`, `route`, `responseCode`, `requestPayload`, and
|
|
667
|
+
`responsePayload`. So the diagnosis chain is: `Client_<X>.Parameters` (localize the stuck section) →
|
|
668
|
+
`Logs.Event` filtered by `clientId` (crash file:line + `transactionId`) → `Logs_<Client>.Api` on that
|
|
669
|
+
`transactionId` (the untruncated api2 rejection). (Distinct from the fatal-before-logging case where the
|
|
670
|
+
api2 request dies before the middleware and writes **no** `Logs_<Client>.Api` row — then read
|
|
671
|
+
`Logs.Issue` instead; see the 2026-08-18 poison-order diagnostic trap above.)
|
|
672
|
+
|
|
673
|
+
- **⚠ The shared `syncTransferOrderFromNetsuite` item-sync WAS NON-IDEMPOTENT — created DUPLICATE
|
|
674
|
+
`TransferOrderItems` every run (found 2026-08-29; ROOT-CAUSED + FIXED 2026-08-30; affected stocking-SO
|
|
675
|
+
TOs too).** The shared
|
|
676
|
+
item loop does not reconcile against existing rows, so every sync appends the TO's items again: an
|
|
677
|
+
attempted real-TrnfrOrd import reached **22 duplicate items all `lineNumber` 1** on TO #15, and a
|
|
678
|
+
**pre-existing stocking-SO** transfer order (`Client_Nychh.TransferOrders` id **1** / number **62541**)
|
|
679
|
+
already carries **13 duplicates** — proving the bug is in the **shared** loop, not the real-TO path.
|
|
680
|
+
Because of this (plus an api2 `transferOrderItem`-reference gap and a receipt-line↔TO-line mapping
|
|
681
|
+
mismatch), the **real NetSuite Transfer Order (TrnfrOrd) item-receipt feature is INTENTIONALLY SKIPPED**
|
|
682
|
+
— a documented skip in `common_sync_togasupply.php`, exactly like the existing Return-Authorization
|
|
683
|
+
(`RtnAuth`) receipt skip — pending a proper rebuild. Full state, the inactive scaffolding left staged,
|
|
684
|
+
and the rebuild spec are on the
|
|
685
|
+
[NYCHH NetSuite → TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md)
|
|
686
|
+
doc. **Do not re-enable real-TO receipts until the shared item loop is made idempotent (which also
|
|
687
|
+
fixes the stocking-SO dups and cleans the pre-existing TO 1).**
|
|
688
|
+
|
|
689
|
+
**ROOT CAUSE + FIX (2026-08-30).** The non-idempotency was NOT a bad reconcile loop — it was that
|
|
690
|
+
`syncTransferOrderFromNetsuite` read `$transferOrder->transferOrderItems` for its idempotency lookup,
|
|
691
|
+
and **api2 never inlines that child collection** (`transfer-orders` has no `Core.InherentRecordChildren`
|
|
692
|
+
row, unlike its sibling order types sales-orders/purchase-orders/item-fulfillments), so the lookup was
|
|
693
|
+
**always empty** and every line re-INSERTed each run (TransferOrder 26 grew to 84 duplicate items). New
|
|
694
|
+
private helper `App_Api_Toga2::getTransferOrderItemsByUuid` now fetches TO items via the **direct
|
|
695
|
+
`GET /transfer-order-items`** query (join `TransferOrders` by uuid, high `recordsPerPage`), used by both
|
|
696
|
+
`syncTransferOrderFromNetsuite` (idempotency) and `syncItemFulfillmentFromNetsuite` (TO-line linking);
|
|
697
|
+
**first-wins**, so pre-existing dups keep one canonical row and no new dups are created. Shared fix —
|
|
698
|
+
all TO clients (GroWrk, Adyen, Elite). The 2.0 root cause and the parity migration that also adds the
|
|
699
|
+
missing `InherentRecordChildren` row are on the
|
|
700
|
+
[V2 REST query contract](../../../2.0/apps/api2/features/v2-rest-query-contract.md). This closes
|
|
701
|
+
rebuild-spec **step 1** on the
|
|
702
|
+
[NYCHH TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md); the
|
|
703
|
+
other real-TrnfrOrd-receipt blockers (api2 `transferOrderItem` reference metadata, receipt-line↔TO-line
|
|
704
|
+
mapping) remain.
|
|
705
|
+
|
|
706
|
+
- **⚠ Inventory-adjustment sync skipped NULL-quantity NetSuite serial DETAIL sub-lines (fixed 2026-08-30).**
|
|
707
|
+
NetSuite serialized inventory-number DETAIL sub-lines **repeat the main item line** with a NULL
|
|
708
|
+
`transactionline.quantity`; `syncInventoryAdjustmentFromNetsuite` tried to write
|
|
709
|
+
`InventoryAdjustmentItems` with a null `quantityAdjustBy` → api2 `EV-10` / MySQL **1048**
|
|
710
|
+
"quantityAdjustBy cannot be null." Those sub-lines carry no adjustment (the real qty is on the summary
|
|
711
|
+
line). **Fix (`library`):** skip lines whose `adjustQtyBy` is null in **BOTH** passes over
|
|
712
|
+
`inventoryList->inventory` (the item-build pass and the serial/unit pass). Shared fix; unblocked NYCHH
|
|
713
|
+
inventory adjustments.
|
|
714
|
+
- **⚠ Item-fulfillment stale-item prune must delete tracking-number children before the unit AND the item
|
|
715
|
+
(fixed 2026-08-30).** In `syncItemFulfillmentFromNetsuite`'s cleanup loop, deleting a stale
|
|
716
|
+
item-fulfillment-item-**unit** failed api2 `EV-11` / MySQL **1451** (FK
|
|
717
|
+
`ItemFulfillmentItemUnits_TrackingNumbers → ItemFulfillmentItemUnits`, RESTRICT) because the
|
|
718
|
+
tracking-number children were not removed first — freezing the fulfillment sync at `1-RUNNING`. **Fix:**
|
|
719
|
+
delete each unit's `itemFulfillmentItemUnitTrackingNumbers` (route
|
|
720
|
+
`/item-fulfillment-item-unit-tracking-numbers`) before the unit, and each item's
|
|
721
|
+
`itemFulfillmentItemTrackingNumbers` (`/item-fulfillment-item-tracking-numbers`) before the item; added
|
|
722
|
+
`itemFulfillmentItems.itemFulfillmentItemTrackingNumbers.uuid` to the GET `fields`. Shared fix (all
|
|
723
|
+
clients); resolved NYCHH ITEM_FULFILLMENTS. (Same delete-children-first rule as the 317/318/319 bridge
|
|
724
|
+
pruning above — extended to the item- and unit-level tracking children in the stale-item path.)
|
|
725
|
+
- **⚠ Item-fulfillment `/units` lookup must be scoped by the line's product on the TRANSFER-ORDER path too
|
|
726
|
+
(fixed 2026-08-30).** A serialized unit failed `POST /units` with `EV-10` / MySQL **1062** "Duplicate
|
|
727
|
+
entry '<itemId>-<serial>' for key `Units.itemId_serialNumber`" because the `Units` lookup filtered by
|
|
728
|
+
item **only on the sales-order path** (`if ($uuidSalesOrderItem)`); a transfer-order line searched
|
|
729
|
+
`Units` by `serialNumber` alone, and with **duplicate `Items` sharing a partNumber** (GroWrk Items 3861
|
|
730
|
+
& 4086, both `21QT006VUS`, each with a unit for serial `SPF65LSAC`) the serial-only lookup returned 2 →
|
|
731
|
+
it did not reuse the existing unit and the create collided on `(itemId, serialNumber)`. **Fix:** resolve
|
|
732
|
+
the line's product uuid for **both** paths into a new `$uuidItemForLine` (SO: from the sales-order-item
|
|
733
|
+
lookup; TO: from `getTransferOrderItemsByUuid`'s `->item->uuid`, so that helper was bumped to **depth
|
|
734
|
+
2**) and scope the `Units` lookup by it (returns exactly 1 → reuse). Latent data problem behind it:
|
|
735
|
+
**duplicate `Items` with the same `partNumber`** should be reconciled later (the fix is robust to it).
|
|
736
|
+
GroWrk ITEM_FULFILLMENTS went from stuck `1-RUNNING` (since Aug 21) to green.
|
|
737
|
+
|
|
643
738
|
## Change history
|
|
644
739
|
|
|
740
|
+
- 2026-08-30 — **Rebuilt GroWrk transfer-order integration + shared IF/inventory-adjustment fixes (all in
|
|
741
|
+
`library/app/api/toga2.php`).** Root-caused the shared `syncTransferOrderFromNetsuite` **non-idempotency**
|
|
742
|
+
to api2 never inlining `transferOrderItems` (`transfer-orders` has no `InherentRecordChildren` row) — the
|
|
743
|
+
idempotency lookup was always empty, so lines re-INSERTed every run (TransferOrder 26 → 84 dup items);
|
|
744
|
+
fixed with a new `getTransferOrderItemsByUuid` direct `GET /transfer-order-items` helper (first-wins),
|
|
745
|
+
used for both TO idempotency and IF→TO-line linking (all TO clients — GroWrk/Adyen/Elite). Fixed the IF
|
|
746
|
+
stale-item prune (delete unit/item tracking-number children before the unit/item — FK RESTRICT/1451/EV-11)
|
|
747
|
+
and the IF `/units` lookup (scope by the line's product on the TO path too via `$uuidItemForLine`, helper
|
|
748
|
+
bumped to depth 2 — a serial-only lookup hit two duplicate `Items` sharing a partNumber → 1062 dup-key),
|
|
749
|
+
and the inventory-adjustment sync (skip NULL-`adjustQtyBy` NetSuite serial DETAIL sub-lines in both passes
|
|
750
|
+
— 1048/EV-10). GroWrk IF unfroze (stuck since Aug 21); NYCHH IF + inventory adjustments resolved. Developer
|
|
751
|
+
also applied the 2.0 parity migration adding the `transfer-orders → transfer-order-items`
|
|
752
|
+
`InherentRecordChildren` row (dbchanges2) — see
|
|
753
|
+
[V2 REST query contract](../../../2.0/apps/api2/features/v2-rest-query-contract.md). (jcardinal)
|
|
754
|
+
- 2026-08-29 — **Stabilize session, additional durable findings.** (a) `syncItemReceiptFromNetsuite`
|
|
755
|
+
now **auto-creates a serialized unit's OWN location** (`custitemnumber_location` → POST `/locations`)
|
|
756
|
+
before the `/units` write, mirroring the item-line location create — a general fix surfaced by GroWrk
|
|
757
|
+
location 1569 (unit location ≠ receipt-line location → EV-12). (b) Recorded the diagnostic that
|
|
758
|
+
`Logs.Event` **truncates** the api2 rejection detail — the untruncated `method`/`route`/`responseCode`/
|
|
759
|
+
`requestPayload`/`responsePayload` is in the per-client `Logs_<Client>.Api` table, joined on the
|
|
760
|
+
envelope's `transactionId`. (c) Recorded that the shared `syncTransferOrderFromNetsuite` item-sync is
|
|
761
|
+
**non-idempotent** (duplicate `TransferOrderItems` every run — TO #15 reached 22 dups, pre-existing
|
|
762
|
+
stocking-SO TO 1/62541 has 13), so it affects stocking-SO TOs too; the **real-TrnfrOrd item-receipt
|
|
763
|
+
feature was intentionally SKIPPED** in `common_sync_togasupply.php` (like the RtnAuth skip) pending a
|
|
764
|
+
rebuild — rebuild spec on the NYCHH TransferOrders-import doc. (jcardinal)
|
|
645
765
|
- 2026-08-29 — **Deploy/verify/stabilize session for the SOAP→REST conversion.** Root-caused the
|
|
646
766
|
**Canon + Endeavor Health ITEM_RECEIPTS** freeze: `fetchPurchaseOrderById` never populated the PO
|
|
647
767
|
vendor (`entity` missing from its SuiteQL SELECT), so `syncPurchaseOrderFromNetsuite`'s fail-loud
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Proposed — git-sourced base+overlay JSON authoring for the Surface layer](architecture/surface-authoring-proposal.md) | A **proposal / handoff recommendation** (not implemented) that the Surface layer's *authoring* model move off hand-authored SQL against the `SurfaceOverrides` E | _underscore/Model/Core/Surface.php, _underscore/Model/Client/SurfaceOverride.php |
|
|
6
6
|
| [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
|
|
7
|
-
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, _underscore/Model/Core/Page.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql |
|
|
7
|
+
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, _underscore/Model/Core/Page.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql |
|
|
8
8
|
| [Address Uniqueness Normalization (unit identifier + 5-digit ZIP comparison)](features/address-uniqueness-normalization.md) | When a business rule says *"only one X per physical address"*, comparing address rows field-for-field does **not** work: the same dwelling is spelled many diffe | _underscore/Model/Rate/Entitlement.php |
|
|
9
9
|
| [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | `_Model_Client_Address::validateAddress` verifies a US address against a **carrier waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-norm | _underscore/Model/Client/Address.php, _underscore/Component/Library/Carriers/Usps/Usps.php |
|
|
10
10
|
| [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php |
|
|
@@ -19,6 +19,8 @@ files:
|
|
|
19
19
|
- dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql
|
|
20
20
|
- dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql
|
|
21
21
|
- dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql
|
|
22
|
+
- dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql
|
|
23
|
+
- dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql
|
|
22
24
|
---
|
|
23
25
|
|
|
24
26
|
## Summary
|
|
@@ -490,6 +492,15 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
|
|
|
490
492
|
and every repo is on the **same branch** so the generated model matches the DB.
|
|
491
493
|
|
|
492
494
|
## Change history
|
|
495
|
+
- **2026-08-29** — Two more worked examples of the **standard-field write grant via hardcoded
|
|
496
|
+
`Core.RecordFields` id literal** (the cross-cluster caveat — a `Client_*`→`Core` JOIN is refused on the
|
|
497
|
+
prod client cluster), each cloning a working sibling: `ItemReceipts.transferOrderId` (Core RF **2153**,
|
|
498
|
+
clone `purchaseOrderId` **85**) and `ItemReceiptItems.transferOrderItemId` (Core RF **2152**, clone
|
|
499
|
+
`purchaseOrderItemId` **471**). Correct + deployed but **dormant** — they only take effect once NYCHH's
|
|
500
|
+
real-`TrnfrOrd` item-receipt path is re-enabled (deferred; see
|
|
501
|
+
[NYCHH TransferOrders import](../../../clients/nychh/features/netsuite-transfer-order-import.md)). Note
|
|
502
|
+
for the team: this recipe was in this doc all along, yet the field-ACL model was reverse-engineered from
|
|
503
|
+
DB/code first this session before the doc was consulted — search the KB first. (jcardinal)
|
|
493
504
|
- **2026-08-28** — Scoped the **third gate**: `Core.AclActions` + `AclActionPermissions` is
|
|
494
505
|
`_Model_Core_Page::meta()` behavior only, and is **dead config in the surface-driven `toga25-supply`
|
|
495
506
|
app** — `_Model_Core_Surface::resolve()` drops an element only when the element itself declares an
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-30
|
|
10
10
|
owners: [tcox, bala, apeterson, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -129,6 +129,13 @@ have **none** — `transfer-orders` (312) had **zero** as of 2026-08-28, so `tra
|
|
|
129
129
|
appears in the record payload no matter what `fields`/`depth` ask for. The client must call the
|
|
130
130
|
**nested route** instead:
|
|
131
131
|
|
|
132
|
+
> **This exact missing row was the ROOT CAUSE of a 1.0 sync bug (2026-08-30).** The NetSuite importer's
|
|
133
|
+
> `syncTransferOrderFromNetsuite` read `$transferOrder->transferOrderItems` for its idempotency lookup;
|
|
134
|
+
> because api2 never inlined that collection, the lookup was **always empty** and every line re-INSERTed
|
|
135
|
+
> each run (TransferOrder 26 grew to 84 duplicate items). The 1.0 fix was a direct
|
|
136
|
+
> `GET /transfer-order-items` query — see
|
|
137
|
+
> [per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
138
|
+
|
|
132
139
|
```
|
|
133
140
|
GET /v2/transfer-orders/{uuid}/transfer-order-items
|
|
134
141
|
```
|
|
@@ -141,6 +148,12 @@ GET /v2/transfer-orders/{uuid}/transfer-order-items
|
|
|
141
148
|
and NetSuite sync — so the blast radius has to be surveyed first. The transfer-order build
|
|
142
149
|
deliberately declined to add one and named the child route in the consuming surface's config
|
|
143
150
|
instead. See [Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
|
|
151
|
+
**Reversed 2026-08-30:** a later dbchanges2 Core migration **did** add the
|
|
152
|
+
`transfer-orders (312) → transfer-order-items (313)` row (route-based lookup + `NOT EXISTS` guard), so
|
|
153
|
+
`GET /transfer-orders` now inlines `transferOrderItems` like its sibling order types — the parity
|
|
154
|
+
alternative to the 1.0 direct-query fix (the developer applied both). **Ordering caveat:** the
|
|
155
|
+
migration must run **after** the duplicate-`TransferOrderItems` cleanup, or the inline expansion
|
|
156
|
+
returns all the dup rows.
|
|
144
157
|
|
|
145
158
|
### ⚠ `join` / `ojoin` drive the WHERE/filter — NOT the response shape
|
|
146
159
|
|
|
@@ -302,6 +315,12 @@ where=(field:op:value,LOGIC,field:op:value,(nested,OR,nested))
|
|
|
302
315
|
write against the DB / `Logs_<Client>.Api`, not the echo. See
|
|
303
316
|
[NYCHH inventory-adjustment → item-fulfillment linking](../../../../clients/nychh/features/netsuite-inventory-adjustment-fulfillment-link.md).
|
|
304
317
|
(jcardinal)
|
|
318
|
+
- 2026-08-30 — Recorded that the missing `transfer-orders` `InherentRecordChildren` row was the **root
|
|
319
|
+
cause** of the 1.0 NetSuite sync duplicating `TransferOrderItems` every run (idempotency read
|
|
320
|
+
`$transferOrder->transferOrderItems`, always empty → re-INSERT; TransferOrder 26 → 84 dups), and that a
|
|
321
|
+
dbchanges2 Core migration **added** the `transfer-orders (312) → transfer-order-items (313)` row (route
|
|
322
|
+
lookup + `NOT EXISTS` guard) for parity — reversing the earlier decision to decline it. Ordering caveat:
|
|
323
|
+
run it **after** the duplicate-TO-item cleanup or the inline expansion returns the dups. (jcardinal)
|
|
305
324
|
- 2026-08-28 — Documented two response-shape facts hit while building the Transfer Orders modal.
|
|
306
325
|
**`calcDepth` defaults to 1**, so a `FIELD_SQL` calculated field on a record one FK hop out returns
|
|
307
326
|
`null` with **no** message (Location's `_address*` fields needed `calcDepth: 2`) — raise it one per
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [NYCHH inventory-adjustment → item-fulfillment linking (direct FK columns, REST)](features/netsuite-inventory-adjustment-fulfillment-link.md) | 1.0 | For NYCHH stock adjustments, the NetSuite **inventory adjustment** carries a link to the **Item Fulfillment** it corrects, on the multi-select body custom field | library/app/api/toga2.php, library/app/api/netsuite/rest.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_hh.php, worker/crons/toga2/netsuite/diagnose_hh_import_inventory_adjustment.php |
|
|
6
|
-
| [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, worker/crons/toga2/netsuite/diagnose_hh_stuck_transfer_order.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, dbchanges2/Client/2026-08-28a - TransferOrderItemsTimestampColumns.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestampColumnsReposition.sql |
|
|
6
|
+
| [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, worker/crons/toga2/netsuite/diagnose_hh_stuck_transfer_order.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, dbchanges2/Client/2026-08-28a - TransferOrderItemsTimestampColumns.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestampColumnsReposition.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql |
|
|
7
7
|
| [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 |
|
|
8
8
|
| [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, _underscore/Model/Client/TransferOrderItem.php, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
9
9
|
| [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, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: nychh
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-30
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
@@ -182,6 +182,14 @@ this feature on the scheduled cron.
|
|
|
182
182
|
|
|
183
183
|
- **Older adjustments have no link.** `custbody_stock_adjustment` was added in NetSuite <1 year ago;
|
|
184
184
|
adjustments predating it will never populate an FK — not a bug.
|
|
185
|
+
- **A NULL `itemFulfillmentId`/`itemFulfillmentItemId` on a NYCHH adjustment is usually CORRECT, not a
|
|
186
|
+
bug.** Only records whose NetSuite multi-select body field `custbody_stock_adjustment` is populated get a
|
|
187
|
+
link — and **0 of 27** NYCHH adjustments carry a value there (only **~1 in 40** recent adjustments
|
|
188
|
+
account-wide do). So a null header/line FK almost always means NetSuite has **no** stock-adjustment link
|
|
189
|
+
on that record, not a code fault. The REST shim's read of `custbody_stock_adjustment` as a **scalar
|
|
190
|
+
SuiteQL column on `transaction`** works when the field IS populated (SuiteQL omits null columns from a
|
|
191
|
+
row, so an empty field just looks "absent"); the shim only builds the link ref when a value exists. (A
|
|
192
|
+
clarifying comment was added at the `customFieldList` build in `rest.php` `fetchInventoryAdjustmentById`.)
|
|
185
193
|
- **Reading the product uuid at `depth=2` silently returns nothing** — the level-3 `item` object is
|
|
186
194
|
omitted at max depth. Use `depth=3` + explicit nested `fields` (see above).
|
|
187
195
|
- **A V2 PUT echo that omits `itemFulfillment` does NOT mean the write failed** — verify against the
|
|
@@ -192,6 +200,15 @@ this feature on the scheduled cron.
|
|
|
192
200
|
|
|
193
201
|
## Change history
|
|
194
202
|
|
|
203
|
+
- 2026-08-30 — Verified that NYCHH adjustments with a NULL `itemFulfillmentId`/`itemFulfillmentItemId`
|
|
204
|
+
are **correct, not a bug**: 0 of 27 NYCHH adjustments populate NetSuite `custbody_stock_adjustment`
|
|
205
|
+
(~1 in 40 account-wide), so most carry no link. Confirmed the shim's scalar SuiteQL read of the field
|
|
206
|
+
works when populated (SuiteQL omits null columns, so an empty field reads as absent) and only builds the
|
|
207
|
+
link ref when a value exists; added a clarifying comment at the `customFieldList` build in `rest.php`
|
|
208
|
+
`fetchInventoryAdjustmentById`. Also: the shared inventory-adjustment sync now **skips NULL-`adjustQtyBy`
|
|
209
|
+
NetSuite serial DETAIL sub-lines** (they repeat the main line with null qty → previously 1048/EV-10) —
|
|
210
|
+
shared fix on the [per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
211
|
+
(jcardinal)
|
|
195
212
|
- 2026-08-29 — **Migrated the feature to direct FK columns over REST, and made it fail loud.**
|
|
196
213
|
Replaced the two bridge tables with direct FK columns
|
|
197
214
|
(`InventoryAdjustments.itemFulfillmentId` reusing `RecordField` 2517/record 154;
|
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: nychh
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-30
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
@@ -18,6 +18,8 @@ files:
|
|
|
18
18
|
- dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql
|
|
19
19
|
- dbchanges2/Client/2026-08-28a - TransferOrderItemsTimestampColumns.sql
|
|
20
20
|
- dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestampColumnsReposition.sql
|
|
21
|
+
- dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql
|
|
22
|
+
- dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql
|
|
21
23
|
related:
|
|
22
24
|
- ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
23
25
|
- ../../../1.0/apps/library/features/toga2-api-client-and-bridge.md
|
|
@@ -160,8 +162,73 @@ that rethrows warnings as a catchable `ErrorException` with exact `file:line`. T
|
|
|
160
162
|
`transferOrderStage` crash after the stale `Logs.Issue` trace misdirected. **When a 1.0 cron freeze's
|
|
161
163
|
`Logs.Issue` trace looks wrong, trust a single-record harness over the aggregated trace.**
|
|
162
164
|
|
|
165
|
+
## Real NetSuite Transfer Order (TrnfrOrd) item receipts — DEFERRED / SKIPPED (2026-08-29)
|
|
166
|
+
|
|
167
|
+
Everything above concerns transfer orders that arrive **as NetSuite sales orders** (the stocking-flag
|
|
168
|
+
routing). A *separate* case is a NetSuite item receipt whose **`createdFrom` is a real
|
|
169
|
+
`TrnfrOrd` record** ("Transfer Order #N"), not a stocking sales order presenting as `'Sales Order'`.
|
|
170
|
+
Importing that path was **attempted this session and INTENTIONALLY DISABLED** — a documented skip in
|
|
171
|
+
`common_sync_togasupply.php`, exactly like the existing Return-Authorization (`RtnAuth`) receipt skip —
|
|
172
|
+
because the feature is **not production-ready**. Do not re-enable it without doing the rebuild below.
|
|
173
|
+
|
|
174
|
+
**Why it was disabled (three blockers):**
|
|
175
|
+
|
|
176
|
+
1. **The shared `syncTransferOrderFromNetsuite` item-sync is NON-IDEMPOTENT.** It appends the TO's items
|
|
177
|
+
every run instead of reconciling — the attempted TO #15 reached **22 duplicate items all `lineNumber`
|
|
178
|
+
1**, and a **pre-existing stocking-SO** transfer order (`Client_Nychh.TransferOrders` id **1** /
|
|
179
|
+
number **62541**) already carries **13 duplicates**. So the duplicate bug is in the **shared** item
|
|
180
|
+
loop and affects **stocking-SO TOs too**, not just the real-TO path (see the shared
|
|
181
|
+
[per-client-sync doc](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)).
|
|
182
|
+
2. **api2 cannot resolve the receipt's `transferOrderItem` reference — `EV-12` "no searchable
|
|
183
|
+
identifiers."** `TransferOrderItems` is **not** set up to be referenced by `(transferOrder,
|
|
184
|
+
lineNumber)` the way `PurchaseOrderItems` is (which is how a receipt line normally links to its
|
|
185
|
+
order line). This needs api2 **reference metadata** making a `TransferOrderItems` identifier
|
|
186
|
+
searchable.
|
|
187
|
+
3. **Receipt-line ↔ TO-line mapping is wrong.** The receipt line's `orderLine` did not match the TO
|
|
188
|
+
item's `lineNumber`, so even past the EV-12 the line link would be incorrect.
|
|
189
|
+
|
|
190
|
+
**Cleanup done:** deleted the TO #15 duplicate artifact (`TransferOrder` 114 + its items).
|
|
191
|
+
|
|
192
|
+
**Inactive scaffolding left STAGED for the rebuild (present but not wired live):**
|
|
193
|
+
|
|
194
|
+
- `App_Api_Netsuite_Rest::fetchTransferOrderById` — real `TrnfrOrd` shim (`source` = location,
|
|
195
|
+
`dest` = `transferLocation`, `ordPicked`/`ordReceived` flags).
|
|
196
|
+
- `syncTransferOrderFromNetsuite($isRealTransferOrderRecord = true)` — dest resolved from the
|
|
197
|
+
`transferLocation` warehouse; stage derived from the picked/received flags.
|
|
198
|
+
- a location → client lookup in the cron.
|
|
199
|
+
- the guarded `transferOrderItem` link in the receipt-item builder.
|
|
200
|
+
|
|
201
|
+
**ACL grants written + deployed this session (correct, but DORMANT until real-TO receipts are
|
|
202
|
+
re-enabled):** two standard-field write grants cloning a working sibling per the
|
|
203
|
+
[ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md) recipe, using
|
|
204
|
+
**hardcoded `Core.RecordFields` id literals** (the cross-cluster caveat — a `Client_*`→`Core` JOIN is
|
|
205
|
+
refused on the prod client cluster):
|
|
206
|
+
|
|
207
|
+
- `dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql` —
|
|
208
|
+
`ItemReceipts.transferOrderId` (Core RecordField **2153**), cloning `purchaseOrderId` (**85**).
|
|
209
|
+
- `dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql` —
|
|
210
|
+
`ItemReceiptItems.transferOrderItemId` (Core RecordField **2152**), cloning `purchaseOrderItemId`
|
|
211
|
+
(**471**).
|
|
212
|
+
|
|
213
|
+
**REBUILD SPEC (all four, in order, before re-enabling + removing the skip):**
|
|
214
|
+
1. Make the shared `syncTransferOrderFromNetsuite` item-sync **idempotent** (reconcile against existing
|
|
215
|
+
`TransferOrderItems` by a stable key) — this also fixes the stocking-SO dups and must **clean the
|
|
216
|
+
pre-existing TO 1** (13 dups). **DONE 2026-08-30:** root cause was api2 never inlining
|
|
217
|
+
`transferOrderItems` (`transfer-orders` has no `InherentRecordChildren` row → idempotency read always
|
|
218
|
+
empty); fixed via the new `getTransferOrderItemsByUuid` direct `GET /transfer-order-items` helper
|
|
219
|
+
(first-wins) — see
|
|
220
|
+
[per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md). Steps 2–4
|
|
221
|
+
still remain, so real-TrnfrOrd receipts stay skipped.
|
|
222
|
+
2. Add api2 **`TransferOrderItems` reference metadata** so a receipt line can resolve its TO item by a
|
|
223
|
+
searchable identifier (the analogue of the `PurchaseOrderItems` `(purchaseOrder, lineNumber)` setup).
|
|
224
|
+
3. **Verify the receipt-line ↔ TO-line mapping** (the `orderLine` ↔ TO-item `lineNumber` correspondence).
|
|
225
|
+
4. Re-enable the path, confirm the (now non-dormant) ACL grants take effect, and **remove the skip**.
|
|
226
|
+
|
|
163
227
|
## Gotchas / known issues
|
|
164
228
|
|
|
229
|
+
- **Real-TrnfrOrd item receipts are disabled — see the DEFERRED section above.** A receipt whose
|
|
230
|
+
`createdFrom` is a real Transfer Order is skipped by design; do not read that as the importer being
|
|
231
|
+
broken.
|
|
165
232
|
- **Order 128829 (no location anywhere) is unresolved** — see the origin-resolution section. Do not
|
|
166
233
|
assume the importer is broken if this single order is stuck; every located order imports.
|
|
167
234
|
- **Routing is gated by the wrapper constant.** If a future client should route transfer orders,
|
|
@@ -174,6 +241,26 @@ that rethrows warnings as a catchable `ErrorException` with exact `file:line`. T
|
|
|
174
241
|
|
|
175
242
|
## Change history
|
|
176
243
|
|
|
244
|
+
- 2026-08-30 — Rebuild-spec **step 1 (make `syncTransferOrderFromNetsuite` idempotent) is DONE**: the
|
|
245
|
+
non-idempotency root cause was api2 never inlining `transferOrderItems` (`transfer-orders` has no
|
|
246
|
+
`InherentRecordChildren` row), so the idempotency read was always empty; fixed via the new
|
|
247
|
+
`getTransferOrderItemsByUuid` direct `GET /transfer-order-items` helper (first-wins). Steps 2–4 (api2
|
|
248
|
+
`TransferOrderItems` reference metadata, receipt-line↔TO-line mapping, re-enable) still remain, so
|
|
249
|
+
real-TrnfrOrd item receipts stay skipped. A dbchanges2 parity migration also added the
|
|
250
|
+
`transfer-orders → transfer-order-items` `InherentRecordChildren` row — see
|
|
251
|
+
[V2 REST query contract](../../../2.0/apps/api2/features/v2-rest-query-contract.md). (jcardinal)
|
|
252
|
+
- 2026-08-29 — Recorded the **real NetSuite `TrnfrOrd` item-receipt feature as DEFERRED/SKIPPED**
|
|
253
|
+
(documented skip in `common_sync_togasupply.php`, like the `RtnAuth` skip) after attempting it and
|
|
254
|
+
hitting three blockers: the shared `syncTransferOrderFromNetsuite` item-sync is **non-idempotent**
|
|
255
|
+
(TO #15 got 22 dup items; pre-existing stocking-SO TO 1/62541 has 13 — shared-loop bug, affects
|
|
256
|
+
stocking-SO TOs too), api2 can't resolve the receipt's `transferOrderItem` reference (`EV-12`, no
|
|
257
|
+
searchable identifier on `TransferOrderItems`), and the receipt-line `orderLine` ≠ TO-item `lineNumber`.
|
|
258
|
+
Deleted the TO #15 dup artifact (TransferOrder 114). Left inactive scaffolding staged
|
|
259
|
+
(`fetchTransferOrderById`, `syncTransferOrderFromNetsuite($isRealTransferOrderRecord=true)`,
|
|
260
|
+
location→client lookup, guarded `transferOrderItem` link). Wrote + deployed (but **dormant**) two ACL
|
|
261
|
+
field-permission grants using hardcoded Core RecordFields literals: `ItemReceipts.transferOrderId`
|
|
262
|
+
(RF 2153, clone `purchaseOrderId` 85) and `ItemReceiptItems.transferOrderItemId` (RF 2152, clone
|
|
263
|
+
`purchaseOrderItemId` 471). Documented the four-step rebuild spec. (jcardinal)
|
|
177
264
|
- 2026-08-28 — Unfroze the `SALES_ORDERS` section (NYCHH + GroWrk) by peeling **three** stacked faults
|
|
178
265
|
on `syncTransferOrderFromNetsuite`: (1) `Undefined property $transferOrderStage` — api2 **omits a
|
|
179
266
|
null FK object** from a depth-2 GET, guarded `transferOrderStage`/`originLocation`/`destinationLocation`
|
|
@@ -40,6 +40,12 @@ This is a team directive. It applies to every session, both frameworks, every re
|
|
|
40
40
|
write, or spawn agents.
|
|
41
41
|
5. **Ordering is unchanged.** The team KB stays the **first** source, ahead of all other
|
|
42
42
|
research — that part was already correct and stays.
|
|
43
|
+
6. **Search the KB before reverse-engineering — mid-session, not just at kickoff.** Before
|
|
44
|
+
reverse-engineering any code, schema, or framework mechanism from the source or the live DB,
|
|
45
|
+
FIRST search the team KB (`node "<TEAM_REPO>/knowledge.js" search --q=…`). The KB is large and
|
|
46
|
+
usually already documents the mechanism — reaching for DB/code archaeology first is the mistake.
|
|
47
|
+
This extends rule 5 (KB is the first source) from kickoff to every point in a session where you
|
|
48
|
+
are about to derive how something works.
|
|
43
49
|
|
|
44
50
|
## How it is enforced
|
|
45
51
|
|
|
@@ -68,6 +74,12 @@ than the old subagent approach did. Mitigate by not re-reading absorbed docs and
|
|
|
68
74
|
only the needed section of a large spec — but priming in the main session is the requirement.
|
|
69
75
|
|
|
70
76
|
## Change history
|
|
77
|
+
- 2026-08-29 — Added rule 6: search the KB before reverse-engineering any code/schema/framework
|
|
78
|
+
mechanism, mid-session and not only at kickoff. Prompted by a NetSuite→TOGa Supply session that
|
|
79
|
+
reverse-engineered the entire 2.0 field-ACL model (AclFieldPermissions, Core.RecordFields,
|
|
80
|
+
EV-9/EV-12, the add-a-writable-field recipe, the cross-cluster hardcoded-literal caveat) from
|
|
81
|
+
DB/code before discovering it was fully documented in
|
|
82
|
+
`2.0/apps/_underscore/features/acl-permission-chain.md` all along. (jcardinal)
|
|
71
83
|
- 2026-08-13 — Standard recorded. Kickoff priming must run in the main session, synchronously,
|
|
72
84
|
and no subagent may start until priming is complete; released by the new
|
|
73
85
|
`knowledge.js kickoff-primed` sentinel. Retired the `context-primer` subagent from the
|
package/package.json
CHANGED