toga-ai 1.0.376 → 1.0.377
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/library/INDEX.md +1 -0
- package/knowledge/1.0/apps/library/features/netsuite-item-isfulfillable-sync.md +102 -0
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +6 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/fulfillable-item-propagation.md +126 -0
- package/knowledge/2.0/apps/_underscore/features/recursive-item-fulfillments.md +3 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/compass-usa/profile.md +7 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
|
|
10
10
|
| [Branded HTML Email Templates (App_Email_Template)](features/email-templates.md) | `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in the 1.0 (`App_`) framework. | library/app/email/template.php, library/app/email/agilant.php |
|
|
11
11
|
| [1.0 MVC Page Pattern & New-App Skeleton](features/mvc-page-pattern-and-app-skeleton.md) | This is the **reusable recipe for standing up a new 1.0 (`App_`) application** and for adding pages to one — the folder-based MVC routing, the page lifecycle, t | library/app/framework.php, library/app/frameworkindex.php, library/app/mvc.php, library/app/database.php, library/app/model.php, library/app/config.php |
|
|
12
|
+
| [isFulfillable from NetSuite during Item Sync (Phase 1)](features/netsuite-item-isfulfillable-sync.md) | This is the **1.0 (Phase 1)** half of the `isFulfillable` feature: reading the NetSuite `isfulfillable` flag during item sync and stamping it onto the **Agilant | library/app/netsuite.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php |
|
|
12
13
|
| [NetSuite SuiteQL/REST API Reference](features/netsuite-suiteql-api-reference.md) | General working reference for the Agilant NetSuite integration: how to authenticate, how SuiteQL behaves, and the confirmed schema of the tables/columns/codes w | library/app/api/netsuite/rest.php, library/ssl/netsuite_ec_key.pem, test/@dave/Junk Drawer/nsq.php |
|
|
13
14
|
| [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php |
|
|
14
15
|
| [NetSuite Sync Alert Monitor (App_SystemMonitor_NetSuiteIntegration)](features/netsuite-sync-alert-monitor.md) | `App_SystemMonitor_NetSuiteIntegration` (`library/app/systemmonitor/netsuiteintegration.php`, title **"NetSuite Sync Alert"**) is a 1.0 system monitor that watc | library/app/systemmonitor/netsuiteintegration.php, worker/crons/infrastructure/system_monitors.php |
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: isFulfillable from NetSuite during Item Sync (Phase 1)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: library
|
|
5
|
+
project: Library
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-20
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- library/app/netsuite.php
|
|
13
|
+
- library/app/api/toga2.php
|
|
14
|
+
- worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php
|
|
15
|
+
related:
|
|
16
|
+
- toga2-api-client-and-bridge.md
|
|
17
|
+
- ../../worker/features/netsuite-togasupply-per-client-sync.md
|
|
18
|
+
- ../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
This is the **1.0 (Phase 1)** half of the `isFulfillable` feature: reading the NetSuite
|
|
23
|
+
`isfulfillable` flag during item sync and stamping it onto the **Agilant-catalog (source/supplier)
|
|
24
|
+
item** in the 2.0 platform. The client-facing copy is reached separately by the
|
|
25
|
+
[2.0 propagation engine](../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md),
|
|
26
|
+
which walks the supply chain up from the source item. Only the source item is stamped here.
|
|
27
|
+
|
|
28
|
+
## Key files / entry points
|
|
29
|
+
- `library/app/netsuite.php` — `App_NetSuite::getItemIsFulfillable($itemInternalId): bool|null`.
|
|
30
|
+
A SOAP `ItemSearchBasic` by internal id; returns the boolean (or `null` when unknown). Mirrors
|
|
31
|
+
the existing `getItemIsSerialized` exactly — the only prior item flag the sync read.
|
|
32
|
+
- `library/app/api/toga2.php` — `getCreateItem` (~line 4364). During item create/update the sync
|
|
33
|
+
now reads `$isFulfillableItem = App_NetSuite::getItemIsFulfillable($nsItemInternalId)` and, when
|
|
34
|
+
**non-null**, stamps `$payload['isFulfillable']` on the create/update PUT|POST to api2. The
|
|
35
|
+
NetSuite internal id is resolved from the order line or from the part number
|
|
36
|
+
(`getItemInternalIdFromPartNumber` / `getItemGroupInternalIdFromPartNumber`). Stores
|
|
37
|
+
`c_netsuiteInternalItemId` on the item.
|
|
38
|
+
- `worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php` — one-time backfill (see below).
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
1. Item sync resolves the NetSuite internal id for the item (order line, else part-number lookup).
|
|
42
|
+
2. `getItemIsFulfillable` SOAP-searches the item and returns the flag.
|
|
43
|
+
3. If non-null, `getCreateItem` adds `isFulfillable` to the item PUT/POST payload to api2. api2's
|
|
44
|
+
Items interceptor then propagates it up-chain (Phase 2).
|
|
45
|
+
4. Going forward the **daily sync** handles new items automatically; existing items are caught up
|
|
46
|
+
by the one-time backfill.
|
|
47
|
+
|
|
48
|
+
### One-time backfill (July 5+ orders)
|
|
49
|
+
`backfill_isfulfillable_jul5.php` catches up Agilant items on SalesOrders since **2026-07-05**:
|
|
50
|
+
- It **PUTs each source item to `/items`** via `App_Api_Toga2::send` — the *same* call the sync
|
|
51
|
+
makes — so **api2's interceptor performs the recursion**. It deliberately does **NOT** write the
|
|
52
|
+
DB directly and does **NOT** walk the chain itself. (Design choice: reuse the one propagation
|
|
53
|
+
path so backfill and steady-state behave identically.)
|
|
54
|
+
- Reads the value from NetSuite **by part number**, fulfillable-if-any (`max` across matching
|
|
55
|
+
internal ids).
|
|
56
|
+
- Env-agnostic: `App_Registry::get('config')` with `DB_HOST_PREFERENCE` = `localhost` (local) /
|
|
57
|
+
`writer.client` (prod); `DRY_RUN` flips true→false. Compass credentials are the same source as
|
|
58
|
+
`sync_togasupply_compass_usa.php`.
|
|
59
|
+
|
|
60
|
+
## Key gotcha — what NetSuite `isfulfillable` actually means
|
|
61
|
+
NetSuite `isfulfillable` (SuiteQL `SELECT isfulfillable FROM item`) is **`T` for InvtPart AND
|
|
62
|
+
NonInvtPart (services)**; it is `F` **only** for Group/bundle headers. It does **NOT** distinguish
|
|
63
|
+
physical goods from services — `itemtype` would, but the sync has never used `itemtype` (only
|
|
64
|
+
`isSerialized`), so this feature intentionally reflects NetSuite's flag **as-is**. **Net effect:
|
|
65
|
+
nearly everything resolves to `fulfillable = 1`.**
|
|
66
|
+
|
|
67
|
+
- **Open product decision (caveat):** because services come back fulfillable, the storefront makes
|
|
68
|
+
everything clickable. Whether that is intended vs. needing `itemtype` to hide services is
|
|
69
|
+
**pending product confirmation**.
|
|
70
|
+
|
|
71
|
+
## Verification performed
|
|
72
|
+
- **Prod read-only dry-run:** 20 reachable Compass items; chain walk correct
|
|
73
|
+
(Agilant `1989 B4NY9UC#ABA` → Compass `1986 B4NY9UC-2`); ~91% of July-5+ order lines reach a
|
|
74
|
+
Compass item, all landing on the correct Compass copy.
|
|
75
|
+
- **Local live backfill:** 85 Agilant PUTs → interceptor → 20 distinct Compass items set (all `=1`),
|
|
76
|
+
each verified to trace through an Agilant source.
|
|
77
|
+
|
|
78
|
+
## Gotchas / known issues
|
|
79
|
+
- **Value stamped only on the Agilant source item here** — the client-facing copy is set by the 2.0
|
|
80
|
+
interceptor. If the interceptor rows aren't deployed in the target env, api2 **403s the whole item
|
|
81
|
+
write** on the unknown `isFulfillable` field (see the Phase-2 doc's deploy gotchas).
|
|
82
|
+
- **Backfill fires the api2 path, not direct SQL.** It relies on the Phase-2 interceptor being live
|
|
83
|
+
in the env it targets; running it against an env without the interceptor rows will 403 every PUT.
|
|
84
|
+
- **🔐 Credentials:** the backfill's Compass client/api uuids come from config; the API secret lives
|
|
85
|
+
in `Client_Compass.Apis` (name `Agilant`) — never reproduce the secret value.
|
|
86
|
+
|
|
87
|
+
## Change history
|
|
88
|
+
- 2026-07-20 — Added `App_NetSuite::getItemIsFulfillable` (SOAP `ItemSearchBasic`, mirrors
|
|
89
|
+
`getItemIsSerialized`) and wired `getCreateItem` to stamp `isFulfillable` on the item PUT/POST to
|
|
90
|
+
api2 when non-null (internal id from order line or part-number lookup; stores
|
|
91
|
+
`c_netsuiteInternalItemId`). Added the one-time `backfill_isfulfillable_jul5.php` cron, which
|
|
92
|
+
re-PUTs Agilant source items so the api2 interceptor does the recursion (no direct DB writes).
|
|
93
|
+
Documented the NetSuite `isfulfillable` semantics gotcha (T for InvtPart + services, F only for
|
|
94
|
+
group headers — does not distinguish service vs physical) and the resulting open product decision.
|
|
95
|
+
(bala)
|
|
96
|
+
|
|
97
|
+
## Related docs
|
|
98
|
+
- [App_Api_Toga2 — API Client & 1.0↔2.0 Bridge](toga2-api-client-and-bridge.md) — the transport
|
|
99
|
+
`getCreateItem`/the backfill use to reach api2.
|
|
100
|
+
- [NetSuite → TOGa Supply Per-Client Sync](../../worker/features/netsuite-togasupply-per-client-sync.md)
|
|
101
|
+
— the sync engine this item flag rides in.
|
|
102
|
+
- [Phase 2 — isFulfillable propagation up the SO↔PO chain (2.0)](../../../2.0/apps/_underscore/features/fulfillable-item-propagation.md).
|
|
@@ -198,6 +198,12 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
198
198
|
|
|
199
199
|
## Change history
|
|
200
200
|
|
|
201
|
+
- 2026-07-20 — Fixed a pre-existing **missing `try`-close / brace bug** in
|
|
202
|
+
`common_sync_togasupply.php` (the inventory-adjustments and item-fulfillments blocks lacked a
|
|
203
|
+
`try` close, a parse error that broke the Compass sync run). Same failure class as the 2026-07-09
|
|
204
|
+
tier-wide `rest.php` outage — a dropped token in this shared engine crashes the run before records
|
|
205
|
+
process. (Fixed incidentally while shipping the `isFulfillable` item-sync feature; see the
|
|
206
|
+
[library Phase-1 doc](../../library/features/netsuite-item-isfulfillable-sync.md).) (bala)
|
|
201
207
|
- 2026-07-10 — Added **Quad + Growrk** to the NetSuite Sync Alert monitor's
|
|
202
208
|
`CLIENT_CONFIGURATIONS` (previously unmonitored); **AIG deliberately left off** (held
|
|
203
209
|
account, checkpoints frozen at the 2026-04-28 seed). Recorded a real, ongoing IF/InvAdj
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
| [Error Reporting — Issue/Event Aggregation (agreed POST-to-receiver design)](features/error-reporting-issue-event.md) | Platform-wide error-reporting infrastructure for TOGA 2.0, built around a two-table **Issue / Event** aggregation model in the shared **Core Logs DB**. | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
|
|
17
17
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
18
18
|
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/probe_je_shape.php, test/@dave/fixer.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
19
|
+
| [isFulfillable Propagation Up the SO↔PO Chain](features/fulfillable-item-propagation.md) | `Items.isFulfillable` is a boolean that gates whether a storefront line's **Qty Fulfilled** cell is actionable. | _underscore/Model/Client/Item.php, _underscore/Model/Compass/Item.php, dbchanges2/Core/2026-07-17 - Items isFulfillable RecordField.sql, dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql, dbchanges2/Client/2026-07-17 - ItemsisFulfillable.sql |
|
|
19
20
|
| [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Every ItemFulfillment (IF) now carries an explicit **stage** — picked → packed → shipped — resolved through `ItemFulfillmentStages → ItemFulfillmentStatuses` (m | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderStatus.php, _underscore/Model/Client/SalesOrderItem.php, _underscore/Model/Client/Item.php, _underscore/Model/Client/PurchaseOrderItem.php, library/app/api/toga2.php, dbchanges2/Client/2026-06-30a - BackfillNullStageItemFulfillmentsToShipped.sql, dbchanges2/Client/2026-06-30b - SalesOrderStatusesPickedPacked.sql, dbchanges2/Client/2026-06-30c - ItemFulfillmentStageIdNotNull.sql, dbchanges2/Client_CompassCanada/2026-06-30a - ItemFulfillmentLifecycleAndShippedBackfill.sql |
|
|
20
21
|
| [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php |
|
|
21
22
|
| [_Model::save() vs raw _Query — no atomic conditional update](features/model-save-vs-query-atomic-update.md) | `_Model::save()` is a plain load-then-write ORM primitive and **cannot express an atomic conditional update** (an optimistic-concurrency / row-claim guard such | _underscore/Model.php, _underscore/Query.php |
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: isFulfillable Propagation Up the SO↔PO Chain
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-20
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/Item.php
|
|
13
|
+
- _underscore/Model/Compass/Item.php
|
|
14
|
+
- dbchanges2/Core/2026-07-17 - Items isFulfillable RecordField.sql
|
|
15
|
+
- dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql
|
|
16
|
+
- dbchanges2/Client/2026-07-17 - ItemsisFulfillable.sql
|
|
17
|
+
related:
|
|
18
|
+
- recursive-item-fulfillments.md
|
|
19
|
+
- ../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md
|
|
20
|
+
- ../../../clients/compass-usa/profile.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
`Items.isFulfillable` is a boolean that gates whether a storefront line's **Qty Fulfilled**
|
|
25
|
+
cell is actionable. The value is **born on the Agilant-catalog (source/supplier) item** during
|
|
26
|
+
the 1.0 NetSuite item sync (see the [Phase 1 doc](../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md)),
|
|
27
|
+
but the toga2-supply storefront shows the **client's own catalog row** — a *different* `Items`
|
|
28
|
+
row linked to the source item only through the multi-tier supply chain. This feature is the
|
|
29
|
+
**2.0 (Phase 2)** half: an api2 interceptor that, whenever an item write carries `isFulfillable`,
|
|
30
|
+
walks the `SalesOrderItem`↔`PurchaseOrderItem` chain **upward** and stamps the same value onto
|
|
31
|
+
every client-facing item it reaches. Value flows source → client copy so the storefront gate is
|
|
32
|
+
correct on the row the customer actually sees.
|
|
33
|
+
|
|
34
|
+
This is a sibling of the [Recursive Item Fulfillments](recursive-item-fulfillments.md) engine —
|
|
35
|
+
same bridge topology, walked for a different payload (a scalar flag rather than fulfillment rows).
|
|
36
|
+
|
|
37
|
+
## Key files / entry points
|
|
38
|
+
- `_underscore/Model/Client/Item.php`
|
|
39
|
+
- `propagateFulfillableAcrossChain(string $itemUuid)` (~lines 497–565) — the engine.
|
|
40
|
+
- `propagateFulfillableWhenPayloadCarriesTheFlag($api)` — the interceptor body, registered on
|
|
41
|
+
`postPost` and `postPut` for Items. **Gated:** `if (!isset($api->httpPayload->isFulfillable)) return;`
|
|
42
|
+
so an ordinary item edit that doesn't touch the flag never triggers a chain walk.
|
|
43
|
+
- `_underscore/Model/Compass/Item.php` — Compass's Item subclass **must call
|
|
44
|
+
`parent::postPost` / `parent::postPut`** so the base propagation isn't shadowed by the Compass
|
|
45
|
+
price-override override. (Same empty-pass-through pattern the recursive-IF engine relies on.)
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
1. **Interceptor gate.** `postPost`/`postPut` call `propagateFulfillableWhenPayloadCarriesTheFlag`,
|
|
49
|
+
which returns immediately unless the payload carried `isFulfillable`. Only the sync (and the
|
|
50
|
+
one-time backfill) send that field, so day-to-day item edits skip the walk entirely.
|
|
51
|
+
2. **Load the source item** by uuid → `id` + `isFulfillable`. If the value is **NULL**, return
|
|
52
|
+
(nothing authoritative to propagate).
|
|
53
|
+
3. **Walk the chain UP** with a `WITH RECURSIVE ChainSalesOrderItems` CTE:
|
|
54
|
+
- **Anchor:** `SalesOrderItems WHERE itemId = <sourceItemId>`.
|
|
55
|
+
- **Recursive step:** join `PurchaseOrderItems_SalesOrderItems` (PSO) then
|
|
56
|
+
`SalesOrderItems_PurchaseOrderItems` (SPO) to climb one tier
|
|
57
|
+
(Agilant SOI → PSO → POI → SPO → next SOI). `UNION` (not `UNION ALL`) stops loops.
|
|
58
|
+
- **Result:** `SELECT DISTINCT LinkedSalesOrderItems.itemId WHERE itemId <> sourceItemId` —
|
|
59
|
+
every client-facing item id reachable above the source.
|
|
60
|
+
4. **Diff-only update:**
|
|
61
|
+
`UPDATE Items SET isFulfillable = <value> WHERE id IN (...) AND (isFulfillable <> <value> OR isFulfillable IS NULL)`.
|
|
62
|
+
Re-runs are no-ops, so repeated syncs / a re-run backfill are safe.
|
|
63
|
+
|
|
64
|
+
## Chain topology (VERIFIED in prod)
|
|
65
|
+
- **Agilant items sit on the `PSO.salesOrderItemId` side; Compass (client-facing) items sit on
|
|
66
|
+
the `SPO.salesOrderItemId` side.** The value born on the Agilant *source* item therefore flows
|
|
67
|
+
**up** to the Compass item the storefront renders.
|
|
68
|
+
- Same four bridge tables as the recursive-IF engine (`PurchaseOrders_SalesOrders`,
|
|
69
|
+
`SalesOrders_PurchaseOrders`, `PurchaseOrderItems_SalesOrderItems`,
|
|
70
|
+
`SalesOrderItems_PurchaseOrderItems`), so a **broken/missing SOI↔POI bridge row breaks this walk
|
|
71
|
+
too** — the same integrity class as the Compass off-by-one bridge bugs (see
|
|
72
|
+
[MITS PO → SO Item Linking](../../../clients/compass-usa/features/mits-po-to-so-item-linking.md)).
|
|
73
|
+
|
|
74
|
+
## Data model / schema (deploy)
|
|
75
|
+
Three `dbchanges2` migrations register the field + hooks; **all must be present in the target
|
|
76
|
+
env or the flag write is rejected:**
|
|
77
|
+
1. **Core RecordField** (`Core/2026-07-17 - Items isFulfillable RecordField.sql`) — recordId **21**
|
|
78
|
+
(Items), field `isFulfillable`, BOOLEAN.
|
|
79
|
+
2. **Core `ApiPayloadInterceptors`** (`Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql`)
|
|
80
|
+
— rows for recordId 21, **POST/POST** and **POST/PUT**, global (clientId/apiId null).
|
|
81
|
+
3. **Client column + ACL** (`Client/2026-07-17 - ItemsisFulfillable.sql`) — `Items.isFulfillable`
|
|
82
|
+
column + ACL rows.
|
|
83
|
+
|
|
84
|
+
## Conflict rule
|
|
85
|
+
Multi-source kits (an item reachable from more than one source) use **LAST-WRITER-WINS**. A
|
|
86
|
+
deterministic `MIN`/`MAX` aggregate was **deliberately deferred** — harmless while values are
|
|
87
|
+
uniformly `1` (see the Phase-1 gotcha: NetSuite flags nearly everything fulfillable).
|
|
88
|
+
|
|
89
|
+
## Client variations
|
|
90
|
+
- **Compass** (USA/Canada, DB `Client_Compass`) is the built/verified client. Its `_Model_Compass_Item`
|
|
91
|
+
overrides `postPost`/`postPut` for a price override, so it **must** `parent::` up to the base or
|
|
92
|
+
propagation silently never fires.
|
|
93
|
+
- Engine itself is shared `_Model_Client_Item` logic; any client with the schema present gets it.
|
|
94
|
+
|
|
95
|
+
## Gotchas / known issues
|
|
96
|
+
- **⚠ Interceptors are DB-driven.** The `postPost`/`postPut` PHP does nothing without the
|
|
97
|
+
`ApiPayloadInterceptors` rows in the **target env's Core DB**. Missing rows → the hook never
|
|
98
|
+
fires **and** api2 EV-8's the unknown `isFulfillable` field, **403-ing the whole request** (not
|
|
99
|
+
just dropping the field). This is the same interceptor-row requirement as the recursive-IF engine.
|
|
100
|
+
- **⚠ Cross-cluster Core/client on prod.** On prod, Core and client DBs are on **separate
|
|
101
|
+
clusters**, so the client ACL migration's cross-DB `Core.RecordFields` uuid lookup must be
|
|
102
|
+
**resolved/substituted at deploy** — it cannot join across clusters at runtime.
|
|
103
|
+
- **⚠ Compass subclass shadowing.** If a future client subclass overrides Items `postPost`/`postPut`
|
|
104
|
+
without calling `parent::`, propagation silently stops for that client.
|
|
105
|
+
- **Product decision unresolved (caveat, not a bug).** Because NetSuite's `isfulfillable` is `T`
|
|
106
|
+
for services as well as physical goods (see Phase-1 doc), propagation makes **everything**
|
|
107
|
+
clickable on the storefront. Whether "reflect NetSuite's flag as-is" is the intended behavior —
|
|
108
|
+
vs. using `itemtype` to hide services — is **pending product confirmation**.
|
|
109
|
+
|
|
110
|
+
## Change history
|
|
111
|
+
- 2026-07-20 — Built the isFulfillable up-chain propagation engine:
|
|
112
|
+
`propagateFulfillableAcrossChain` + the payload-gated `postPost`/`postPut` interceptor on Items
|
|
113
|
+
(recursive SOI↔POI CTE, diff-only `UPDATE`), plus the Compass `parent::` fix so the price-override
|
|
114
|
+
subclass doesn't shadow it. Registered the field + interceptors via three `dbchanges2` migrations
|
|
115
|
+
(Core RecordField 21, Core POST/POST + POST/PUT interceptor rows, Client column + ACL). Verified
|
|
116
|
+
in prod (Agilant `1989 B4NY9UC#ABA` → Compass `1986 B4NY9UC-2`; 20 reachable Compass items) and via
|
|
117
|
+
a local live backfill (85 Agilant PUTs → 20 distinct Compass items set). Last-writer conflict rule
|
|
118
|
+
for multi-source kits (MIN/MAX aggregate deferred). (bala)
|
|
119
|
+
|
|
120
|
+
## Related docs
|
|
121
|
+
- [Phase 1 — isFulfillable from NetSuite during item sync (1.0)](../../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md)
|
|
122
|
+
— where the value is sourced + the one-time backfill.
|
|
123
|
+
- [Recursive Item Fulfillments](recursive-item-fulfillments.md) — the sibling engine over the same
|
|
124
|
+
bridge topology.
|
|
125
|
+
- [Compass MITS PO → SO Item Linking](../../../clients/compass-usa/features/mits-po-to-so-item-linking.md)
|
|
126
|
+
— how the SOI↔POI bridges get built (and the integrity bugs that break this walk).
|
|
@@ -143,6 +143,9 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
|
|
|
143
143
|
- 2026-06-08 — Documented the Recursive Item Fulfillments engine (interceptor-driven upstream fulfillment mirroring, bundle scaling, reconcile loop). (jcardinal)
|
|
144
144
|
|
|
145
145
|
## Related docs
|
|
146
|
+
- [isFulfillable Propagation Up the SO↔PO Chain](fulfillable-item-propagation.md) — a sibling
|
|
147
|
+
engine that walks the **same** bridge topology to propagate a scalar item flag (rather than
|
|
148
|
+
fulfillment rows) from the source item up to the client-facing copy.
|
|
146
149
|
- GroWrk transfer order flow plan: `clients/growrk/features/transfer-order-flow.md`.
|
|
147
150
|
- `_underscore` architecture (interceptors, `internalApiRequest`, `_Model` layer).
|
|
148
151
|
- `api2` architecture (V2 metadata engine that fires these interceptors; the
|
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)_ — 13 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
8
|
- **worker** (Worker) — 15 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
|
10
10
|
- **togadesk** (TOGa Desk) — 9 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
|
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
20
|
+
- **_underscore** (_Underscore) _(framework core)_ — 35 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 30 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
22
|
- **api2** (API) — 12 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -11,6 +11,7 @@ apps:
|
|
|
11
11
|
- worker1.5
|
|
12
12
|
- worker2
|
|
13
13
|
- dbchanges2
|
|
14
|
+
- library
|
|
14
15
|
project: _Underscore
|
|
15
16
|
client: compass-usa
|
|
16
17
|
type: profile
|
|
@@ -88,4 +89,10 @@ separate, related client (see its own profile).
|
|
|
88
89
|
order. Contrast Quad, which gets the full picked/packed machine. See
|
|
89
90
|
[IF stage lifecycle & order status](../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
|
|
90
91
|
- Built on the shared 2.0 Recursive Item Fulfillments engine (upstream mirroring).
|
|
92
|
+
- **`Items.isFulfillable` gates the storefront Qty Fulfilled cell (2026-07).** Sourced from
|
|
93
|
+
NetSuite onto the Agilant source item during the 1.0 item sync
|
|
94
|
+
([Phase 1](../../1.0/apps/library/features/netsuite-item-isfulfillable-sync.md)) and propagated
|
|
95
|
+
up the SO↔PO chain to the client-facing Compass item by an api2 interceptor
|
|
96
|
+
([Phase 2](../../2.0/apps/_underscore/features/fulfillable-item-propagation.md)). Verified on prod
|
|
97
|
+
Compass chains; a one-time July-5+ backfill re-PUTs source items through the same path.
|
|
91
98
|
- This profile is a starting point; expand as more Compass-specific behavior is captured.
|
package/package.json
CHANGED