toga-ai 1.0.263 → 1.0.265

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 |
@@ -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: 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.263",
3
+ "version": "1.0.265",
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",
@@ -33,6 +33,52 @@ description: Start-of-session context loader for TOGA Technology projects. Run t
33
33
  > block message is never to fight it: run Steps 0–3, and the moment preflight executes you
34
34
  > are free to read docs and work. (Emergency override only: `KICKOFF_GATE_DISABLED=1`.)
35
35
 
36
+ ## Session permission policy — auto-accept local file I/O, ALWAYS confirm dangerous execution
37
+
38
+ > **This policy is in force for the ENTIRE session, from the moment `/kickoff` runs until it
39
+ > ends. It is a standing, team-wide default — apply it without being reminded.**
40
+ >
41
+ > The intent: keep the developer in flow for safe, reversible, local work, while never letting
42
+ > an irreversible or environment-touching action happen without an explicit human "go ahead".
43
+
44
+ **Auto-accept (proceed without a permission prompt) — safe, local, reversible:**
45
+
46
+ - **Reading files** — `Read`, `Grep`, `Glob`, and any read-only inspection of the local
47
+ working tree. Never pause to ask "may I read this?".
48
+ - **Writing/editing files** in the local working tree — `Write`, `Edit`, `NotebookEdit`, and
49
+ creating new local files. Proceed as long as the target is inside the developer's local
50
+ checkout(s). (Standard write discipline still applies — see the stub-first rule in Step 6.)
51
+
52
+ **ALWAYS ask for explicit permission first (never auto-run) — anything that executes, leaves
53
+ the machine, or touches a shared/remote/production environment:**
54
+
55
+ - **Shell execution** — every `Bash` / PowerShell command runner invocation. Before running
56
+ one, state what it does and why, and wait for the developer's "go ahead". (The kickoff gate
57
+ already blocks shell during priming; this extends the caution to the whole session.)
58
+ - **Deploys, AWS / DevOps, and infrastructure** — `/ship`, `/land-and-deploy`, `/canary`, the
59
+ `devops` agent's AWS CLI/console actions, Elastic Beanstalk / EC2 / RDS / S3 / IAM changes,
60
+ and any command that mutates infrastructure. Confirm the target environment explicitly.
61
+ - **Remote database access** — the `toga-db` MCP tools (`toga_query`, etc.) and any query run
62
+ against **production or any non-local environment**. Name the cluster/environment and get a
63
+ yes before executing. Treat production as require-confirmation on every call.
64
+ - **Network, browser, and remote git** — `WebFetch`/`WebSearch`, the `/browse` browser
65
+ automation, `git push`, PR creation/merge, and any call that sends data off the machine or
66
+ writes to a remote. Confirm before each.
67
+
68
+ **Rules of application:**
69
+
70
+ 1. When in doubt about whether an action executes something or reaches outside the local
71
+ environment, **treat it as dangerous and ask.** The default for the unknown is *confirm*.
72
+ 2. "Auto-accept file I/O" does **not** mean "act without judgment" — still respect
73
+ `/freeze`, `/guard`, and any directory scoping the developer set.
74
+ 3. A blanket "go ahead", "just do it", or "you have full access" from the developer covers the
75
+ current task's expected actions, but you must **still flag** any dangerous action that falls
76
+ outside the scope they approved (a different environment, a production write, a deploy).
77
+ 4. This is a **directive, not an enforcement mechanism.** For hard, mechanical enforcement the
78
+ team should also encode these as `permissions` allow/ask rules in the checked-in
79
+ `.claude/settings.json` (allow `Read`/`Edit`/`Write`; `ask` for `Bash` and the remote/DB/
80
+ deploy tools). Until that exists, follow this policy by discipline on every session.
81
+
36
82
  ## Arguments — text passed after `/kickoff` never skips any step
37
83
 
38
84
  Trailing text (e.g. `/kickoff worker2 backend fix for Compass`, even a detailed paragraph
@@ -253,6 +299,10 @@ Tell the developer concisely:
253
299
  summaries" list — the developer must see that the core is loaded and which app pulled it in.
254
300
  - Which knowledge docs you loaded (by title). Where a repo/standard has **no knowledge yet**,
255
301
  say so explicitly: "No knowledge captured yet for X — `capture` will build it as you work."
302
+ - **State the session permission policy in one line** so the developer knows the rules for
303
+ this session: "File reads and local edits run automatically; anything that executes, deploys,
304
+ hits a remote/production environment, or leaves this machine, I'll confirm with you first."
305
+ (See *Session permission policy* near the top of this skill.)
256
306
  - Confirm you're primed and ready for their first task.
257
307
 
258
308
  **Heavy-load check (from preflight `estimate`).** Preflight returns