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.
@@ -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-06-29
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-06-23
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-06-18
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 `[ups] label_format = "PNG"` config (the client's
74
- `getLabelFormat()` reads it; defaults ZPL); FedEx via `$shipmentRequest->labelImageType =
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). Remaining wiring: the reprint/return-label combine
84
- (frontend calls a backend generate endpoint instead of the client-side `pdf-lib` merge), and
85
- the return-label generation itself.
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
- - `upsShipmentApi` reads ship-to from the **Sales Order** (`shipToAddressId`), not the
111
- Item Fulfillment — form-edited addresses never reach UPS.
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
- - **`labelPdfFile` (`FIELD_STORAGE`) write can silently skip** when set on a lazy
146
- `new _Model_Client_TrackingNumber($id)` (never loaded): `number` (a real column) saves but the
147
- storage write is skipped (no error, no S3 object — and `copyFileToS3` re-throws, so a skip ≠ a
148
- failed write). Likely needs `->load()` before setting `labelPdfFile`. **Open issue** — the S3
149
- label save was still not landing as of 2026-06-18.
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. `SHIPPER_NUMBER` is a hardcoded constant in the UPS client.
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-06-19
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-06-23
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-06-10
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-06-18
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 (being rewired to the backend)
62
+ ## Reprint (backend-combined, 2026-07)
63
63
 
64
64
  Fulfilled-shipments view (`FulfilledShipmentsPage` → `ShipmentsCardTableForm` with
65
- `isFulfilledShipmentsView`): multi-select shipments, merge each stored label into one
66
- multi-page PDF, print once. The label-storage rework (see the carrier-shipping-labels doc)
67
- now stores the raw carrier **PNG** on `TrackingNumbers.labelPdfFile` and builds the PDF on the
68
- **backend** (`_Component_Library_LabelPdf`/FPDF). So the current client-side `pdf-lib` merge —
69
- which reads `labelPdfFile` expecting a base64 **PDF** — is now **stale**: it receives a PNG.
70
- Reprint must be rewired to call a backend generate endpoint that returns the combined
71
- (shipping + return) PDF; until then it won't produce a printable label even when one is stored.
72
- The reprint reference was also corrected from `shipment.itemFulfillmentPackages` to
73
- `itemFulfillmentTrackingNumbers` (bridge migration).
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
- - **Address source mismatch**: `upsShipmentApi` reads ship-to from the **Sales Order**
85
- (`SalesOrder.shipToAddressId`), NOT the address edited in the form (the IF's) — form
86
- address edits never reach the carrier. Open decision: read the IF address instead.
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: call the backend `LabelPdf` generate endpoint (PNG → combined PDF) instead
143
- of the stale client-side `pdf-lib` merge (per the carrier-shipping-labels doc). The backend
144
- half (PNG store + on-demand PDF) is done.
145
- - Return-label flow: `returnTrackingNumberId` (nullable) on `ItemFulfillmentItemUnits`;
146
- "needs return label" checkbox; return-address editable combo ("Rolodex pattern" of
147
- client locations, overridable, validated); "return label" text on return pages of the
148
- combined PDF.
149
- - Address-source decision, UPS 35-char hardening, location-scoped inventory lookup,
150
- carrier/method on the NS IF, responsiveness per Figma, FedEx + UPS end-to-end
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-06-30
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.264",
3
+ "version": "1.0.266",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",