toga-ai 1.0.445 → 1.0.446

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/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,12 @@ 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
12
15
  - toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx
13
16
  - toga2-supply/src/components/ui/Tables/types.ts
14
17
  - toga2-supply/src/components/ui/GoogleMapsLink.tsx
@@ -302,26 +305,41 @@ existing IF), `fulfillShipment` + `generateReturnLabel` (label), `saveShipmentTo
302
305
  (push-back). New pieces needed: the punch-out entry carrying an IF id, and a **label-only mode**
303
306
  (skip the create step). This is a separate feature/ticket — recorded as planned direction, not done.
304
307
 
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.
308
+ ## Package Units of Measure (weight + dimensions) — IMPLEMENTED (TRUE-79191, 2026-07-22)
309
+
310
+ The package **weight** and **dimensions** on the Update Info / Edit Shipment form now have **real
311
+ unit selectors** (base **Pound** / **Inch**), replacing the earlier visual-only placeholder. The
312
+ chosen units **persist on the tracking number** and **rehydrate on reopen**. Verified end-to-end
313
+ (the saved `TrackingNumber` carries the selected measure ids from the frontend create payload).
314
+ Commit `e91f7029d`. **Supersedes the earlier design** (VOLUME and COUNT dropped entirely;
315
+ `dimensionMeasureId` → `lengthMeasureId`; `weightMeasureId` added).
316
+
317
+ - **Data model — ZERO new tables (design confirmed with product owner Jeff).** Reuse the existing
318
+ client **`Measures`** table (`id`/`uuid`/`slug`/`name`/`symbol`) and add two columns:
319
+ **`measureType ENUM('LENGTH','WEIGHT')`** and **`conversionFactorToBase DECIMAL(18,9)`**.
320
+ - **Base units seeded into every client:** **Inch** (LENGTH, factor `1`) and **Pound** (WEIGHT,
321
+ factor `1`). **Convention: `value_in_base = value * conversionFactorToBase`.** Metric (cm/kg) is an
322
+ **opt-in per-client seed** added later; the base always stays Inch/Pound. Carrier constraint is the
323
+ reason a base/conversion exists at all — FedEx/UPS ship APIs accept only IN/CM + LB/KG.
324
+ - **`TrackingNumbers` gains two FKs** (hard DB constraints → `Measures`): **`lengthMeasureId`** (one
325
+ unit governs length/width/height) and **`weightMeasureId`**. Existing rows are **backfilled to
326
+ Inch/Pound** (one-time migration; **not** re-run-safe).
327
+ - **Models (`_underscore`):** `_Model_Client_Measure` gains `measureType` (`FIELD_CHAR`) +
328
+ `conversionFactorToBase` (`FIELD_DECIMAL`); `_Model_Client_TrackingNumber` gains
329
+ `lengthMeasureId`/`weightMeasureId` (`FIELD_FOREIGNKEY` → `_Model_Client_Measure`).
330
+ - **api2 needs NO code.** The new fields ride the metadata-driven V2 engine — exposed purely via Core
331
+ `RecordFields` + Client `AclFieldPermissions`. **The `AclFieldPermissions` grant is mandatory for
332
+ READ, not only write** — see the acl-permission-chain doc's field-registration recipe and the
333
+ read-visibility rule (a registered field with no grant is silently omitted from the response, which
334
+ presented here as empty unit dropdowns).
335
+ - **Frontend:** two react-hook-form selects fed by **`GET /v2/measures`**, split client-side by
336
+ `measureType`. **The FK is WRITTEN as a nested relation OBJECT `{uuid}`** (`lengthMeasure` /
337
+ `weightMeasure`), exactly like the sibling `shippingCarrier` — **NOT a scalar id**; read
338
+ field-selectors request `lengthMeasure`/`weightMeasure`. (Same whole-object react-select rule as
339
+ every other select in these forms — see that gotcha.)
340
+ - **Migrations (`dbchanges2`):** `Core/2026-07-22a - MeasureUomRecordFields.sql`,
341
+ `Client/2026-07-22a - MeasureUomColumns.sql`, `Client/2026-07-22b - TrackingNumberMeasureIds.sql`,
342
+ `Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql`.
325
343
 
326
344
  ## Sources — meeting notes / decision origins
327
345
 
@@ -671,6 +689,17 @@ not the base `_Model_Client_ItemFulfillment`. Tested with GroWrk; UPS support wa
671
689
  for Compass and is not yet in prod.
672
690
 
673
691
  ## Change history
692
+ - 2026-07-22 — TRUE-79191 (commit `e91f7029d`, full-stack): **IMPLEMENTED package Units of Measure** —
693
+ the package weight + dimensions now have real Pound/Inch unit selectors that persist on the
694
+ `TrackingNumber` and rehydrate on reopen (verified end-to-end). ZERO new tables: reused the client
695
+ `Measures` table + `_Model_Client_Measure` with `measureType ENUM('LENGTH','WEIGHT')` +
696
+ `conversionFactorToBase DECIMAL(18,9)` (base Inch/Pound factor 1, `value_in_base = value *
697
+ conversionFactorToBase`, metric opt-in later); `TrackingNumbers` gained `lengthMeasureId` (governs
698
+ L/W/H) + `weightMeasureId` FKs (existing rows backfilled Inch/Pound, one-time). **VOLUME + COUNT
699
+ dropped** and `dimensionMeasureId` → `lengthMeasureId` vs the earlier design. api2 needed NO code
700
+ (metadata-driven: Core `RecordFields` + Client `AclFieldPermissions`); the FK is written as a nested
701
+ relation object `{uuid}` like `shippingCarrier`. Migrations `Core/2026-07-22a`, `Client/2026-07-22a
702
+ /b/c`. (mhammontree)
674
703
  - 2026-07-21 — TRUE-79191 (commit `651e9d9d0`, toga2-supply UI fixes + UoM design): fixed the shared
675
704
  **`GoogleMapsLink`** mapping the recipient instead of the address (excluded `addressee`; read
676
705
  `street || line1` not `line1`; killed an always-true empty-part filter and a `replace`+`encodeURIComponent`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.445",
3
+ "version": "1.0.446",
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",