toga-ai 1.0.445 → 1.0.447

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.
@@ -4,7 +4,7 @@
4
4
  |-----|---------|-------|
5
5
  | [Proposed — git-sourced base+overlay JSON authoring for the Surface layer](architecture/surface-authoring-proposal.md) | A **proposal / handoff recommendation** (not implemented) that the Surface layer's *authoring* model move off hand-authored SQL against the `SurfaceOverrides` E | _underscore/Model/Core/Surface.php, _underscore/Model/Client/SurfaceOverride.php |
6
6
  | [_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 |
7
- | [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, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql |
7
+ | [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, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql |
8
8
  | [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | `_Model_Client_Address::validateAddress` verifies a US address against a **carrier waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-norm | _underscore/Model/Client/Address.php |
9
9
  | [_ApiRequest ENCODE__JSON now sends Content-Type: application/json](features/apirequest-json-content-type.md) | `_ApiRequest::execute()`'s `ENCODE__JSON` branch json-encoded the request body but never set a `Content-Type` header. | _underscore/ApiRequest.php |
10
10
  | [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 |
@@ -30,4 +30,4 @@
30
30
  | [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_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
31
31
  | [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 | |
32
32
  | [Refreshing a Local Dev Database from Beta (dev-sandbox)](workflows/local-db-refresh-from-beta.md) | How to reset a local 2.0 dev database from the **beta / dev-sandbox** environment: dump each schema (`Core`, `Client_<Id>`, `Logs_<Id>`, …) from the beta host, | api2/Config/, _underscore/Loader.php, _underscore/Model/Client/BundleTranslation.php, api2/Component/Api/V2/V2.php, toga25-supply/sync_compasscanada_schema.sql |
33
- | [Running a 2.0 App Locally (browser, end-to-end via api2)](workflows/running-a-2.0-app-locally.md) | The full dependency chain required to run a 2.0 client app **through the browser**, end-to-end, against a **local `api2`** (e.g. | _underscore/Environment.php, _underscore/Config.php, _underscore/Database.php, _underscore/Route.php, api2/Component/Api/V2/V2.php, api2/index.php, api2/.htaccess |
33
+ | [Running a 2.0 App Locally (browser, end-to-end via api2)](workflows/running-a-2.0-app-locally.md) | The full dependency chain required to run a 2.0 client app **through the browser**, end-to-end, against a **local `api2`** (e.g. | _underscore/Environment.php, _underscore/Config.php, _underscore/Database.php, _underscore/Route.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php, api2/index.php, api2/.htaccess |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-23
9
+ updated: 2026-07-27
10
10
  owners: ["jcardinal", "mhammontree", "tcox"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -15,6 +15,7 @@ files:
15
15
  - dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql
16
16
  - dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql
17
17
  - dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql
18
+ - dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql
18
19
  ---
19
20
 
20
21
  ## Summary
@@ -53,6 +54,16 @@ writable). Without it the field is rejected on write **and** is unreadable:
53
54
  fully registered (`Core.RecordFields` row present, so no `EV-8`) yet still `EZ-2` because it has
54
55
  no `AclFieldPermissions` row — **registering a field and granting access to it are two separate
55
56
  steps**, and shipping the field registration without the grant is a recurring launch blocker.
57
+ - **⚠ The V2 GET response's OUTPUT field list is itself BUILT from `AclFieldPermissions`, so a field
58
+ with no grant is often SILENTLY OMITTED from the response body — not surfaced as an `EZ-2` error.**
59
+ `_Component_Api_V2::getAclFieldPermissions()` (`api2/Component/Api/V2/V2.php` ~L5795) resolves the
60
+ caller's readable/writable set, and the output serializers (`foreach ($aclFieldPermissions as $field
61
+ => $isWritable)` output loops, ~L3181 / L6082 / L6302) iterate **only** that set — a `RecordField`
62
+ with no grant row for the caller's role never enters the list, so it just isn't in the JSON (200,
63
+ no `messages[]`). This is **beyond the known EV-9 (write) rule: `AclFieldPermissions` gates READ
64
+ VISIBILITY too.** Concrete symptom (TRUE-79191): `Measures.measureType` was registered but ungranted,
65
+ so it came back absent and the frontend unit dropdowns were empty. When a field renders blank with no
66
+ error, check for a missing `AclFieldPermissions` grant before suspecting the selector or the data.
56
67
  - **`AclFieldPermissions` is `UNIQUE (recordFieldId, roleId)`** (Client blank DDL) — guard inserts
57
68
  with `NOT EXISTS`. For a **read-only, server-written** field (one populated by a `postPost`
58
69
  interceptor rather than the API caller), grant `isWritable = 0`.
@@ -168,6 +179,16 @@ rejects the write with a **specific code** (see the
168
179
  writable sibling field** on the same record — pick a sibling that actually has explicit
169
180
  `AclFieldPermissions` rows (e.g. on `TrackingNumbers`, `containsBattery` has them, whereas
170
181
  `requiresSignature` had none), and resolve `roleId` by subselect.
182
+ **⚠ When the grant FANS OUT across ALL client DBs, mirror a STABLE, universally-present sibling —
183
+ not a recently-added one.** A migration that copies a sibling's grants only works on a client where
184
+ that sibling *itself* has grants. Mirror a long-established field (e.g. the FK `shippingMethodId`)
185
+ whose `AclFieldPermissions` rows exist in **every** client DB. Mirroring a newly-added field
186
+ **fails silently** on clients where that field's own grants haven't propagated: TRUE-79191's
187
+ measure-id grant first mirrored `needsReturnLabel` and ended up ungranted on `Client_True`
188
+ (verified: `Client_True` had `shippingMethodId` grants but **no** `needsReturnLabel` grant) → the
189
+ new field was EV-9 on write / omitted on read there. Because the clone reads each client's **own**
190
+ sibling rows, it also correctly adapts to that client's role set (Growrk role 1; Compass roles
191
+ 1,3,4) — another reason to resolve by subselect, never hardcode.
171
192
  4. **Generated model-class declaration** (`_underscore/Model/Client/<Name>.php`) — the field must
172
193
  be declared on the **generated** PHP model class, e.g.
173
194
  `public $needsReturnLabel = self::FIELD_BOOLEAN;`. **Without it the API returns `EO-1`**
@@ -206,6 +227,16 @@ the field.
206
227
 
207
228
  ## Change history
208
229
 
230
+ - **2026-07-22** — TRUE-79191: documented that the V2 GET **OUTPUT** field list is itself built from
231
+ `AclFieldPermissions` (`getAclFieldPermissions` + the output `foreach` loops in
232
+ `api2/.../V2.php`), so an **ungranted field is SILENTLY OMITTED** from the response (200, no
233
+ `messages[]`) — read visibility is gated, not only writes (beyond the EV-9 write rule). Symptom:
234
+ `Measures.measureType` absent → empty unit dropdowns. Also added the **cross-client fan-out rule**:
235
+ a grant migration that clones a sibling must mirror a **stable, universally-present** sibling
236
+ (e.g. `shippingMethodId`), not a recently-added one — mirroring `needsReturnLabel` left the new
237
+ measure-id field ungranted on `Client_True` (which had `shippingMethodId` grants but no
238
+ `needsReturnLabel` grant); cloning each client's own rows also adapts to its role set
239
+ (Growrk role 1; Compass 1,3,4). (mhammontree)
209
240
  - **2026-07-23** — TRUE-79533: noted that a **read-only field only needs a grant on the role(s) that
210
241
  actually read it** — mirror a sibling field of the same kind the readers already consume rather than
211
242
  granting every role. The standard FK `serviceAddressId` mirrored `saleItemId`'s reader, **role 1** in
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-07-09
10
- owners: ["apeterson"]
9
+ updated: 2026-07-27
10
+ owners: ["apeterson", "mhammontree"]
11
11
  files:
12
12
  - api2/Config/
13
13
  - _underscore/Loader.php
@@ -87,6 +87,14 @@ Order matters — dump first so a failed dump never wipes a local schema it can'
87
87
  `Client_CompassCanada` selected as the active DB in your client — a standalone `USE` line run
88
88
  as a separate statement does **not** carry over, and the tables land in the wrong schema. See
89
89
  [tracking-number-bridges](../features/tracking-number-bridges.md) for the bridge tables.
90
+ - **⚠ Pulling updated models from prod WITHOUT applying the matching `dbchanges2` migrations locally
91
+ → `1054 Unknown column`.** The mirror image of the missing-model 500 above: if you `git pull`
92
+ `_underscore` models that reference a column a **newer migration** adds, but your local client DB
93
+ hasn't run that migration, queries fail with `1054 Unknown column`. Concrete case:
94
+ `Items.isFulfillable` (added by the 2026-07-17 migration) — the pulled Item model selected the
95
+ column before the local schema had it. **Fix:** apply the pending `dbchanges2` migrations in date
96
+ order, or reset the local client DB(s) from a beta snapshot (the steps above). Keep local
97
+ `_underscore` **and** the local schema on the same migration point.
90
98
  - **The logs-DB write trap still applies.** A reset only refreshes the schemas you loop over; if
91
99
  you skip `Logs_<Id>` the first request 500s (`Unknown database 'logs_<id>'`) on the logging
92
100
  write. See [per-client DB connections](../features/per-client-database-connections.md).
@@ -97,6 +105,10 @@ Order matters — dump first so a failed dump never wipes a local schema it can'
97
105
 
98
106
  ## Change history
99
107
 
108
+ - 2026-07-27 — TRUE-79191: added the reverse-drift gotcha — pulling updated `_underscore` models from
109
+ prod **without** applying the matching `dbchanges2` migrations locally throws `1054 Unknown column`
110
+ (e.g. `Items.isFulfillable` from the 2026-07-17 migration); apply pending migrations or reset the
111
+ local client DB from a beta snapshot. (mhammontree)
100
112
  - 2026-07-09 — Initial capture: non-locking `mysqldump` recipe (beta `admin` lacks `RELOAD`), the
101
113
  VPN-only beta host, and the two recurring post-reset side-effects — a missing `_Model_Client_*`
102
114
  autoload 500 (pull the model file, no cache) and the CompassCanada 17-table drift
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-07-09
9
+ updated: 2026-07-27
10
10
  owners: ["mhammontree"]
11
11
  files:
12
12
  - _underscore/Environment.php
@@ -14,6 +14,7 @@ files:
14
14
  - _underscore/Database.php
15
15
  - _underscore/Route.php
16
16
  - api2/Component/Api/V2/V2.php
17
+ - api2/Controller/Index.php
17
18
  - api2/index.php
18
19
  - api2/.htaccess
19
20
  related:
@@ -95,6 +96,15 @@ Checked in `V2.php` (~lines 485–800):
95
96
 
96
97
  ## Gotchas / known issues
97
98
 
99
+ - **⚠ On a locked-down Windows dev box, PCRE JIT crashes EVERY api2 route with a 500.** During api2
100
+ bootstrap (`Controller/Index.php`), `Sentry\init()` runs a `preg_split()` that fails with
101
+ **`preg_split(): Allocation of JIT memory failed, PCRE JIT will be disabled`** — on a hardened
102
+ Windows Enterprise machine the OS refuses the executable-memory (W^X) allocation the PCRE JIT needs.
103
+ The `_underscore` error handler **escalates that warning to a fatal**, so **every V2 route returns
104
+ 500**, masquerading as a code bug. **Fix (local only): set `pcre.jit=0` in `php.ini` and restart
105
+ Apache.** Nothing to ship — beta/prod PHP is permitted to allocate executable memory, so the JIT
106
+ works there and there is no code change.
107
+
98
108
  - **The silent `Route.php:525` error** — `"Failed to determine how to render view for route
99
109
  '/v2/...'"` (thrown at `_underscore/Route.php:525`) means the API handler returned **no**
100
110
  response, so `_Route` fell through to view rendering. It is thrown during bootstrap
@@ -118,6 +128,10 @@ Checked in `V2.php` (~lines 485–800):
118
128
 
119
129
  ## Change history
120
130
 
131
+ - 2026-07-27 — TRUE-79191: added the **PCRE JIT** gotcha — on a locked-down Windows Enterprise dev
132
+ box `Sentry\init()` (`api2/Controller/Index.php`) hits `preg_split(): Allocation of JIT memory
133
+ failed`, which the `_underscore` error handler escalates to a fatal, 500ing **every** V2 route;
134
+ fix is `pcre.jit=0` in `php.ini` + Apache restart (local only — nothing to ship). (mhammontree)
121
135
  - 2026-07-09 — Documented the full local browser-run dependency chain (Apache `ENVIRONMENT`,
122
136
  `dev-*`→`dev` slug collapse, Core-driven client-DB registration, `/v2/auth/login` browser gate,
123
137
  the `Route.php:525` silent-fallthrough gotcha, and the stale-Core "missing model file" fatal),
@@ -3,5 +3,5 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [TOGa Supply (toga2-supply) Architecture](architecture.md) | `toga2-supply` is the **React + Vite frontend** for TOGa Supply — warehouse fulfillment tooling (shipment selection, fulfill & ship against carrier APIs, NetSui | toga2-supply/src/api/toga.ts, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx |
6
- | [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx, toga2-supply/src/components/ui/Tables/types.ts, toga2-supply/src/components/ui/GoogleMapsLink.tsx, toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx, toga2-supply/src/pages/ShipmentItems/api/ShipmentItemsApi.ts, toga2-supply/src/pages/ShipmentItems/types.ts, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/RETURNLABELFIELDS.json, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx, toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/styles/index.scss, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts, toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
6
+ | [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | _underscore/Model/Client/Measure.php, _underscore/Model/Client/TrackingNumber.php, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/getEditShipmentFormOptions.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/validateFormOnSubmit.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/checkDimensions.ts, toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx, toga2-supply/src/components/ui/Tables/types.ts, toga2-supply/src/components/ui/GoogleMapsLink.tsx, toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx, toga2-supply/src/pages/ShipmentItems/api/ShipmentItemsApi.ts, toga2-supply/src/pages/ShipmentItems/types.ts, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/RETURNLABELFIELDS.json, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx, toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/styles/index.scss, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts, toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
7
7
  | [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-supply` (React + Vite) builds and deploys on **AWS Amplify**. | toga2-supply/amplify.yml, toga2-supply/.gitattributes, toga2-supply/.github/workflows/sync-stage-environments.yml, toga2-supply/.env.qc-security |
@@ -6,9 +6,14 @@ project: TOGa Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-21
9
+ updated: 2026-07-27
10
10
  owners: [mhammontree]
11
11
  files:
12
+ - _underscore/Model/Client/Measure.php
13
+ - _underscore/Model/Client/TrackingNumber.php
14
+ - toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/getEditShipmentFormOptions.ts
15
+ - toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/validateFormOnSubmit.ts
16
+ - toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/checkDimensions.ts
12
17
  - toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx
13
18
  - toga2-supply/src/components/ui/Tables/types.ts
14
19
  - toga2-supply/src/components/ui/GoogleMapsLink.tsx
@@ -302,26 +307,41 @@ existing IF), `fulfillShipment` + `generateReturnLabel` (label), `saveShipmentTo
302
307
  (push-back). New pieces needed: the punch-out entry carrying an IF id, and a **label-only mode**
303
308
  (skip the create step). This is a separate feature/ticket — recorded as planned direction, not done.
304
309
 
305
- ## Planned direction persist package dimension + volume Units of Measure (design, 2026-07-21)
306
-
307
- **Design decision recorded for future work (not implemented this session).** Today the package
308
- dimension in./cm selector is **visual-only** and `TrackingNumbers` stores `length`/`width`/`height`
309
- with **no unit**. The plan is to persist the package **DIMENSION** unit (for L/W/H) and the **VOLUME**
310
- unit on the tracking record.
311
-
312
- - **Recommended design = ZERO new tables, only columns.** REUSE the existing client **`Measures`**
313
- table + **`_Model_Client_Measure`** model (already referenced by `Items.defaultMeasureId`): add a
314
- **`measureType` ENUM** (LENGTH / VOLUME / WEIGHT / COUNT per Jeff the types are enumerators, so
315
- **no separate `MeasureTypes` table**) plus a **`conversionFactorToBase`** column, and add
316
- **`dimensionMeasureId` / `volumeMeasureId`** FK columns on **`TrackingNumbers`**.
317
- - **Carrier constraint drives the conversion factor.** FedEx/UPS ship APIs accept only **IN/CM**
318
- dimension units and **LB/KG** weight units, so the chosen measure must map/convert to the carrier
319
- enum hence `conversionFactorToBase`. **Volume is NOT sent to carriers** (dimensional weight is
320
- derived from L/W/H).
321
- - **Alternative (documented for opt-in):** two dedicated new tables instead of reusing `Measures`.
322
- - Spans `_underscore` (Measure/TrackingNumber models), `dbchanges2`, and `api2`; full plan + the
323
- deployment order live in external working files (`package_units_of_measure_plan.md`,
324
- `TRUE-79191_deployment_order.md`) not mirrored into the KB.
310
+ ## Package Units of Measure (weight + dimensions) IMPLEMENTED (TRUE-79191, 2026-07-22)
311
+
312
+ The package **weight** and **dimensions** on the Update Info / Edit Shipment form now have **real
313
+ unit selectors** (base **Pound** / **Inch**), replacing the earlier visual-only placeholder. The
314
+ chosen units **persist on the tracking number** and **rehydrate on reopen**. Verified end-to-end
315
+ (the saved `TrackingNumber` carries the selected measure ids from the frontend create payload).
316
+ Commit `e91f7029d`. **Supersedes the earlier design** (VOLUME and COUNT dropped entirely;
317
+ `dimensionMeasureId` `lengthMeasureId`; `weightMeasureId` added).
318
+
319
+ - **Data model ZERO new tables (design confirmed with product owner Jeff).** Reuse the existing
320
+ client **`Measures`** table (`id`/`uuid`/`slug`/`name`/`symbol`) and add two columns:
321
+ **`measureType ENUM('LENGTH','WEIGHT')`** and **`conversionFactorToBase DECIMAL(18,9)`**.
322
+ - **Base units seeded into every client:** **Inch** (LENGTH, factor `1`) and **Pound** (WEIGHT,
323
+ factor `1`). **Convention: `value_in_base = value * conversionFactorToBase`.** Metric (cm/kg) is an
324
+ **opt-in per-client seed** added later; the base always stays Inch/Pound. Carrier constraint is the
325
+ reason a base/conversion exists at all — FedEx/UPS ship APIs accept only IN/CM + LB/KG.
326
+ - **`TrackingNumbers` gains two FKs** (hard DB constraints `Measures`): **`lengthMeasureId`** (one
327
+ unit governs length/width/height) and **`weightMeasureId`**. Existing rows are **backfilled to
328
+ Inch/Pound** (one-time migration; **not** re-run-safe).
329
+ - **Models (`_underscore`):** `_Model_Client_Measure` gains `measureType` (`FIELD_CHAR`) +
330
+ `conversionFactorToBase` (`FIELD_DECIMAL`); `_Model_Client_TrackingNumber` gains
331
+ `lengthMeasureId`/`weightMeasureId` (`FIELD_FOREIGNKEY` → `_Model_Client_Measure`).
332
+ - **api2 needs NO code.** The new fields ride the metadata-driven V2 engine — exposed purely via Core
333
+ `RecordFields` + Client `AclFieldPermissions`. **The `AclFieldPermissions` grant is mandatory for
334
+ READ, not only write** — see the acl-permission-chain doc's field-registration recipe and the
335
+ read-visibility rule (a registered field with no grant is silently omitted from the response, which
336
+ presented here as empty unit dropdowns).
337
+ - **Frontend:** two react-hook-form selects fed by **`GET /v2/measures`**, split client-side by
338
+ `measureType`. **The FK is WRITTEN as a nested relation OBJECT `{uuid}`** (`lengthMeasure` /
339
+ `weightMeasure`), exactly like the sibling `shippingCarrier` — **NOT a scalar id**; read
340
+ field-selectors request `lengthMeasure`/`weightMeasure`. (Same whole-object react-select rule as
341
+ every other select in these forms — see that gotcha.)
342
+ - **Migrations (`dbchanges2`):** `Core/2026-07-22a - MeasureUomRecordFields.sql`,
343
+ `Client/2026-07-22a - MeasureUomColumns.sql`, `Client/2026-07-22b - TrackingNumberMeasureIds.sql`,
344
+ `Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql`.
325
345
 
326
346
  ## Sources — meeting notes / decision origins
327
347
 
@@ -598,6 +618,24 @@ the call 403s and returns no label.
598
618
  programmatic `setValue` for a user-meaningful prefill must pass `{ shouldDirty: true }` or the form
599
619
  still reads "clean" (also affects the mint dirty-border above). File:
600
620
  `EditShipment/view/modals/ReturnShippingModal.tsx`.
621
+ - **⚠ A submit handler must branch on a validation helper's RETURN value, NOT on the same-tick
622
+ `errors` snapshot after `setError`.** Same RHF-formState-lag family as the `isDirty` gotcha above:
623
+ the destructured `errors` object is one render **behind** `setError`/`clearErrors`, so a helper that
624
+ sets field errors and a caller that then reads `Object.keys(errors)` on the **same tick** see a
625
+ stale (empty) `errors` — the FIRST submit sailed past validation straight to the API even with
626
+ invalid fields. **Two rules for any toga2-supply form doing manual RHF validation on submit:**
627
+ (1) the helper must **RETURN a fresh boolean/result** and the caller must branch on that return
628
+ value, never on the same-tick `errors` snapshot; (2) the helper must **`clearErrors` on the
629
+ now-valid path**, not only `setError` on the invalid path. *Concrete failure:* `checkDimensions`
630
+ had branches for every partial L/W/H combination and for all-absent but **none for all-present**,
631
+ so once a partial submit flagged a dimension, completing all three never cleared it and submit
632
+ stayed blocked in RHF state **permanently**. Fix: `validateFormOnSubmit.ts` and `checkDimensions.ts`
633
+ now return a boolean (`hasErrors`) and clear errors on the valid state; `EditShipmentForm.tsx`
634
+ branches on the returned result. (This validation now also runs in **edit mode** — detail/address
635
+ fields were previously skipped on the edit save path.) Files:
636
+ `EditShipment/helpers/ShipmentDetailsForm/validateFormOnSubmit.ts`,
637
+ `EditShipment/helpers/ShipmentDetailsForm/checkDimensions.ts`,
638
+ `EditShipment/view/components/forms/EditShipmentForm.tsx`.
601
639
  - **⚠ api2 V2 field SELECTORS are dot-paths of REAL DB column names — a wrong name silently returns
602
640
  nothing.** V2 responses select fields by dot-path (e.g.
603
641
  `"itemFulfillmentItems.salesOrderItem.item.skuNumber"`). The real `Items` columns are **`skuNumber`
@@ -671,6 +709,17 @@ not the base `_Model_Client_ItemFulfillment`. Tested with GroWrk; UPS support wa
671
709
  for Compass and is not yet in prod.
672
710
 
673
711
  ## Change history
712
+ - 2026-07-22 — TRUE-79191 (commit `e91f7029d`, full-stack): **IMPLEMENTED package Units of Measure** —
713
+ the package weight + dimensions now have real Pound/Inch unit selectors that persist on the
714
+ `TrackingNumber` and rehydrate on reopen (verified end-to-end). ZERO new tables: reused the client
715
+ `Measures` table + `_Model_Client_Measure` with `measureType ENUM('LENGTH','WEIGHT')` +
716
+ `conversionFactorToBase DECIMAL(18,9)` (base Inch/Pound factor 1, `value_in_base = value *
717
+ conversionFactorToBase`, metric opt-in later); `TrackingNumbers` gained `lengthMeasureId` (governs
718
+ L/W/H) + `weightMeasureId` FKs (existing rows backfilled Inch/Pound, one-time). **VOLUME + COUNT
719
+ dropped** and `dimensionMeasureId` → `lengthMeasureId` vs the earlier design. api2 needed NO code
720
+ (metadata-driven: Core `RecordFields` + Client `AclFieldPermissions`); the FK is written as a nested
721
+ relation object `{uuid}` like `shippingCarrier`. Migrations `Core/2026-07-22a`, `Client/2026-07-22a
722
+ /b/c`. (mhammontree)
674
723
  - 2026-07-21 — TRUE-79191 (commit `651e9d9d0`, toga2-supply UI fixes + UoM design): fixed the shared
675
724
  **`GoogleMapsLink`** mapping the recipient instead of the address (excluded `addressee`; read
676
725
  `street || line1` not `line1`; killed an always-true empty-part filter and a `replace`+`encodeURIComponent`
@@ -769,6 +818,13 @@ for Compass and is not yet in prod.
769
818
  the form via `getValueFromPath`'s unguarded `.split`). Remaining Figma tweaks (Dimensions in./cm
770
819
  dropdown + gaps, Item Count right-align, return-label row gaps, shipment-card indicator top gap) were
771
820
  cosmetic — no doc change. (mhammontree)
821
+ - 2026-07-27 — TRUE-79191 (CodeRabbit review pass): fixed the Edit Shipment form's **first-submit
822
+ validation bypass** — `validateFormOnSubmit`/`checkDimensions` now RETURN a boolean and the submit
823
+ handler branches on that instead of the stale same-tick `errors` snapshot; `checkDimensions` now
824
+ clears errors on the all-present dimensions path (previously had no all-present branch, so a partial
825
+ submit could block submit permanently). Edit mode now runs the same detail/address validation
826
+ (previously skipped) and `updateShipment` returns step-identifying failures. Recorded as a reusable
827
+ RHF-validation gotcha. (mhammontree)
772
828
  - 2026-07-14 — TRUE-79191 (commit `724d17c1a`): fixed **Signature Type** not saving/rehydrating —
773
829
  documented the reusable **whole-object react-select** convention (`{uuid,name}`; map on `.uuid`,
774
830
  rehydrate the object) as a gotcha; the string comparison `data.signatureType === "Required"`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.445",
3
+ "version": "1.0.447",
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",