toga-ai 1.0.156 → 1.0.158
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.
|
@@ -8,3 +8,4 @@
|
|
|
8
8
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
|
|
9
9
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql |
|
|
10
10
|
| [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
|
|
11
|
+
| [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Units for Items for Purchase Orders — Data Structure
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-22
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files: []
|
|
12
|
+
related: []
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Summary
|
|
16
|
+
Describes how unit (serialized inventory) data is linked to sales-order and purchase-order
|
|
17
|
+
line items behind the `units-for-items-for-purchase-orders` TableView. A unit only becomes
|
|
18
|
+
visible in this view if it can be walked from the sales-order item out to a unit record. There
|
|
19
|
+
are **two independent linkage paths** in the schema, and the view traverses only one of them —
|
|
20
|
+
which is the root cause of "units not showing" cases.
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
A `Unit` can be tied to an order line through either of two chains:
|
|
24
|
+
|
|
25
|
+
**1. Receipt path (what this view follows):**
|
|
26
|
+
```
|
|
27
|
+
SalesOrderItems
|
|
28
|
+
-> SalesOrderItems_PurchaseOrderItems (item-level bridge: salesOrderItemId, purchaseOrderItemId)
|
|
29
|
+
-> PurchaseOrderItems (PurchaseOrderItems.id = bridge.purchaseOrderItemId)
|
|
30
|
+
-> ItemReceiptItems (ItemReceiptItems.purchaseOrderItemId = PurchaseOrderItems.id)
|
|
31
|
+
-> ItemReceiptItemUnits (ItemReceiptItemUnits.itemReceiptItemId = ItemReceiptItems.id)
|
|
32
|
+
-> Units (ItemReceiptItemUnits.unitId)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**2. Fulfillment path (direct; NOT followed by this view):**
|
|
36
|
+
```
|
|
37
|
+
SalesOrderItems
|
|
38
|
+
-> ItemFulfillmentItems (ItemFulfillmentItems.salesOrderItemId = SalesOrderItems.id)
|
|
39
|
+
-> ItemFulfillmentItemUnits (ItemFulfillmentItemUnits.itemFulfillmentItemId = ItemFulfillmentItems.id)
|
|
40
|
+
-> Units (ItemFulfillmentItemUnits.unitId)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The receipt path requires both the item-level bridge **and** an item receipt against the
|
|
44
|
+
bridged purchase-order line. The fulfillment path reaches `SalesOrderItems` directly by
|
|
45
|
+
`salesOrderItemId` with no purchase order or bridge involved.
|
|
46
|
+
|
|
47
|
+
## Data model
|
|
48
|
+
Header-level vs item-level bridges, and the level each transaction is anchored at:
|
|
49
|
+
|
|
50
|
+
- **`PurchaseOrders_SalesOrders`** — *header* bridge: links a customer PO to a sales order
|
|
51
|
+
(`purchaseOrderId`, `salesOrderId`).
|
|
52
|
+
- **`SalesOrderItems_PurchaseOrderItems`** — *item* bridge: links a sales-order line to a
|
|
53
|
+
vendor purchase-order line (`salesOrderItemId`, `purchaseOrderItemId`). This is the bridge
|
|
54
|
+
the receipt path depends on.
|
|
55
|
+
- **Item receipts are PO-anchored only**: `ItemReceipts.purchaseOrderId`,
|
|
56
|
+
`ItemReceiptItems.purchaseOrderItemId` — there is **no sales-order FK on a receipt**. The
|
|
57
|
+
only way a received unit reaches a sales order is through the item bridge.
|
|
58
|
+
- **Item fulfillments are SO-anchored**: `ItemFulfillments.salesOrderId`,
|
|
59
|
+
`ItemFulfillmentItems.salesOrderItemId`.
|
|
60
|
+
- Both customer POs and vendor POs live in the same `PurchaseOrders` table, distinguished by
|
|
61
|
+
which bridge references them (header bridge = customer PO; item bridge via
|
|
62
|
+
`PurchaseOrderItems` = vendor PO).
|
|
63
|
+
- `Items.inventoryType` governs whether units exist at all (see Gotchas).
|
|
64
|
+
|
|
65
|
+
## Client variations
|
|
66
|
+
None — uniform. The linkage structure is framework-level and identical across clients.
|
|
67
|
+
|
|
68
|
+
## Gotchas / known issues
|
|
69
|
+
- **The view only walks the receipt path.** A line whose units exist solely via the
|
|
70
|
+
fulfillment path (shipped from stock, never received against the bridged PO line) shows
|
|
71
|
+
**0 units** even though units physically exist. No data backfill can surface these — only a
|
|
72
|
+
resolver/view change that also walks the fulfillment path will.
|
|
73
|
+
- **Missing item bridge → no units**, even when units were received. Without a
|
|
74
|
+
`SalesOrderItems_PurchaseOrderItems` row, the receipt path has no starting link.
|
|
75
|
+
- **A present bridge is not sufficient.** If the bridged PO line has no
|
|
76
|
+
`ItemReceiptItemUnits`, the receipt path still resolves to nothing.
|
|
77
|
+
- **`inventoryType` determines unit existence**: `SERIALIZED` and `HYBRID` items can produce
|
|
78
|
+
unit records; `NON_SERIALIZED` (bulk/consumable) never do. `HYBRID` items fulfilled as a
|
|
79
|
+
plain quantity (no serial capture) also produce no units — legitimately blank.
|
|
80
|
+
- **Receipt-path counts are PO-line-wide.** Units surfaced reflect everything received against
|
|
81
|
+
the bridged PO line, which may exceed or differ from what was actually fulfilled to a given
|
|
82
|
+
sales order (over/under-attribution), because attribution is by PO line, not by the
|
|
83
|
+
fulfillment that shipped the unit.
|
|
84
|
+
|
|
85
|
+
## Change history
|
|
86
|
+
- 2026-06-22 — Documented the two-path units linkage model behind the
|
|
87
|
+
units-for-items-for-purchase-orders view and why the receipt-only traversal hides
|
|
88
|
+
fulfillment-only units. (bala)
|
|
89
|
+
|
|
90
|
+
## Related docs
|
|
91
|
+
- [[recursive-item-fulfillments]]
|
|
92
|
+
- [[tracking-number-bridges]]
|
package/knowledge/INDEX.md
CHANGED
|
@@ -14,7 +14,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
14
14
|
|
|
15
15
|
## 2.0 framework
|
|
16
16
|
|
|
17
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
17
|
+
- **_underscore** (_Underscore) _(framework core)_ — 10 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
18
18
|
- **worker2** (Worker) — 8 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
19
19
|
- **api2** (API) — 4 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
20
20
|
- **dbchanges2** (Database Changes) _(framework core)_ — 1 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: compass-canada
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-06-
|
|
8
|
+
updated: 2026-06-22
|
|
9
9
|
owners: ["bala"]
|
|
10
10
|
files:
|
|
11
11
|
- worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php
|
|
@@ -28,8 +28,9 @@ flow, adapted for Canadian carriers and bilingual email.
|
|
|
28
28
|
## Key files / entry points
|
|
29
29
|
- `worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php`
|
|
30
30
|
— the scheduled cron (1.0 worker tier). Reads mailbox `compasscanada.status@togatech.com`
|
|
31
|
-
(OAuth2 creds
|
|
32
|
-
`/advance-shipping-notices`.
|
|
31
|
+
(OAuth2 creds — currently hardcoded with a TODO; must move to `App_Registry::get('config')['compasscanada']['gt_asn_oauth2']`
|
|
32
|
+
before production), parses the 24-column CSV, posts to api2 `/advance-shipping-notices`.
|
|
33
|
+
Scheduled in `worker/schedules/cron.worker.sync.json` (every 4h).
|
|
33
34
|
- `_underscore/Model/Compass/AdvanceShippingNotice.php` — `postPost` interceptor that builds the
|
|
34
35
|
ItemFulfillment chain on each ASN POST. Compass Canada inherits it via the empty
|
|
35
36
|
`_underscore/Model/Compass/Canada/AdvanceShippingNotice.php`.
|
|
@@ -38,21 +39,27 @@ flow, adapted for Canadian carriers and bilingual email.
|
|
|
38
39
|
user emails/notifications, and stamps `c_dtInTransitEmailSent = NOW()`.
|
|
39
40
|
|
|
40
41
|
## How it works
|
|
41
|
-
1. Cron gets an O365 OAuth2 token (creds
|
|
42
|
+
1. Cron gets an O365 OAuth2 token (creds currently hardcoded; see Gotchas for migration path),
|
|
42
43
|
opens the INBOX.
|
|
43
44
|
2. Per attachment: decode CSV, gate on exactly 24 columns (otherwise email the team the file +
|
|
44
|
-
skip).
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
skip). Uses `preg_split('/\r?\n/', ...)` on the first line so Windows CRLF attachments parse
|
|
46
|
+
correctly.
|
|
47
|
+
3. **Before the row loop** — build three lookup maps once per file: `$purchaseOrderUuidByNumber`
|
|
48
|
+
(`SELECT number, uuid FROM PurchaseOrders`), `$shippingMethodByCarrierId`, `$carrierIdByName`.
|
|
49
|
+
4. Per row: validate PO / part / tracking (early `continue` with error recording on missing
|
|
50
|
+
critical fields); look up SO + contact user + CC addresses; resolve carrier →
|
|
51
|
+
Ground shipping method (falls back to `DEFAULT_GROUND_SHIPPING_METHOD_ID = 3`); upsert
|
|
52
|
+
`TrackingNumbers`; resolve item by `Items.partNumber` joined through `VendorItems`; find-or-create
|
|
53
|
+
a `Units` row per serial.
|
|
54
|
+
5. POST `/advance-shipping-notices` with ASNIU (unit) payload so the interceptor can act.
|
|
55
|
+
6. After the POST: query each generated ASNIU UUID back from DB, insert
|
|
56
|
+
`AdvanceShippingNoticeItemUnits_TrackingNumbers`. This runs for EVERY newly created ASNIU,
|
|
57
|
+
regardless of whether the TrackingNumber record was new or pre-existing.
|
|
58
|
+
7. The Compass ASN `postPost` interceptor (when enabled) creates the `ItemFulfillment` (one per
|
|
52
59
|
SO number, reused if it exists), `ItemFulfillmentItems`, `ItemFulfillmentItemUnits`, and the
|
|
53
60
|
IF/IFI/IFIU `_TrackingNumbers` bridges.
|
|
54
|
-
|
|
55
|
-
resolve the user's language, send the EN/FR in-transit email via
|
|
61
|
+
8. Only when a tracking number is newly inserted: write Notifications (1.0 `db_store` + 2.0),
|
|
62
|
+
resolve the user's language, send the EN/FR in-transit email via
|
|
56
63
|
`/email-templates/sendEmail`, then stamp `c_dtInTransitEmailSent = NOW()`.
|
|
57
64
|
|
|
58
65
|
## Data model
|
|
@@ -73,30 +80,47 @@ a bilingual EN/FR in-transit email (USA is English only) and uses Canadian carri
|
|
|
73
80
|
|
|
74
81
|
## Gotchas / known issues
|
|
75
82
|
- **CSV "Part Number" is the VENDOR part number (`VendorItems.vendorPartNumber`), NOT
|
|
76
|
-
`Items.partNumber
|
|
77
|
-
`Items.partNumber` `C2CY5UC#ABA`.)
|
|
83
|
+
`Items.partNumber` directly.** Resolve the item through the `VendorItems` join. (e.g. vendor
|
|
84
|
+
`IMFP3260` → `Items.partNumber` `C2CY5UC#ABA`.)
|
|
78
85
|
- **G&T's ASN file inconsistently drops the `IM` prefix** (`FP3260` vs `IMFP3260`) on some lines
|
|
79
86
|
→ item-not-found → those lines are skipped and reported in the import-report email. This is a
|
|
80
87
|
G&T-side export issue; toga transmits the correct `IMFP…` to G&T as the cXML `SupplierPartID`
|
|
81
88
|
(see `2_transmit_mits_purchase_orders_to_vendors.php`), so it is NOT a MITS bug.
|
|
89
|
+
- **Build lookup maps OUTSIDE the while loop.** `$purchaseOrderUuidByNumber`, `$shippingMethodByCarrierId`,
|
|
90
|
+
and `$carrierIdByName` must be built once before the row loop — they were originally inside,
|
|
91
|
+
causing a full-table scan of `PurchaseOrders`, `ShippingMethods`, and `ShippingCarriers` on
|
|
92
|
+
every CSV row.
|
|
93
|
+
- **ASNIU→TrackingNumber link is NOT gated on `$isNewTrackingNumber`.** The ASNIU is always
|
|
94
|
+
freshly created on each import row; the `AdvanceShippingNoticeItemUnits_TrackingNumbers` insert
|
|
95
|
+
must run regardless of whether the TrackingNumber record itself was new or pre-existing.
|
|
96
|
+
Gating it on "new TN only" means re-sent / re-processed CSV rows create orphaned ASNIUs with
|
|
97
|
+
no tracking link.
|
|
98
|
+
- **`$shippingMethodByCarrierId[$selectedCarrierId]` needs a null-coalesce fallback.** On PHP 8,
|
|
99
|
+
accessing a missing key without `?? DEFAULT_GROUND_SHIPPING_METHOD_ID` produces a deprecation
|
|
100
|
+
warning that becomes an error. Always write `$shippingMethodByCarrierId[$selectedCarrierId] ?? DEFAULT_GROUND_SHIPPING_METHOD_ID`.
|
|
101
|
+
- **CSV CRLF attachments:** use `preg_split('/\r?\n/', $decodedAttachment)[0]` (not
|
|
102
|
+
`explode("\n", ...)`) for the header-line column-count check. `explode("\n")` leaves a trailing
|
|
103
|
+
`\r` on the first line when the file uses Windows line endings, making `str_getcsv` miscount
|
|
104
|
+
columns and silently skip the file.
|
|
82
105
|
- **The ASN→IF interceptor must be ENABLED per client.** `Core.ApiPayloadInterceptors` for
|
|
83
|
-
`recordId 55`, `prePostProcessing=POST`, `httpMethod=POST` must be `isActive=1
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
does not hit the URL-length limit (HTTP 414). POST requires the `sendEmail` record script
|
|
90
|
-
registered for the POST method (`Core.RecordScripts`), else the engine returns `EV-8`.
|
|
106
|
+
`recordId 55`, `prePostProcessing=POST`, `httpMethod=POST` must be `isActive=1`. It was OFF for
|
|
107
|
+
Compass Canada; enabling it is required or ASNs post but the IF/IFI/IFIU chain never builds.
|
|
108
|
+
- **OAuth2 credentials are currently hardcoded** in the cron with a `// TODO` comment. Before
|
|
109
|
+
going to production, move them to `config.<env>.ini` under `[compasscanada]` and read via
|
|
110
|
+
`App_Registry::get('config')['compasscanada'][…]`. The `config.<env>.ini` must be deployed
|
|
111
|
+
with the code or the OAuth2 token request fails silently.
|
|
91
112
|
- Each CSV line creates one ASN; the interceptor dedups to **one ItemFulfillment per SO**.
|
|
92
113
|
- A new carrier needs its **own** Ground `ShippingMethod`, or the TrackingNumber ends up with the
|
|
93
114
|
right carrier but another carrier's Ground method id.
|
|
94
115
|
- `App_Database::fetchOne()` (1.0) throws on a 0-row result — guard item lookups with `numRows()`
|
|
95
116
|
before `fetchOne()`.
|
|
96
|
-
- The cron reads OAuth2 creds from config `[compasscanada]`; the updated `config.<env>.ini` must
|
|
97
|
-
be deployed with the code or the token request fails.
|
|
98
117
|
|
|
99
118
|
## Change history
|
|
119
|
+
- 2026-06-22 — Full variable rename (no abbreviations); moved PO/carrier/shipping-method lookup maps
|
|
120
|
+
outside the CSV row loop (was causing N full-table queries per file); fixed ASNIU→TN link to run
|
|
121
|
+
unconditionally for new ASNIUs (not only when TN was new); added PHP 8 null-coalesce on carrier→
|
|
122
|
+
method lookup; fixed CSV CRLF detection with `preg_split`; added `DEFAULT_GROUND_SHIPPING_METHOD_ID`
|
|
123
|
+
named constant; function renamed `buildOrderItemsHtmlGrandAndToy` (camelCase). (bala)
|
|
100
124
|
- 2026-06-19 — Built the G&T ASN import cron + one-time backfill script; matched items by
|
|
101
125
|
`VendorItems.vendorPartNumber`; added Canadian carriers + per-carrier Ground methods; EN/FR
|
|
102
126
|
in-transit email via POST; enabled the Compass ASN→IF interceptor for Compass Canada. (bala)
|
package/package.json
CHANGED