toga-ai 1.0.362 → 1.0.364

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.
@@ -112,6 +112,30 @@ Worked example: the reprint API `GET /v2/tracking-numbers/reprint`
112
112
  Client-DB `AclRecordScripts` grant (migrations in `dbchanges2`) before it stopped 403ing — see the
113
113
  carrier-shipping-labels feature doc.
114
114
 
115
+ ## Add a writable field to a V2 record (3-file migration)
116
+
117
+ Adding a persisted, API-writable field to an existing V2 record is a **three-file migration set**
118
+ — miss any one and the API rejects the write with a specific code (see the
119
+ [V2 API error codes](../../api2/features/v2-api-error-codes.md) map):
120
+
121
+ 1. **Core `RecordFields` registration** (`dbchanges2/Core/`) — register the field name + type
122
+ (e.g. `'STRING'`) against the record. Resolve the `recordId` by **route**, not a hardcoded id.
123
+ **Without it the API returns `EV-8`** ("field does not exist").
124
+ 2. **Client column** (`dbchanges2/Client/`) — `ALTER TABLE <Table> ADD <field> <type>`, guarded
125
+ by an `INFORMATION_SCHEMA` existence check so it is safe to replay and safe against the
126
+ `BLANK_CLIENT_DATABASE` template.
127
+ 3. **Client `AclFieldPermissions` grant** (`dbchanges2/Client/`) — grant the caller's role(s)
128
+ read/write on the new field. **Without it the API returns `EV-9`** ("no permission to write
129
+ field"). The reliable way to get the right rows is to **clone the grants of an existing
130
+ writable sibling field** on the same record — pick a sibling that actually has explicit
131
+ `AclFieldPermissions` rows (e.g. on `TrackingNumbers`, `containsBattery` has them, whereas
132
+ `requiresSignature` had none), and resolve `roleId` by subselect.
133
+
134
+ Worked example (TRUE-79191, `TrackingNumbers.signatureType`): `Core/... - TrackingNumberSignatureTypeRecordField.sql`
135
+ (RecordField, `STRING`), `Client/... - TrackingNumberSignatureType.sql` (guarded `ALTER TABLE`),
136
+ `Client/... - TrackingNumberSignatureTypeFieldPermission.sql` (cloned `containsBattery`'s grants).
137
+ This mirrors the earlier `needsReturnLabel` / `returnAddressId` 3-file sets.
138
+
115
139
  ## Checklist (so the chain is never half-built)
116
140
 
117
141
  - [ ] `AclRecordPermissions` row for the role × record with the right CRUD flags.
@@ -123,6 +147,10 @@ carrier-shipping-labels feature doc.
123
147
 
124
148
  ## Change history
125
149
 
150
+ - **2026-07-17** — TRUE-79191: added the **"add a writable field to a V2 record" 3-file migration**
151
+ recipe (Core `RecordFields` → `EV-8` if missing; guarded Client column; Client `AclFieldPermissions`
152
+ → `EV-9` if missing, cloned from a writable sibling like `containsBattery`), worked example
153
+ `TrackingNumbers.signatureType`. Cross-linked the new V2 API error-codes map. (mhammontree)
126
154
  - **2026-07-02** — Added the record-script dispatch grant (`AclRecordScripts`) as a gate distinct
127
155
  from the record-CRUD chain, with the full recipe for exposing a scripted API on a `_Model`
128
156
  (Core.RecordScripts route + Client `AclRecordScripts` grant + `(&$api, …)` method), discovered
@@ -53,6 +53,24 @@ scripted endpoint in **`Core.RecordScripts`** (id 17: `recordId` 13 = Addresses,
53
53
  route = validateAddress) returns to **`data.addresses.validateAddress`**, which is exactly
54
54
  what the frontend reads.
55
55
 
56
+ ## `GET /addresses/validate` — the migration-registered route (2026-07)
57
+
58
+ TRUE-79191 added a **second** route to the same `validateAddress` PHP method, this time **registered
59
+ in `dbchanges2` source** (closing the parity gap noted below) rather than only in the live DB:
60
+
61
+ - **Route** — `Core.RecordScripts` row: `recordId` = Addresses, `method` `GET`, route **`validate`**,
62
+ `phpMethod` `validateAddress` (`dbchanges2/Core/2026-07-16b - AddressValidateRecordScript.sql`).
63
+ - **Dispatch grant** — Client `AclRecordScripts` grant to roles **Public (1) / Super User (3) /
64
+ Base (4)** (`dbchanges2/Client/2026-07-16c - AddressValidateScriptAcl.sql`). Without it the call
65
+ 403s with `EZ-1`.
66
+ - **Response path** — because the route is `validate`, the result lands at
67
+ **`data.addresses.validate`** (the Fulfill & Ship return-address modal reads exactly this;
68
+ distinct from the older global `validateAddress` route's `data.addresses.validateAddress`).
69
+
70
+ The PHP method (`_Model_Client_Address::validateAddress`) already existed and was unchanged — it was
71
+ simply **unrouted in source** until this migration set exposed it. New scripted routes should follow
72
+ this pattern (route + `AclRecordScripts` both in `dbchanges2`).
73
+
56
74
  ## Client variations
57
75
 
58
76
  None — this is shared engine + framework behavior. The carrier credentials are resolved
@@ -68,6 +86,11 @@ per the standard client carrier config.
68
86
 
69
87
  ## Change history
70
88
 
89
+ - 2026-07-17 — TRUE-79191: added a **`dbchanges2`-registered** `GET /addresses/validate` route to the
90
+ same `validateAddress` method (Core `RecordScripts` route `validate` + Client `AclRecordScripts`
91
+ grant to Public/SuperUser/Base) so the Fulfill & Ship return-address modal can validate on save;
92
+ response at `data.addresses.validate`. Method was pre-existing but unrouted. Closes the source/DB
93
+ parity gap going forward. (mhammontree)
71
94
  - 2026-07-07 — Documented `_Model_Client_Address::validateAddress` (USPS→FedEx→UPS waterfall
72
95
  + normalized success shape) and the global `GET /addresses/validateAddress`
73
96
  (`Core.RecordScripts` id 17) endpoint, including the dispatch precedence, the
@@ -145,6 +145,29 @@ NetSuite** at all. It now iterates the `itemFulfillmentTrackingNumbers` bridge a
145
145
  (`packageDescr = 'Return Label'`). (The 2026-06-16 bridge repoint fixed the four queries in
146
146
  `Model/Client/ItemFulfillment.php`; this trait method was a separate straggler on the same table.)
147
147
 
148
+ ## Signature type on the label (implemented 2026-07)
149
+
150
+ Signature was **never applied to labels before** — the carrier libs ignored it. Fulfill & Ship now
151
+ carries a **4-level signature type** (None Specified / Indirect / Direct / Adult) end-to-end onto the
152
+ outbound label:
153
+
154
+ - **Storage & field.** `TrackingNumbers.signatureType` (`VARCHAR(255)`), exposed as
155
+ `_Model_Client_TrackingNumber::$signatureType = self::FIELD_CHAR` and registered as a Core
156
+ RecordField + Client `AclFieldPermissions` grant (the 3-file migration — see the ACL doc).
157
+ - **Threading.** A **generic `$signatureType`** field was added to
158
+ `Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php`. Both outbound paths
159
+ (`fedexShipmentApi`, `upsShipmentApi` in `Model/Client/ItemFulfillment.php`) now **SELECT
160
+ `TrackingNumbers.signatureType`** and pass it into the request. **Return-label flows are
161
+ untouched — no signature on returns.**
162
+ - **FedEx mapping** (`Component/Library/Carriers/Fedex/Fedex.php`): maps the level to per-package
163
+ `packageSpecialServices.signatureOptionType` → `INDIRECT` / `DIRECT` / `ADULT`. **None Specified
164
+ is sent explicitly as `NO_SIGNATURE_REQUIRED`.**
165
+ - **UPS mapping** (`Component/Library/Carriers/Ups/Ups.php`): maps to
166
+ `Package.PackageServiceOptions.DeliveryConfirmation.DCISType` (`2` = Signature Required, `3` =
167
+ Adult) on **both the OAuth and the legacy payloads**. **⚠ UPS has no Indirect tier — Indirect
168
+ falls back to Signature Required (`2`).** None Specified is **omitted** (UPS default, no
169
+ confirmation).
170
+
148
171
  ## `FIELD_STORAGE` mechanics (reference)
149
172
 
150
173
  `Model.php` handles storage fields separately from regular columns (write loop ~line
@@ -259,6 +282,14 @@ still discards the unsaved in-memory value.
259
282
  bill) — expect a hard error rather than a silently-wrong bill in those cases.
260
283
 
261
284
  ## Change history
285
+ - 2026-07-17 — TRUE-79191: applied **signature type** to outbound labels for the first time (carrier
286
+ libs previously ignored it). Added a generic `$signatureType` on `ShipmentRequest`;
287
+ `fedexShipmentApi`/`upsShipmentApi` now SELECT + pass `TrackingNumbers.signatureType`; FedEx maps to
288
+ `packageSpecialServices.signatureOptionType` (INDIRECT/DIRECT/ADULT, None → explicit
289
+ `NO_SIGNATURE_REQUIRED`); UPS maps to `DeliveryConfirmation.DCISType` (2=Signature, 3=Adult) on both
290
+ OAuth + legacy payloads, **Indirect falls back to 2 (UPS has no Indirect tier)**, None omitted.
291
+ Return-label flows untouched. `TrackingNumbers.signatureType` column + `$signatureType=FIELD_CHAR`
292
+ added. (mhammontree)
262
293
  - 2026-07-14 — TRUE-79191 return-label increment: `generateReturnLabel` ships from the **per-fulfillment** return address (`returnAddressId` → Addresses/States, falling back to `RETURN_ADDRESS_*`; supersedes the fixed `RETURN_ADDRESSES` set) and returns `returnTrackingNumberUuid`; documented the return-leg reuse semantic (return reuses outbound weight/carrier/method/account — only the address is independent). Fixed `Trait/Netsuite/ItemFulfillment.php::createNetsuiteItemFulfillment`, which read the retired `itemFulfillmentPackages` so no tracking reached NetSuite — now iterates the `itemFulfillmentTrackingNumbers` bridge and pushes outbound + return (2nd package, `packageDescr='Return Label'`). `reprintLabelsApi` `$caption` now accepts a position-aligned CSV for per-page captions. Added the local live-UPS testing setup gotcha (`[ups]` OAuth config, CIE endpoint/shipper, `ShippingMethods.code`). (mhammontree)
263
294
  - 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)
264
295
  - 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)
@@ -11,5 +11,6 @@
11
11
  | [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
12
12
  | [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TableView.php, toga2-supply/src/api/toga.ts |
13
13
  | [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
14
+ | [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php |
14
15
  | [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | |
15
16
  | [New Environment Configuration & Provisioning (api2)](workflows/environment-configuration-and-provisioning.md) | What it takes for a 2.0 API environment (e.g. | api2/Config/<environment>.ini, api2/Controller/Index.php, dbchanges2/Core/2026-06-16a - DatabaseHosts for new QA QC stage demo environments.sql, _underscore/Route.php |
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: "V2 API error/message codes (EV/EZ troubleshooting map)"
3
+ framework: "2.0"
4
+ repo: api2
5
+ project: API
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-17
10
+ owners: [mhammontree]
11
+ files:
12
+ - api2/Component/Api/V2/V2.php
13
+ related:
14
+ - ../../_underscore/features/acl-permission-chain.md
15
+ - scripted-api-post-body-args.md
16
+ - ../architecture.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the
22
+ response `error` field, grouped by family: `EN-*` authentication, `EZ-*` authorization,
23
+ `EV-*` validation, `EO-*` operation, plus `W*`/`D*`. This doc is the **troubleshooting map**
24
+ for the codes you hit when adding a field or a scripted API to a V2 record — each with its
25
+ root cause and the exact fix — so a teammate can go straight from the code to the missing
26
+ migration instead of re-deriving it. All field/script ACL rows live in the **CLIENT** DB
27
+ (`Records.aclDatabase = 'CLIENT'`), keyed by the caller's `roleId`.
28
+
29
+ ## The codes (cause → fix)
30
+
31
+ | Code | Meaning | Root cause | Fix |
32
+ |------|---------|------------|-----|
33
+ | **EV-6** | Resource / route not found | A **scripted-API** route segment is being treated as a record uuid because no Record Script matches — i.e. the `Core.RecordScripts` route row is missing | Register the `Core.RecordScripts` row (`recordId`, `method`, `route`, `phpMethod`) |
34
+ | **EV-8** | Request field does not exist | A field sent in the payload isn't registered as a **`Core.RecordFields`** row for that record | Register the field in `Core.RecordFields` (see the "add a field to a V2 record" recipe in the ACL doc) |
35
+ | **EV-9** | No permission to write field | The field exists but the caller's role has no **`AclFieldPermissions`** grant (`isWritable=1`) in the CLIENT DB | Add the `AclFieldPermissions` row for the role (clone a writable sibling field's grant) |
36
+ | **EZ-1** | Unauthorized record/script dispatch | Missing **`AclRecordScripts`** (scripted APIs) or the **`AclRecordPermissions`** four-table chain (records) for the caller's role | Grant `AclRecordScripts` (scripts) or complete the record-CRUD chain |
37
+ | **EV-5** | Duplicate `transactionId` | The globally-unique `transactionId` was reused | Send a fresh unique `transactionId` per request |
38
+ | **EV-12** | Record's parent not synced | (Fulfill & Ship) POST `/item-fulfillments` when the Sales Order isn't synced into Toga yet | `GET /sales-orders/syncNetsuiteSalesOrder?netsuiteInternalSalesOrderId=<id>` first |
39
+
40
+ ## How to diagnose
41
+
42
+ 1. **Resolve the role.** Field/script ACLs are keyed by `roleId` **in the CLIENT DB**, and role
43
+ ids differ per client — resolve by subselect (`SELECT id FROM Roles WHERE name='Base'`), never
44
+ hardcode. To find the right grant shape, inspect a **working sibling** field/script's rows.
45
+ 2. **EV-8 vs EV-9** are the two halves of adding a writable field: EV-8 = the Core `RecordFields`
46
+ registration is missing; EV-9 = the field exists but the Client `AclFieldPermissions` grant is
47
+ missing. You will typically hit EV-8 first, fix it, then hit EV-9.
48
+ 3. **EV-6 vs EZ-1** are the two halves of exposing a scripted API: EV-6 = no `Core.RecordScripts`
49
+ route (the segment falls through to record lookup); EZ-1 = the route exists but there's no
50
+ `AclRecordScripts` dispatch grant for the caller's role.
51
+
52
+ ## Related recipes
53
+
54
+ - **Add a writable field to a V2 record (3-file migration)** and **expose a scripted API**
55
+ (RecordScript + AclRecordScripts): see
56
+ [ACL Permission Chain](../../_underscore/features/acl-permission-chain.md).
57
+ - The response-envelope shape and code families: see [api2 architecture](../architecture.md).
58
+
59
+ ## Change history
60
+
61
+ - 2026-07-17 — TRUE-79191: documented the EV-6/EV-8/EV-9/EZ-1 troubleshooting map (plus EV-5/EV-12)
62
+ discovered wiring the Signature Type field and the `addresses/validate` scripted route on Fulfill
63
+ & Ship — each code mapped to the missing migration (RecordField / AclFieldPermissions /
64
+ RecordScripts / AclRecordScripts) and the resolve-role-by-sibling technique. (mhammontree)
65
+ </content>
66
+ </invoke>
@@ -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/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/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 | 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 |
@@ -21,6 +21,8 @@ files:
21
21
  - toga2-supply/src/styles/index.scss
22
22
  - toga2-supply/src/components/ui/BaseInput/BaseInput.tsx
23
23
  - toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts
24
+ - toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts
25
+ - toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx
24
26
  - toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts
25
27
  - toga2-supply/src/pages/EditShipment/types.ts
26
28
  - toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx
@@ -209,6 +211,42 @@ bare string** — see the gotcha below before wiring a new select field.
209
211
  12px check glyph. This is the **shared** checkbox, so the change applies **app-wide**, not just to
210
212
  the shipment forms — audit other consumers when touching it.
211
213
 
214
+ ## Signature Type — 4-level enum (2026-07)
215
+
216
+ Signature moved from a **boolean `requiresSignature`** to a **4-level `signatureType` enum**:
217
+ **None Specified / Indirect / Direct / Adult** (cards read "No / Indirect / Direct / Adult Signature
218
+ Required"). It persists on `TrackingNumbers.signatureType` and is now applied to the outbound label
219
+ (see the carrier-shipping-labels doc's signature-type mapping + the UPS "no Indirect tier" rule).
220
+
221
+ - **Single source of truth:** `src/pages/EditShipment/viewModel/signatureTypes.ts` holds the dropdown
222
+ options, the card-label helper `signatureCardLabel`, the legacy-boolean derivation
223
+ `requiresSignatureFromLevel`, and `SIGNATURE_CARRIER_MAP`. Change signature behavior here, not in
224
+ the components.
225
+ - **Back-compat:** create + edit save paths send `trackingNumber.signatureType` **and still derive
226
+ `requiresSignature`** (via `requiresSignatureFromLevel`) so older consumers keep working. The API
227
+ read field lists request `signatureType`; edit rehydrates from it; shipment cards render a computed
228
+ `signatureLabel` with a legacy fallback for rows saved before the enum.
229
+ - Backend: `TrackingNumbers.signatureType` column + `_Model_Client_TrackingNumber::$signatureType`
230
+ (registered via the 3-file V2-field migration — see the ACL doc).
231
+ - **The whole-object react-select rule still applies** — `signatureType`'s form value is the option
232
+ OBJECT `{uuid,name}`; map on `.uuid`, rehydrate the object (see the react-select gotcha below).
233
+
234
+ ## Return Address validation modal — "Select Return Address" (2026-07)
235
+
236
+ Saving a return address now runs a **carrier validation step** (Figma node 3037-17080). On Save the
237
+ `ReturnShippingModal` calls `validateReturnAddress()` and **always** opens `SelectReturnAddressModal`,
238
+ which shows the carrier-normalized **Suggested Address** vs the **Original** as radios (the Suggested
239
+ heading always shows, with an empty state when the carriers return none), Cancel / Save; the chosen
240
+ address is applied.
241
+
242
+ - **Backend:** `validateReturnAddress()` (`UpdateShipmentApi.ts`) does `GET /addresses/validate`
243
+ (result at `response.data.addresses.validate`) — the migration-registered scripted route on the
244
+ existing USPS→FedEx→UPS `validateAddress` cascade (see the `_underscore` address-validation doc).
245
+ - **⚠ State-code gotcha:** the stored state form value is only `{uuid, name}`, but USPS needs the
246
+ **2-letter code**. `getStates` already fetches `code`, but the `EditShipmentPage` mapping was
247
+ **dropping** it — carry `code` through the state options and resolve it at validation time, or
248
+ validation fails on a missing/blank state.
249
+
212
250
  ## Planned direction — "Fulfill & Ship" from an existing Item Fulfillment (future, Eric)
213
251
 
214
252
  A **third entry mode** is planned (distinct from Select-Items-create and the pencil-edit): a
@@ -459,6 +497,19 @@ not the base `_Model_Client_ItemFulfillment`. Tested with GroWrk; UPS support wa
459
497
  for Compass and is not yet in prod.
460
498
 
461
499
  ## Change history
500
+ - 2026-07-17 — TRUE-79191: replaced the boolean `requiresSignature` with a **4-level `signatureType`
501
+ enum** (None Specified / Indirect / Direct / Adult) — `viewModel/signatureTypes.ts` is the single
502
+ source of truth (options, `signatureCardLabel`, `requiresSignatureFromLevel` back-compat,
503
+ `SIGNATURE_CARRIER_MAP`); save paths send `signatureType` and still derive `requiresSignature`; the
504
+ level is now applied to the outbound label (carrier mapping in the carrier-shipping-labels doc).
505
+ Added the **"Select Return Address" validation modal** (`SelectReturnAddressModal.tsx`;
506
+ `validateReturnAddress()` GET `/addresses/validate` → `data.addresses.validate`; Save always
507
+ validates then shows Suggested-vs-Original) and documented the **state-code gotcha** (carry `code`
508
+ through state options; USPS needs the 2-letter code the `{uuid,name}` value drops). Also: app-wide
509
+ `BaseInput` select-focus now uses `border-1` + inset ring (was `border-2`, which shifted text on
510
+ focus) and `BasicTable` gained `stickyHeader`/`headerCellPadding`/`tableContainerMarginBottom` props;
511
+ the rest of the layout/overflow pass (AuthLayout gap rebalance, page top-gap removals, device-list
512
+ grow-to-cap, shipment-card `w-full`/`min-h-full`) was cosmetic. (mhammontree)
462
513
  - 2026-07-15 — TRUE-79191: made the Edit Return Label **Addressee a searchable combobox** (native
463
514
  `<input list>` + `<datalist>`, filters + allows custom text, prefills via `applyWarehouse`); added a
464
515
  **backdrop-click discard guard** (guarded `BaseModal onClose` on `formState.isDirty`); restyled the
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
19
19
 
20
20
  - **_underscore** (_Underscore) _(framework core)_ — 33 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
21
  - **worker2** (Worker) — 30 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
- - **api2** (API) — 11 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
22
+ - **api2** (API) — 12 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
24
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
25
25
  - **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
- | [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**, VIP auto-approve, and EN/FR localization — lives **entirely i | _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Compass/Usa/ApprovalDecision.php, _underscore/Model/Compass/Canada/ApprovalDecision.php |
5
+ | [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**, VIP auto-approve, and EN/FR localization — lives **entirely i | _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Usa/ApprovalDecision.php, _underscore/Model/Compass/Canada/ApprovalDecision.php |
6
6
  | [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
7
7
  | [Cost Centers — Unit Locations, numeric-only policy](features/cost-centers.md) | 2.0 | A Compass "cost center" — the value a user picks in commerce and that lands on an order — is **not** a `CostCenters` row. | toga2-commerce/src/pages/Cart/api/CartApi.ts, worker1.5/crons/toga2/compass/import_locations.php, _underscore/Model/Compass/SalesOrder.php, api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-06 - RemoveNonNumericCostCenters.sql |
8
8
  | [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql, dbchanges2/Client/2026-07-15a - ExcludeFeeItemsFromItemFulfillmentsForSalesOrdersView.sql |
@@ -6,10 +6,11 @@ project: _Underscore
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-07-16
10
- owners: ["apeterson"]
9
+ updated: 2026-07-17
10
+ owners: ["apeterson", "dfranks"]
11
11
  files:
12
12
  - _underscore/Model/Compass/ApprovalDecision.php
13
+ - _underscore/Model/Compass/SalesOrder.php
13
14
  - _underscore/Model/Compass/Usa/ApprovalDecision.php
14
15
  - _underscore/Model/Compass/Canada/ApprovalDecision.php
15
16
  related:
@@ -54,6 +55,24 @@ Because the divergence is a runtime branch and not an override, do **not** add p
54
55
  the empty subclasses — extend the shared parent and branch there if a genuine tenant difference is
55
56
  needed.
56
57
 
58
+ ### VIP manager auto-approve — the rule and its three (all supervisor-derived) triggers
59
+ A single rule governs VIP auto-approve: a **manager-stage (step 2)** approval auto-approves iff the
60
+ assigned manager's `Users.c_isVip = 1` **AND** the order subtotal
61
+ `<= _Model_Compass_SalesOrder::AUTO_APPROVE_IF_MANAGER_IS_VIP_AND_UNDER_AMOUNT` (= `5000`).
62
+ Subtotal = `SUM(SalesOrderItems.quantity * price)` via `_Model_Client_SalesOrder::_subtotal()`.
63
+
64
+ That rule is evaluated in exactly three places, and **all three derive the manager from a
65
+ supervisor relationship**:
66
+ - **(a) Order creation** — `_Model_Compass_SalesOrder::postPost`, inside
67
+ `elseif (!empty($managerUserUuid))`, where `$managerUserUuid` comes from the requester's
68
+ `Users.supervisorUserId`.
69
+ - **(b) "Order for" / contact change** — `_Model_Compass_SalesOrder::postPut`, only when the PUT
70
+ payload changes `contact`; it re-derives the manager from the *new* contact's supervisor.
71
+ - **(c) Manager reassignment** — `_Model_Compass_ApprovalDecision::handleManagerReassignment`, wired
72
+ into `postPut` **only** (added by TRUE-78851). It writes log notes
73
+ `"Manager reassigned — approval decision reset"` (non-VIP) or
74
+ `"Manager approval skipped due to VIP logic"` (VIP auto-approve).
75
+
57
76
  ### Manager reassignment and the notification list
58
77
  When a step-2 (Manager) approval decision is **reassigned** to a new manager,
59
78
  `_swapManagerEmailAddress()` updates the order's CC/notification list in `SalesOrderEmailAddresses`:
@@ -67,6 +86,22 @@ requester**. All of these comparisons are case-insensitive (`strcasecmp()`), mat
67
86
  `getFilteredCcEmails()`.
68
87
 
69
88
  ## Gotchas / known issues
89
+ - **VIP auto-approve is bypassed when the requester has no supervisor (POST-path gap).** All three
90
+ VIP evaluation triggers above are supervisor-derived. When a requester has
91
+ `Users.supervisorUserId = NULL`, order creation skips the manager branch entirely (no step-2
92
+ decision, no manager). If an admin then *manually adds* a manager, that **CREATES** the step-2
93
+ decision as a POST to `/approval-decisions` → `_Model_Compass_ApprovalDecision::postPost`, which
94
+ calls only `logApprovalDecision` + `handleApprovalDecision` — **neither contains VIP auto-approve
95
+ logic**. So a VIP manager under the $5000 threshold added to a no-supervisor order **never
96
+ auto-approves** and must be manually overridden. TRUE-78851 only wired `handleManagerReassignment`
97
+ into `postPut` (reassigning an existing manager decision), never the first assignment (POST).
98
+ - **Diagnostic signature:** absence of **both** `handleManagerReassignment` log notes in
99
+ `Logs_<Tenant>.Record`, combined with a step-2 decision whose `approvalDecisionTypeId = NULL`
100
+ and `decidedByUserId` = the admin (not the manager). The order event log lives in the logs
101
+ cluster schema `Logs_Compass` (per-client `Logs_<Tenant>`), table `Record`, columns
102
+ `(recordId, primaryKeyId, userId, timestamp, note)`; SalesOrder `RECORD_ID = 14`.
103
+ - Real incident: order SA134461 — VIP manager Spyros Gravas, subtotal $926.47 (both auto-approve
104
+ conditions met) still required manual override because the requester had no supervisor.
70
105
  - **Never drop the order requester from notifications on manager reassignment.** If the outgoing
71
106
  manager happens to also be the order requester (same email), deleting the old-manager address
72
107
  from `SalesOrderEmailAddresses` would silently remove the requester from all order notifications.
@@ -81,6 +116,13 @@ requester**. All of these comparisons are case-insensitive (`strcasecmp()`), mat
81
116
  intentionally empty; the tenant branch lives in the parent on `clientIdentifier`.
82
117
 
83
118
  ## Change history
119
+ - 2026-07-17 — Documented the VIP manager auto-approve rule (VIP manager + subtotal ≤ $5000) and
120
+ its three supervisor-derived triggers (`postPost` creation, `postPut` contact change,
121
+ `handleManagerReassignment` on `postPut`), plus the **POST-path gap**: a requester with no
122
+ supervisor never gets VIP auto-approve because a manually-added manager creates the step-2 decision
123
+ via `_Model_Compass_ApprovalDecision::postPost`, which has no VIP logic (TRUE-78851 covered only
124
+ the `postPut` reassignment path). Recorded the diagnostic signature and prod incident SA134461.
125
+ Investigation/planning only — no code shipped. (dfranks)
84
126
  - 2026-07-16 — Fixed `_swapManagerEmailAddress()` dropping the order requester from the order's
85
127
  notification list (`SalesOrderEmailAddresses`) when a step-2 manager was reassigned and the
86
128
  outgoing manager's email also belonged to the requester: now looks up the requester email via
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.362",
3
+ "version": "1.0.364",
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",