toga-ai 1.0.264 → 1.0.266
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/2.0/apps/_underscore/INDEX.md +2 -2
- package/knowledge/2.0/apps/_underscore/architecture.md +15 -2
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +28 -2
- package/knowledge/2.0/apps/_underscore/features/carrier-shipping-labels.md +88 -15
- package/knowledge/2.0/apps/_underscore/features/recursive-item-fulfillments.md +32 -1
- package/knowledge/2.0/apps/dbchanges2/architecture.md +36 -1
- package/knowledge/2.0/apps/toga2-supply/architecture.md +8 -1
- package/knowledge/2.0/apps/toga2-supply/features/fulfill-and-ship.md +26 -24
- package/knowledge/2.0/standards/backend-testing.md +58 -0
- package/knowledge/clients/compass-usa/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/features/asn-to-item-fulfillment.md +14 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
| [_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 |
|
|
6
6
|
| [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, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
|
|
7
7
|
| [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
|
|
8
|
-
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
8
|
+
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
9
9
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
10
10
|
| [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 |
|
|
11
11
|
| [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). | _underscore/Component/Forecast/SaleImport/SaleImport.php, _underscore/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/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| [_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 |
|
|
14
14
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
|
|
15
15
|
| [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 |
|
|
16
|
-
| [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 |
|
|
16
|
+
| [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, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
|
|
17
17
|
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
|
|
18
18
|
| [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 |
|
|
19
19
|
| [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 | |
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal", "rgirish"]
|
|
9
|
+
updated: 2026-07-02
|
|
10
|
+
owners: ["jcardinal", "rgirish", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/_underscore.php
|
|
13
13
|
- _underscore/Loader.php
|
|
@@ -35,6 +35,9 @@ can be silently dropped — confirm commit semantics before relying on a write.
|
|
|
35
35
|
The Surface presentation layer replaces `Page::meta()` but `meta()` must NOT be deleted until
|
|
36
36
|
every page is cut over (per-surface, with a parity diff); config describes, PHP decides —
|
|
37
37
|
business logic never moves into Surface config.
|
|
38
|
+
Storage fields (`FIELD_STORAGE`) must be written by assigning then `save()` — historically a
|
|
39
|
+
`__get` refetch on the write path silently dropped the assigned value (fixed 2026-07); still avoid
|
|
40
|
+
*reading* a storage field between assign and save (it refetches and discards the unsaved value).
|
|
38
41
|
|
|
39
42
|
## Entry point & boot sequence
|
|
40
43
|
|
|
@@ -319,9 +322,19 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
|
|
|
319
322
|
|
|
320
323
|
## Gotchas / known issues
|
|
321
324
|
|
|
325
|
+
- **`FIELD_STORAGE` writes were silently dropped platform-wide (fixed 2026-07, `Model.php`).** Two
|
|
326
|
+
bugs: (a) `buildSqlFieldValue()` read the field via `$this->$field`, which for a storage field
|
|
327
|
+
triggers `__get()` → a disk/S3 **refetch** that overwrote the just-assigned value before `save()`
|
|
328
|
+
persisted it — so storage-field writes never persisted, on both S3 and folder backends; fixed by
|
|
329
|
+
reading the raw `_model_fields[$field]['value']` for storage fields. (b) The folder-mode READ
|
|
330
|
+
branch checked `filesize($cacheFilePath)` (an S3-only temp path) instead of `filesize($filePath)`,
|
|
331
|
+
so folder reads always returned null; fixed. **Remaining follow-up (separate ticket):** `__get()`
|
|
332
|
+
still refetches storage unconditionally, so a dirty read (read-after-assign, pre-save) discards the
|
|
333
|
+
unsaved value.
|
|
322
334
|
- **`_Database::register()` auto-starts a lazy transaction (since Apr 2 2026, commit `fa7835ed`).** Any code that calls `register()` and then writes to that DB must call `_Database::transactionCommit()` before the request ends — otherwise MySQL silently rolls back all writes when the connection closes. Lazy transactions only materialise on the first write, so read-only callers are unaffected. See `_underscore/Database.php:48`. First discovered when Rate SAML user provisioning silently discarded all new user INSERTs (Jun 2026).
|
|
323
335
|
|
|
324
336
|
## Change history
|
|
325
337
|
- 2026-06-11 — Documented lazy transaction gotcha in `_Database::register()` (rgirish)
|
|
326
338
|
- 2026-06-25 — Added the Surface platform UI presentation/configuration layer (DB-driven UI config replacing `Page::meta()`, CTO-reviewed AGREE-WITH-ADJUSTMENTS) (jcardinal)
|
|
327
339
|
- 2026-06-29 — Surface made the enforced (un-flagged) presentation path; reserved id blocks renumbered to Records 333–341 / RecordFields 2246–2433 (known seed-vs-provisioned drift). (jcardinal)
|
|
340
|
+
- 2026-07-02 — Fixed framework-wide `FIELD_STORAGE` write-drop and folder-read bugs in `Model.php`; noted the remaining unconditional-refetch dirty-read follow-up. (mhammontree)
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-07-02
|
|
10
|
+
owners: ["jcardinal", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Core/Page.php
|
|
@@ -90,6 +90,28 @@ then checks the caller's roles against `AclRecordPermissions` (→ `EZ-1` if no
|
|
|
90
90
|
readable/writable fields from `AclFieldPermissions`. `_Model_Core_Page::meta()` reads the same
|
|
91
91
|
tables to return per-page ACL to the frontend.
|
|
92
92
|
|
|
93
|
+
## Record-script dispatch grant (`AclRecordScripts`) — a separate gate
|
|
94
|
+
|
|
95
|
+
A **scripted API** (Record Script) has its own ACL gate, **distinct from the four-table record-CRUD
|
|
96
|
+
chain above**: dispatch is authorized by an **`AclRecordScripts`** row, not by
|
|
97
|
+
`AclRecordPermissions`. Without it the call **403s** even though the route and the PHP method exist.
|
|
98
|
+
|
|
99
|
+
To expose a scripted API on a `_Model` in 2.0:
|
|
100
|
+
1. **Route** — a `Core.RecordScripts` row (id-agnostic; resolve the record by `route=`, not a
|
|
101
|
+
hardcoded id — same discipline as the reserved-id-block work). Fields: `recordId`, `method`
|
|
102
|
+
(GET/POST/…), `route` (the segment after the record name), `phpMethod`.
|
|
103
|
+
2. **Dispatch grant** — a Client-DB **`AclRecordScripts`** row for the roles that may call it
|
|
104
|
+
(typically Public / SuperUser / Base). This is the 403-preventing gate and is the step most
|
|
105
|
+
easily missed; it mirrors the **Surface `meta`** precedent (`GET /v2/surfaces/meta`).
|
|
106
|
+
3. **PHP method** — signature `public static function <phpMethod>(&$api, …namedQueryArgs)`; the
|
|
107
|
+
named args are parsed from the query string; the return value is JSON-encoded into the
|
|
108
|
+
response `data`. Tenant scope comes from the JWT-pinned client DB — no manual client filtering.
|
|
109
|
+
|
|
110
|
+
Worked example: the reprint API `GET /v2/tracking-numbers/reprint`
|
|
111
|
+
(`_Model_Client_TrackingNumber::reprintLabelsApi`) required both a `Core.RecordScripts` route and a
|
|
112
|
+
Client-DB `AclRecordScripts` grant (migrations in `dbchanges2`) before it stopped 403ing — see the
|
|
113
|
+
carrier-shipping-labels feature doc.
|
|
114
|
+
|
|
93
115
|
## Checklist (so the chain is never half-built)
|
|
94
116
|
|
|
95
117
|
- [ ] `AclRecordPermissions` row for the role × record with the right CRUD flags.
|
|
@@ -101,6 +123,10 @@ tables to return per-page ACL to the frontend.
|
|
|
101
123
|
|
|
102
124
|
## Change history
|
|
103
125
|
|
|
126
|
+
- **2026-07-02** — Added the record-script dispatch grant (`AclRecordScripts`) as a gate distinct
|
|
127
|
+
from the record-CRUD chain, with the full recipe for exposing a scripted API on a `_Model`
|
|
128
|
+
(Core.RecordScripts route + Client `AclRecordScripts` grant + `(&$api, …)` method), discovered
|
|
129
|
+
wiring the `tracking-numbers/reprint` scripted API. (mhammontree)
|
|
104
130
|
- **2026-06-23** — Documented after repeatedly missing the logic-group/expression rows when
|
|
105
131
|
granting access to new records. Created alongside the `item-translations` record (331) ACL,
|
|
106
132
|
whose grant in `dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql` is a worked example of
|
|
@@ -6,12 +6,14 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-02
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/ItemFulfillment.php
|
|
13
|
+
- _underscore/Model/Client/TrackingNumber.php
|
|
13
14
|
- _underscore/Model/Client/ItemFulfillments/TrackingNumber.php
|
|
14
15
|
- _underscore/Component/Library/LabelPdf/LabelPdf.php
|
|
16
|
+
- _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php
|
|
15
17
|
- _underscore/Component/Library/Carriers/Ups/Ups.php
|
|
16
18
|
- _underscore/Component/Library/Carriers/Fedex/Fedex.php
|
|
17
19
|
- _underscore/Trait/Netsuite/ItemFulfillment.php
|
|
@@ -33,7 +35,11 @@ Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label
|
|
|
33
35
|
(UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracking
|
|
34
36
|
number and label attached. As of 2026-06 the label is requested + stored as a raw
|
|
35
37
|
**PNG** and the printable PDF is **generated on demand** via `_Component_Library_LabelPdf`
|
|
36
|
-
(FPDF) — superseding the old Labelary ZPL→PDF approach.
|
|
38
|
+
(FPDF) — superseding the old Labelary ZPL→PDF approach. As of 2026-07 the **reprint** combine
|
|
39
|
+
also runs on the backend — a scripted API (`reprintLabelsApi`) reads the stored PNGs and returns
|
|
40
|
+
one combined base64 PDF (the client-side `pdf-lib` merge is gone); **return labels** are generated
|
|
41
|
+
per carrier (`generateReturnLabel`); and carrier **billing account** is threaded from the tracking
|
|
42
|
+
record instead of a hardcoded constant.
|
|
37
43
|
|
|
38
44
|
## Key files / entry points
|
|
39
45
|
|
|
@@ -51,6 +57,20 @@ number and label attached. As of 2026-06 the label is requested + stored as a ra
|
|
|
51
57
|
(`Component/Library/LabelPdf/LabelPdf.php`): builds a multi-page 4x6 PDF from raw PNG label
|
|
52
58
|
images (one per page, optional "Return Label" caption) via FPDF; returns the PDF as a binary
|
|
53
59
|
string. Built to take N pages so a combined shipping + return label is one print-once PDF.
|
|
60
|
+
- `_Model_Client_TrackingNumber::reprintLabelsApi(&$api, $uuids, $caption)`
|
|
61
|
+
(`Model/Client/TrackingNumber.php`): the **backend reprint** scripted API, exposed as a
|
|
62
|
+
`Core.RecordScripts` route **`GET /v2/tracking-numbers/reprint`**. Loads each `TrackingNumbers`
|
|
63
|
+
record by UUID, reads its stored raw PNG (`labelPdfFile`), and returns a **combined base64 PDF**
|
|
64
|
+
via `buildFromPngLabels`. Replaced the frontend `pdf-lib` merge (which broke once labels became
|
|
65
|
+
PNG — it expected a base64 PDF). Requires a Client-DB `AclRecordScripts` dispatch grant or it
|
|
66
|
+
403s — see the ACL permission-chain doc's record-script dispatch section.
|
|
67
|
+
- `_Model_Client_ItemFulfillment::generateReturnLabel(&$api, $uuid)`
|
|
68
|
+
(`Model/Client/ItemFulfillment.php`): builds a per-carrier **return** shipment request
|
|
69
|
+
(ship-from = customer, ship-to = warehouse return address; Ground service; FedEx uses drop-off
|
|
70
|
+
pickup), creates a **second** `TrackingNumbers` record, stores its raw PNG, and links it as
|
|
71
|
+
`returnTrackingNumberId` on the `ItemFulfillments_TrackingNumbers` header bridge.
|
|
72
|
+
- `getLabelFormat()` (client UPS config helper): now **defaults to `PNG`** (was ZPL) and validates
|
|
73
|
+
against a supported-format allowlist, so a carrier label reliably comes back as an embeddable PNG.
|
|
54
74
|
- `_Component_Library_Carriers_Ups::submitShipmentRequest`
|
|
55
75
|
(`Component/Library/Carriers/Ups/Ups.php`): returns `{ success, errorMessage,
|
|
56
76
|
trackingNumber, encodedLabel }`.
|
|
@@ -70,8 +90,8 @@ number and label attached. As of 2026-06 the label is requested + stored as a ra
|
|
|
70
90
|
The label is **stored as the raw carrier PNG** (not a Labelary PDF) and the printable PDF is
|
|
71
91
|
**generated on demand** server-side:
|
|
72
92
|
|
|
73
|
-
1. **Carrier returns PNG.** UPS via `
|
|
74
|
-
|
|
93
|
+
1. **Carrier returns PNG.** UPS via `getLabelFormat()`, which **now defaults to `PNG`** (was ZPL)
|
|
94
|
+
and validates against a supported-format allowlist; FedEx via `$shipmentRequest->labelImageType =
|
|
75
95
|
'PNG'` + `labelStockType = 'PAPER_4X6'`. Labelary is gone.
|
|
76
96
|
2. **Store the raw PNG** on `TrackingNumbers.labelPdfFile` (a `FIELD_STORAGE` field; the name is
|
|
77
97
|
retained though it now holds a PNG) via the model/S3 mechanism — never touch S3 by hand.
|
|
@@ -80,9 +100,28 @@ The label is **stored as the raw carrier PNG** (not a Labelary PDF) and the prin
|
|
|
80
100
|
display + the NetSuite File Cabinet copy.
|
|
81
101
|
|
|
82
102
|
Jeff approved **PNG over GIF**: a PDF lib embeds PNG with **no extra extension** (FPDF parses
|
|
83
|
-
non-alpha PNG in pure PHP — no GD/Imagick).
|
|
84
|
-
|
|
85
|
-
the
|
|
103
|
+
non-alpha PNG in pure PHP — no GD/Imagick). The remaining wiring is now done (2026-07): the
|
|
104
|
+
reprint combine moved to the backend `reprintLabelsApi` scripted API and return-label generation
|
|
105
|
+
(`generateReturnLabel`) is implemented — see the sections below.
|
|
106
|
+
|
|
107
|
+
## Carrier billing account & return labels (implemented 2026-07)
|
|
108
|
+
|
|
109
|
+
**Billing account is data-driven, not a constant.** The selected
|
|
110
|
+
`shippingCarrierAccountNumber` on the tracking record is threaded into the carrier request as
|
|
111
|
+
`ShipmentRequest->$shipperAccountNumber` for both UPS and FedEx billing. The old hardcoded
|
|
112
|
+
`SHIPPER_NUMBER` / `ACCOUNT_NUMBER` constants were **removed**. Two guards:
|
|
113
|
+
- **Fail fast on an empty account** — never ship on a blank billing account.
|
|
114
|
+
- **Fail if an Item Fulfillment has more than one tracking number** — with >1 tracking record the
|
|
115
|
+
code can't decide which account to bill, so it errors rather than guess.
|
|
116
|
+
|
|
117
|
+
**Return labels.** When `ShipmentRequest->isReturn` is set, the carrier request injects the
|
|
118
|
+
return-service flags: UPS `Shipment.ReturnService.Code = '9'` (Print Return Label); FedEx
|
|
119
|
+
`shipmentSpecialServices.returnShipmentDetail` (`returnType = PRINT_RETURN_LABEL`, RMA reason
|
|
120
|
+
"Customer Return"). `generateReturnLabel` builds the return request with **ship-from = customer,
|
|
121
|
+
ship-to = warehouse**, at **Ground / customer-drops-off** service level (decision), creates a 2nd
|
|
122
|
+
`TrackingNumbers` record, stores its raw PNG, and links `returnTrackingNumberId` on the
|
|
123
|
+
`ItemFulfillments_TrackingNumbers` bridge. Warehouse return addresses come from a `RETURN_ADDRESSES`
|
|
124
|
+
set (two verified addresses: Naperville default + Plainview).
|
|
86
125
|
|
|
87
126
|
## `FIELD_STORAGE` mechanics (reference)
|
|
88
127
|
|
|
@@ -101,14 +140,34 @@ store whatever value was assigned. `_Cloud::copyFileToS3` **re-throws** on AWS e
|
|
|
101
140
|
if no exception reached the caller and execution continued, the write was never
|
|
102
141
|
attempted.
|
|
103
142
|
|
|
143
|
+
**Root cause of the "labelPdfFile never lands" bug — two framework-core `Model.php` bugs
|
|
144
|
+
(fixed 2026-07):** these were the true reason storage-field writes silently vanished, and they
|
|
145
|
+
affect **every** 2.0 model that uses `FIELD_STORAGE`, not just labels — so a fuller
|
|
146
|
+
framework-level note is pending on the `_underscore` architecture doc.
|
|
147
|
+
1. **Write path re-fetched over the assigned value.** `buildSqlFieldValue()` read the field via
|
|
148
|
+
`$this->$field`, which for a `FIELD_STORAGE` field triggers `__get()` → a disk/S3 **refetch**
|
|
149
|
+
that overwrote the just-assigned in-memory value *before* `save()` persisted it. Net effect:
|
|
150
|
+
storage-field writes silently never persisted (both S3 and folder backends). Fixed by reading
|
|
151
|
+
the raw `_model_fields[$field]['value']` for storage fields instead of going through `__get`.
|
|
152
|
+
2. **Folder-mode read used the wrong path for its size check.** The folder-storage READ branch
|
|
153
|
+
checked `filesize($cacheFilePath)` (an S3-only temp path) instead of `filesize($filePath)`, so
|
|
154
|
+
folder-mode reads always returned null. Fixed to check `$filePath`.
|
|
155
|
+
|
|
156
|
+
**Known remaining follow-up (separate ticket):** `__get()` still refetches storage
|
|
157
|
+
**unconditionally**, so a *dirty read* (reading a storage field after assigning but before saving)
|
|
158
|
+
still discards the unsaved in-memory value.
|
|
159
|
+
|
|
104
160
|
## Gotchas / known issues
|
|
105
161
|
|
|
106
162
|
- UPS **test** endpoint (`wwwcie.ups.com`, debug mode) returns a constant tracking
|
|
107
163
|
number `1ZXXXXXXXXXXXXXXXX` + SAMPLE labels → `TrackingNumbers.number` UNIQUE
|
|
108
164
|
collision and File Cabinet duplicate-filename failures on repeat tests (see the
|
|
109
165
|
fulfill-and-ship doc for the cleanup SQL). Never in prod.
|
|
110
|
-
-
|
|
111
|
-
|
|
166
|
+
- ~~`upsShipmentApi` reads ship-to from the Sales Order, not the Item Fulfillment.~~
|
|
167
|
+
**Fixed 2026-07:** carrier label ship-to now uses
|
|
168
|
+
`COALESCE(ItemFulfillments.shipToAddressId, SalesOrders.shipToAddressId)` — the edited IF address,
|
|
169
|
+
falling back to the SO — aligning the label with `fulfill()`'s NetSuite query. Form-edited
|
|
170
|
+
addresses now reach the carrier.
|
|
112
171
|
- UPS `AddressLine` max 35 chars/line; send an array of trimmed lines (FedEx already
|
|
113
172
|
does), not `line1 . ' ' . line2`.
|
|
114
173
|
- `ShippingMethods.code` is carrier-scoped: UPS numeric (`03` Ground, `01` Next Day
|
|
@@ -142,19 +201,33 @@ attempted.
|
|
|
142
201
|
works with the no-throw second arg: `_Config::fedex('tracking_client_id', false) ?: …` (same as
|
|
143
202
|
the UPS client). Without it, FedEx shipping dies with "Configuration parameter
|
|
144
203
|
'tracking_client_id' … has not been defined".
|
|
145
|
-
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
204
|
+
- ~~`labelPdfFile` (`FIELD_STORAGE`) write silently skips / S3 label save not landing.~~
|
|
205
|
+
**Root cause found + fixed 2026-07** — it was two framework-core `Model.php` `FIELD_STORAGE`
|
|
206
|
+
bugs (a `__get` refetch overwriting the assigned value on the write path, and a wrong-path
|
|
207
|
+
`filesize` check on the folder-mode read), not a lazy-load issue. See the *FIELD_STORAGE
|
|
208
|
+
mechanics* section above.
|
|
209
|
+
- **`ItemFulfillment.php` had two SQL injections + debug output (fixed 2026-07).** `postPost()`
|
|
210
|
+
interpolated `$payload->uuid` and `fulfill()` interpolated the NetSuite `$internalId` into an
|
|
211
|
+
UPDATE — both now escaped (`_Database::escape`). `fulfill()` also had leftover debug
|
|
212
|
+
`echo`/`print_r` that leaked PII and could corrupt the JSON API envelope — removed. Never emit raw
|
|
213
|
+
output from a scripted API/interceptor; it breaks the `{isSuccess,…,data}` response.
|
|
150
214
|
- **FPDF can't embed alpha-channel or interlaced PNG** — carrier label PNGs must be flat raster.
|
|
151
215
|
If one isn't, `buildFromPngLabels` throws; fix = GD normalize (flatten/de-interlace) or TCPDF.
|
|
152
216
|
- **UPS shipper number must match the endpoint.** Beta hits the UPS **CIE test** endpoint
|
|
153
217
|
(`wwwcie.ups.com`); a production account number there returns `120100`/`120121`
|
|
154
218
|
("missing/invalid shipper number" / "cannot be used"). Use a CIE-enabled shipper number for
|
|
155
|
-
test, or point at prod UPS.
|
|
219
|
+
test, or point at prod UPS. **The shipper/billing account is now the tracking record's
|
|
220
|
+
`shippingCarrierAccountNumber`** (threaded via `ShipmentRequest->$shipperAccountNumber`); the
|
|
221
|
+
hardcoded `SHIPPER_NUMBER`/`ACCOUNT_NUMBER` constants were removed. So for CIE tests the account
|
|
222
|
+
on the tracking record itself must be CIE-enabled.
|
|
223
|
+
- **FedEx credentials are hardcoded in `Fedex.php` and were intentionally retained** (Mark's
|
|
224
|
+
decision) — do not "fix" them into config as part of unrelated work.
|
|
225
|
+
- **Ship guards (2026-07).** Shipping now **fails fast** if the selected carrier account is empty,
|
|
226
|
+
and **errors if an Item Fulfillment has >1 tracking number** (can't determine which account to
|
|
227
|
+
bill) — expect a hard error rather than a silently-wrong bill in those cases.
|
|
156
228
|
|
|
157
229
|
## Change history
|
|
230
|
+
- 2026-07-02 — Reprint combine moved to the backend: new `_Model_Client_TrackingNumber::reprintLabelsApi` scripted API (RecordScript `GET /v2/tracking-numbers/reprint`) returns a combined base64 PDF from the stored PNGs (frontend `pdf-lib` merge removed). Added return-label generation (`generateReturnLabel`; UPS ReturnService `9` / FedEx `PRINT_RETURN_LABEL`; Ground/customer-drops-off; `RETURN_ADDRESSES`; links `returnTrackingNumberId`). Carrier billing account threaded from the tracking record (`shippingCarrierAccountNumber`) — removed `SHIPPER_NUMBER`/`ACCOUNT_NUMBER` constants, fail-fast on empty account, error on >1 tracking number per IF. Ship-to now `COALESCE(IF, SO)` address. `getLabelFormat()` defaults PNG + allowlist. Fixed two SQL injections (`postPost` `$payload->uuid`, `fulfill` NetSuite `$internalId`) and removed leftover debug `echo`/`print_r` from `fulfill()`. Found + fixed the framework-core root cause of storage writes silently vanishing (two `Model.php` `FIELD_STORAGE` bugs — see FIELD_STORAGE mechanics). (mhammontree)
|
|
158
231
|
- 2026-06-18 — Implemented the label-storage rework: request PNG from UPS/FedEx, store the raw PNG, generate the printable PDF on demand via the new `_Component_Library_LabelPdf` (FPDF); dropped Labelary. Added the FedEx tracking-cred config-fallback fix and the `_Loader`→Composer FPDF autoloader toggle. Open: the `labelPdfFile` S3 write still skips on a lazy-model save. (mhammontree)
|
|
159
232
|
- 2026-06-16 — Repointed the carrier-label/NS-IF tracking-number queries (`upsShipmentApi`, `fedexShipmentApi`, `fulfill`, `createNetsuiteItemFulfillment`) from the dropped `ItemFulfillmentPackages` table to the `ItemFulfillments_TrackingNumbers` bridge — they had been saving the label/`number` to the wrong/null record (reprint `labelPdfFile` null; NS IF missing tracking). Confirmed working on beta. (mhammontree)
|
|
160
233
|
- 2026-06-10 — Documented carrier shipping label mechanics (UPS/FedEx) + the authoritative GIF label-storage decision and NetSuite IF attachment. (mhammontree)
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-02
|
|
10
10
|
owners: [jcardinal, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/ItemFulfillment.php
|
|
@@ -16,6 +16,7 @@ files:
|
|
|
16
16
|
- _underscore/Model/Compass/AdvanceShippingNotice.php
|
|
17
17
|
- dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql
|
|
18
18
|
- dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql
|
|
19
|
+
- dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql
|
|
19
20
|
related:
|
|
20
21
|
- ../architecture.md
|
|
21
22
|
- ../../api2/architecture.md
|
|
@@ -102,12 +103,42 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
|
|
|
102
103
|
upstream mirror. A later reconciling PUT cleans up stale records via set comparison. Full
|
|
103
104
|
delete handling would need a `preDelete` mechanism + `PRE/DELETE` interceptor rows.
|
|
104
105
|
- **Transfer-order fulfillments are explicitly excluded from the upstream walk.** `reconcileUpstreamLevel()` returns `null` immediately when the downstream IF has `transferOrderId` set (no `salesOrderId`). This is intentional for standalone TOs (GroWrk v1). When SO→TO→SO chain scenarios are needed, Phase 5 of the GroWrk transfer order plan extends this method to walk `TransferOrders_SalesOrders` bridge tables — see `clients/growrk/features/transfer-order-flow.md`.
|
|
106
|
+
- **Broken SOI↔POI bridge → empty header-only duplicate IFs (root cause, guard added
|
|
107
|
+
2026-07-02).** The item walk maps a downstream IFI up via
|
|
108
|
+
`IFI.salesOrderItemId → PurchaseOrderItems_SalesOrderItems → SalesOrderItems_PurchaseOrderItems`.
|
|
109
|
+
When that upstream `SalesOrderItems_PurchaseOrderItems` bridge is **missing/broken** (the same
|
|
110
|
+
integrity class as the ~116 off-by-one Compass orders — see the Compass
|
|
111
|
+
`asn-to-item-fulfillment` doc), `$desiredItems` resolves to **zero** even though the downstream
|
|
112
|
+
IF has items. Pre-guard, step 4's eager resolve-or-create then POSTed an **empty header-only
|
|
113
|
+
upstream IF** (e.g. Compass SA133377 got duplicate empty customer-facing IFs 71724/71725
|
|
114
|
+
alongside the legit direct-ASN IF 70635). **Guard:** `reconcileUpstreamLevel` now, after
|
|
115
|
+
building `$desiredItems`, if it is empty **AND** the downstream IF has >0 ItemFulfillmentItems,
|
|
116
|
+
`error_log()`s a diagnostic and returns `null` (skips the upstream mirror) — distinguished from
|
|
117
|
+
the legitimate header-first POST case (downstream IF genuinely has 0 items), which is unchanged.
|
|
118
|
+
- **Duplicate customer-facing IF: the engine only adopts an upstream IF it created itself, never a
|
|
119
|
+
pre-existing one (known gap, adoption DEFERRED).** `reconcileUpstreamLevel` finds an existing
|
|
120
|
+
upstream IF only via `upstreamItemFulfillmentId`; it does **not** adopt a pre-existing IF that
|
|
121
|
+
already sits on the upstream SO from another flow (e.g. a direct-ASN-created IF). So a broken
|
|
122
|
+
bridge could yield two customer-facing IFs on the same SO (SA133377: direct-ASN IF 70635 + a
|
|
123
|
+
mirror-created duplicate). **Naive adoption is dangerous and was intentionally not implemented:**
|
|
124
|
+
step 5's delete-stale step deletes upstream IFIs with no matching downstream group, so adopting
|
|
125
|
+
an IF while the item walk is empty/broken would **DELETE the adopted IF's legitimate line items**.
|
|
126
|
+
Requires a CTO design review before implementing adoption semantics. The 2026-07-02 broken-bridge
|
|
127
|
+
guard prevents the duplicate cascade for this failure mode without needing adoption.
|
|
105
128
|
- **Out of scope:** NetSuite sync of upstream IFs; transfer-order fulfillments
|
|
106
129
|
(`transferOrderId`) are skipped.
|
|
107
130
|
- **Performance:** every child write re-runs a full upstream walk (read-heavy, but each
|
|
108
131
|
upstream record is written at most once — idempotent). Fine for normal fulfillment sizes.
|
|
109
132
|
|
|
110
133
|
## Change history
|
|
134
|
+
- 2026-07-02 — Added a **broken-bridge guard** to `reconcileUpstreamLevel`: when `$desiredItems`
|
|
135
|
+
is empty but the downstream IF has items (broken SOI↔POI bridge), it logs and returns null
|
|
136
|
+
instead of creating an empty header-only duplicate upstream IF. Root-caused on Compass SA133377
|
|
137
|
+
(SO 107609), where a broken bridge + a pre-existing direct-ASN IF produced duplicate empty
|
|
138
|
+
customer-facing IFs (71724/71725) and left the correct serial/tracking off the customer IF;
|
|
139
|
+
data repaired by `dbchanges2/Client_Compass/2026-07-02a`. Companion fix — adopting a pre-existing
|
|
140
|
+
upstream IF instead of creating a duplicate — was **DEFERRED** pending CTO review (the delete-stale
|
|
141
|
+
step would delete the adopted IF's line items when the item walk is empty). (jcardinal)
|
|
111
142
|
- 2026-06-19 — Documented explicit TO exclusion in reconcileUpstreamLevel; cross-linked GroWrk transfer order plan. (rgirish)
|
|
112
143
|
- 2026-06-08 — Documented the Recursive Item Fulfillments engine (interceptor-driven upstream fulfillment mirroring, bundle scaling, reconcile loop). (jcardinal)
|
|
113
144
|
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-02
|
|
10
10
|
owners: [jcardinal, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- Core/
|
|
@@ -198,6 +198,41 @@ WHERE
|
|
|
198
198
|
- Keep the single statement well under `max_allowed_packet` (64 MB default) — a few thousand
|
|
199
199
|
rows is comfortably fine.
|
|
200
200
|
|
|
201
|
+
## Self-referencing DELETE — wrap the subquery in a derived table
|
|
202
|
+
|
|
203
|
+
MySQL forbids a `DELETE` (or `UPDATE`) whose `WHERE` subquery reads **the same table being
|
|
204
|
+
modified** — it fails with **error 1093: "You can't specify target table 'X' for update in
|
|
205
|
+
FROM clause."** This bites cleanup migrations that delete duplicate rows by selecting which
|
|
206
|
+
rows to keep from the very table being pruned.
|
|
207
|
+
|
|
208
|
+
The fix is to **materialize the subquery through an extra derived-table layer**, forcing MySQL
|
|
209
|
+
to snapshot the row set before the delete runs:
|
|
210
|
+
|
|
211
|
+
```sql
|
|
212
|
+
-- WRONG — error 1093, ItemFulfillments is both the DELETE target and read in the subquery
|
|
213
|
+
DELETE FROM ItemFulfillments
|
|
214
|
+
WHERE id NOT IN (
|
|
215
|
+
SELECT MIN(id)
|
|
216
|
+
FROM ItemFulfillments
|
|
217
|
+
GROUP BY salesOrderItemId, trackingSerial
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
-- CORRECT — wrap the inner SELECT in a derived table so it is materialized first
|
|
221
|
+
DELETE FROM ItemFulfillments
|
|
222
|
+
WHERE id NOT IN (
|
|
223
|
+
SELECT keepId FROM (
|
|
224
|
+
SELECT MIN(id) AS keepId
|
|
225
|
+
FROM ItemFulfillments
|
|
226
|
+
GROUP BY salesOrderItemId, trackingSerial
|
|
227
|
+
) AS keep
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The extra `SELECT … FROM ( … ) AS keep` wrapper is the whole idiom: MySQL evaluates the inner
|
|
232
|
+
query into a temporary derived table, so the outer `DELETE` no longer "sees" a live read of
|
|
233
|
+
its own target. Used in `2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql` to prune
|
|
234
|
+
duplicate `ItemFulfillments` rows.
|
|
235
|
+
|
|
201
236
|
## Relationship to the rest of 2.0
|
|
202
237
|
|
|
203
238
|
`dbchanges2` is registered as a **2.0 core repo** (`role: core` in `registry.json`) — it is
|
|
@@ -6,7 +6,7 @@ project: TOGa Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-02
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-supply/src/api/toga.ts
|
|
@@ -28,6 +28,11 @@ Fulfillment creation, label printing/reprinting). It is a pure frontend: all bus
|
|
|
28
28
|
logic lives in the 2.0 API (`api2` engine over `_underscore` models). It has **no
|
|
29
29
|
backend code of its own** — `dependsOn: [api2]`.
|
|
30
30
|
|
|
31
|
+
**Critical rules:** `toga2-supply` is a **legacy app slated for replacement** — build UI
|
|
32
|
+
**in-repo** and do **not** extract shared components to `@agilant/toga-blox` (that investment
|
|
33
|
+
belongs in the successor app). It is a pure frontend (`dependsOn: [api2]`); all business logic
|
|
34
|
+
stays in the 2.0 API, never in the frontend.
|
|
35
|
+
|
|
31
36
|
## Topology (dev/test setup)
|
|
32
37
|
|
|
33
38
|
- The frontend runs **locally** on the developer machine (e.g.
|
|
@@ -66,6 +71,8 @@ Response envelope (2.0 API): `{ isSuccess, status, error, messages, data: { <rou
|
|
|
66
71
|
- Browser print: the print dialog handles one file at a time and `window.print()` fires
|
|
67
72
|
once per session — hence the combined-PDF reprint design. Popups must be allowed
|
|
68
73
|
(label window is `window.open`).
|
|
74
|
+
- Legacy status: this repo is being replaced; do not refactor toward `toga-blox` extraction —
|
|
75
|
+
keep new UI local to the repo.
|
|
69
76
|
|
|
70
77
|
## Related docs
|
|
71
78
|
|
|
@@ -6,7 +6,7 @@ project: TOGa Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-02
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx
|
|
@@ -59,18 +59,19 @@ GroWrk, June 2026); FedEx + UPS both need verification before prod.
|
|
|
59
59
|
`saveShipmentToNetsuite` → `createNetsuiteItemFulfillment`, which attaches the label
|
|
60
60
|
to the IF **after** creating it (the IF doesn't exist yet when the label is bought).
|
|
61
61
|
|
|
62
|
-
## Reprint (
|
|
62
|
+
## Reprint (backend-combined, 2026-07)
|
|
63
63
|
|
|
64
64
|
Fulfilled-shipments view (`FulfilledShipmentsPage` → `ShipmentsCardTableForm` with
|
|
65
|
-
`isFulfilledShipmentsView`): multi-select shipments,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
`
|
|
65
|
+
`isFulfilledShipmentsView`): multi-select shipments, print one combined multi-page PDF once.
|
|
66
|
+
Because labels are now stored as raw carrier **PNG** on `TrackingNumbers.labelPdfFile` (see the
|
|
67
|
+
carrier-shipping-labels doc), the **combine now runs on the backend**: `ShipmentsApi.ts` calls the
|
|
68
|
+
scripted API **`GET /v2/tracking-numbers/reprint`** (`_Model_Client_TrackingNumber::reprintLabelsApi`,
|
|
69
|
+
passing the selected tracking UUIDs + caption), which reads the stored PNGs and returns one combined
|
|
70
|
+
base64 **PDF**. The old client-side `pdf-lib` merge (which expected a base64 PDF and broke on a PNG)
|
|
71
|
+
was **removed**. The tracking source was also corrected from `shipment.itemFulfillmentPackages` to
|
|
72
|
+
`itemFulfillmentTrackingNumbers` (bridge migration). Reading tracking still needs the bridge ACL
|
|
73
|
+
chain (see gotchas) **and** the reprint RecordScript needs its `AclRecordScripts` dispatch grant, or
|
|
74
|
+
the call 403s and returns no label.
|
|
74
75
|
|
|
75
76
|
## Gotchas / known issues
|
|
76
77
|
|
|
@@ -81,9 +82,10 @@ The reprint reference was also corrected from `shipment.itemFulfillmentPackages`
|
|
|
81
82
|
both between runs:
|
|
82
83
|
`UPDATE TrackingNumbers SET number = CONCAT(number,'-',id) WHERE number='1ZXXXXXXXXXXXXXXXX'`
|
|
83
84
|
and delete/rename the prior File Cabinet file. Never happens in prod.
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
- ~~Address source mismatch: ship-to read from the SO, not the edited IF address.~~
|
|
86
|
+
**Fixed 2026-07:** carrier ship-to now uses `COALESCE(ItemFulfillments.shipToAddressId,
|
|
87
|
+
SalesOrders.shipToAddressId)` — the form-edited IF address, falling back to the SO — so
|
|
88
|
+
address edits now reach the carrier.
|
|
87
89
|
- **UPS address limits**: `AddressLine` max 35 chars/line; code currently sends
|
|
88
90
|
`line1 + ' ' + line2` as one line — should send a trimmed array (FedEx path already
|
|
89
91
|
does).
|
|
@@ -139,16 +141,15 @@ The reprint reference was also corrected from `shipment.itemFulfillmentPackages`
|
|
|
139
141
|
|
|
140
142
|
## Remaining work
|
|
141
143
|
|
|
142
|
-
- Reprint rewire
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
combined PDF.
|
|
149
|
-
-
|
|
150
|
-
|
|
151
|
-
verification before prod.
|
|
144
|
+
- ~~Reprint rewire~~ — done (backend `reprintLabelsApi` combine; see Reprint section).
|
|
145
|
+
- ~~Address-source decision~~ — done (`COALESCE(IF, SO)`).
|
|
146
|
+
- Return-label flow: backend generation is done (`generateReturnLabel`; UPS ReturnService `9` /
|
|
147
|
+
FedEx `PRINT_RETURN_LABEL`; 2nd tracking number linked via `returnTrackingNumberId` on the
|
|
148
|
+
bridge; Ground/customer-drops-off). Remaining **frontend** polish: "needs return label" control,
|
|
149
|
+
a return-address editable combo ("Rolodex pattern" of client locations, overridable, validated),
|
|
150
|
+
and "return label" text on return pages of the combined PDF.
|
|
151
|
+
- UPS 35-char hardening, location-scoped inventory lookup, carrier/method on the NS IF,
|
|
152
|
+
responsiveness per Figma, FedEx + UPS end-to-end verification before prod.
|
|
152
153
|
|
|
153
154
|
## Client variations
|
|
154
155
|
|
|
@@ -158,6 +159,7 @@ not the base `_Model_Client_ItemFulfillment`. Tested with GroWrk; UPS support wa
|
|
|
158
159
|
for Compass and is not yet in prod.
|
|
159
160
|
|
|
160
161
|
## Change history
|
|
162
|
+
- 2026-07-02 — Reprint rewired to the backend: `ShipmentsApi.ts` now calls `GET /v2/tracking-numbers/reprint` (`reprintLabelsApi`) for a combined base64 PDF built from the stored PNGs; removed the stale client-side `pdf-lib` merge. Return-label backend generation landed (`generateReturnLabel`, `returnTrackingNumberId` on the bridge). Ship-to address source resolved to `COALESCE(IF, SO)`. (mhammontree)
|
|
161
163
|
- 2026-06-18 — PNG label-storage rework reflected on the frontend: labels now store as PNG and the PDF is built on the backend (`LabelPdf`), so the client-side `pdf-lib` reprint is stale and must be rewired to a backend generate endpoint. Fixed the Fulfilled Shipments responsiveness/clip bug (Reprint button unreachable at 100% zoom) — `overflow-scroll` → `overflow-auto` and gave `FulfilledShipmentsPage`'s outer its own scroll; documented the `/shipments` vs `/fulfilled-shipments` two-component trap and the `AuthLayout overflow-hidden` clip. (mhammontree)
|
|
162
164
|
- 2026-06-16 — Drove the full beta flow green for UPS/GroWrk. Updated the save step to the `/item-fulfillment-tracking-numbers` bridge (FE commit `b5c16592d`, key `itemFulfillmentTrackingNumbers`). Added gotchas: local-FE↔deployed-beta-BE skew, `dtSubmitted` fulfilled/pending gating, `createNetsuiteItemFulfillment` orderLine fragility on partial/already-fulfilled orders, and the bridge-ACL 403 that nulls tracking/labels (reprint). (mhammontree)
|
|
163
165
|
- 2026-06-10 — Documented the Fulfill & Ship flow (SO sync, label purchase, NetSuite IF creation, success gating, reprint). Driven green on beta for UPS/GroWrk; FedEx + prod verification pending. (mhammontree)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 2.0 Backend Testing (no PHPUnit — ad-hoc client-pinned scripts)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: _Underscore
|
|
5
|
+
client: shared
|
|
6
|
+
type: standard
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-07-02
|
|
9
|
+
owners: [mhammontree]
|
|
10
|
+
files: []
|
|
11
|
+
related:
|
|
12
|
+
- ../apps/_underscore/architecture.md
|
|
13
|
+
- ../apps/_underscore/features/per-client-database-connections.md
|
|
14
|
+
- ../apps/test/architecture.md
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Summary
|
|
18
|
+
|
|
19
|
+
The 2.0 backend (`_underscore` + its apps) has **no PHPUnit harness**. Verify backend behavior
|
|
20
|
+
with **ad-hoc scripts in the `test` repo** (per-dev folders, e.g. `@Mark`) that bootstrap
|
|
21
|
+
`_underscore` from a 2.0 app and pin a real client database explicitly. This is the accepted
|
|
22
|
+
verification path until/unless a formal harness exists — do not assume a `phpunit` suite.
|
|
23
|
+
|
|
24
|
+
## Bootstrapping a verification script
|
|
25
|
+
|
|
26
|
+
From a 2.0 app root (e.g. `worker2`): `chdir` into the app, require Composer autoload, require
|
|
27
|
+
`_underscore.php`, and set `ENVIRONMENT` before bootstrap:
|
|
28
|
+
|
|
29
|
+
```php
|
|
30
|
+
chdir('/path/to/worker2');
|
|
31
|
+
require 'vendor/autoload.php';
|
|
32
|
+
require '_underscore.php'; // boots Loader/Config/etc.
|
|
33
|
+
// ENVIRONMENT must be set (env var / server) so _Config picks the right INI
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## You MUST pin the client DB explicitly
|
|
37
|
+
|
|
38
|
+
`_underscore::DB_CLIENT` is a **logical alias**, not a schema name — there is no database literally
|
|
39
|
+
named `Client`. Real per-client schemas are `Client_True`, `Client_Aig`, etc. In a real request V2
|
|
40
|
+
pins the concrete schema from the JWT; a standalone script has no JWT, so **you must register it**:
|
|
41
|
+
|
|
42
|
+
```php
|
|
43
|
+
_Database::register(
|
|
44
|
+
database: 'Client_True', // real per-client schema
|
|
45
|
+
// …host/user/pw from config…
|
|
46
|
+
alias: _underscore::DB_CLIENT // the logical alias models resolve against
|
|
47
|
+
);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Without this, any `Model/Client/*` query resolves the unmapped alias and fails. Use a client whose
|
|
51
|
+
schema you have locally/on the target env (internal testing uses `Client_True` = TOGA Technology).
|
|
52
|
+
Remember the per-client **logs** connection trap too (see the per-client-database-connections doc).
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
|
|
56
|
+
- Do not add or assume a PHPUnit suite for 2.0 backend work; write a `test`-repo script instead.
|
|
57
|
+
- Always pin the concrete `Client_<Id>` schema to `DB_CLIENT` (and logs/archive if the code logs).
|
|
58
|
+
- Keep scripts in your per-dev `test` folder; never point them at production databases.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
-
| [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql |
|
|
5
|
+
| [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
|
|
6
6
|
| [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql |
|
|
7
7
|
| [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
8
8
|
| [Compass MITS PO Transmission to Vendors](features/mits-po-transmission-to-vendors.md) | 2.0 | The 1.0 worker cron `2_transmit_mits_purchase_orders_to_vendors.php` transmits Compass PurchaseOrders to their vendors (Office Depot, Strategic Systems, Compass | worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, library/app/client/compass.php |
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
8
|
+
updated: 2026-07-02
|
|
9
9
|
owners: [jcardinal, bala]
|
|
10
10
|
files:
|
|
11
11
|
- _underscore/Model/Compass/AdvanceShippingNotice.php
|
|
@@ -18,6 +18,7 @@ files:
|
|
|
18
18
|
- dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql
|
|
19
19
|
- dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql
|
|
20
20
|
- dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql
|
|
21
|
+
- dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql
|
|
21
22
|
related:
|
|
22
23
|
- ../../../2.0/apps/_underscore/features/recursive-item-fulfillments.md
|
|
23
24
|
- ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
@@ -207,6 +208,18 @@ Compass Canada (`Model/Compass/Canada/`) is a separate sub-client. The Compass c
|
|
|
207
208
|
|
|
208
209
|
## Change history
|
|
209
210
|
Dated one-liners, newest first.
|
|
211
|
+
- 2026-07-02 — Repaired SA133377 (SO 107609, line 1 MD7F4LL/A-S): the customer IF (IFI 276821)
|
|
212
|
+
had no serial and the wrong tracking (`522944658493` — the first of two tracking-only vendor
|
|
213
|
+
ASNs on PO 103641 that reconciled onto the direct-ASN IF 70635) instead of `873840444805`
|
|
214
|
+
(the real device unit 63108 / serial SKRX9Y4W537 arrived up the mirror chain from downstream
|
|
215
|
+
IF 71723). Migration `2026-07-02a` adds the missing IFIU + correct unit-level tracking, swaps
|
|
216
|
+
the stale header/item tracking, and retires the empty duplicate IF 71725 (idempotent, sql-reviewer
|
|
217
|
+
SAFE). Root cause was a broken `SalesOrderItems_PurchaseOrderItems` bridge on the downstream
|
|
218
|
+
shipped SO item (PO item 179306, no upstream bridge — same off-by-one integrity class as the
|
|
219
|
+
~116 orders above) feeding the recursive mirror engine, which then created empty duplicate
|
|
220
|
+
customer-facing IFs; forward engine guard added in the
|
|
221
|
+
[Recursive Item Fulfillments](../../../2.0/apps/_underscore/features/recursive-item-fulfillments.md)
|
|
222
|
+
doc (2026-07-02). (jcardinal)
|
|
210
223
|
- 2026-06-30 — `resolveOrCreateItemFulfillment` now resolves the shipped `ItemFulfillmentStage` by
|
|
211
224
|
status slug and sets `itemFulfillmentStage` on the IF payload, so ASN-created fulfillments land
|
|
212
225
|
shipped (not stage-less) on both Compass USA + Canada. Part of the platform-wide IF stage
|
package/package.json
CHANGED