toga-ai 1.0.283 → 1.0.284

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,6 +4,7 @@
4
4
  |-----|---------|-------|
5
5
  | [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
6
6
  | [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
7
+ | [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 |
7
8
  | [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
8
9
  | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
9
10
  | [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: "Address Validation (carrier waterfall + validateAddress scripted endpoint)"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-07
10
+ owners: [mhammontree]
11
+ files:
12
+ - _underscore/Model/Client/Address.php
13
+ related:
14
+ - ../../api2/features/scripted-api-post-body-args.md
15
+ - ../../api2/architecture.md
16
+ - ../../../clients/rate/features/whole-home-warranty-purchase-guard.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ `_Model_Client_Address::validateAddress` verifies a US address against a **carrier
22
+ waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-normalized
23
+ address. It is exposed to the frontend as the **global** scripted endpoint
24
+ `GET /addresses/validateAddress`, and is also callable server-side by any model/interceptor
25
+ that needs to validate + normalize an address before persisting it (e.g. the Rate
26
+ Whole Home Warranty purchase guard).
27
+
28
+ ## Key files / entry points
29
+
30
+ - `_Model_Client_Address::validateAddress(&$api, $address1, $address2, $city, $state, $zip)`
31
+ (`_underscore/Model/Client/Address.php`) — runs USPS, then FedEx, then UPS.
32
+ - **On success** (carriers agree) returns keys: `success:true`, `address1`, `address2`,
33
+ `city`, `state` (2-letter code), `zipCode`, `country`. UPS additionally supplies
34
+ `zip4`/`zip5`.
35
+ - **On failure** (all carriers fail) returns the **last (UPS) response** with
36
+ `success:false` (and an `error`).
37
+
38
+ ## The scripted endpoint is registered in the DB, not in source (parity gap)
39
+
40
+ The `GET /addresses/validateAddress` endpoint the frontend calls is wired as a **global**
41
+ scripted endpoint in **`Core.RecordScripts`** (id 17: `recordId` 13 = Addresses, method
42
+ `GET`, route `validateAddress`, `phpMethod` `validateAddress`). Verified in production
43
+ 2026-07-07.
44
+
45
+ - It is **not** in any client-level `CustomRecordScripts` (Rate's is empty), and it is
46
+ **not** present in `dbchanges2` source — the registration exists **only in the live DB**.
47
+ Treat this as a known source/DB parity gap: you will not find the endpoint by grepping
48
+ `dbchanges2`.
49
+ - **Dispatch precedence:** the V2 engine checks `Core.RecordScripts` **before** a client's
50
+ `CustomRecordScripts`, so a global script wins for every client unless a client overrides it.
51
+ - **Response path:** a scripted method's return value is placed at
52
+ `data.<record>.<route>` in the API envelope — so `validateAddress` (record = Addresses,
53
+ route = validateAddress) returns to **`data.addresses.validateAddress`**, which is exactly
54
+ what the frontend reads.
55
+
56
+ ## Client variations
57
+
58
+ None — this is shared engine + framework behavior. The carrier credentials are resolved
59
+ per the standard client carrier config.
60
+
61
+ ## Gotchas / known issues
62
+
63
+ - **DB-only registration.** Because the `Core.RecordScripts` row is not in `dbchanges2`,
64
+ the endpoint's existence is invisible to source search and is not reproduced by replaying
65
+ migrations into a fresh DB. Register scripted endpoints in `dbchanges2` going forward.
66
+ - **Waterfall requires agreement on success.** The success shape is only returned when the
67
+ carriers agree; a `success:false` result carries the last (UPS) response, not a merged one.
68
+
69
+ ## Change history
70
+
71
+ - 2026-07-07 — Documented `_Model_Client_Address::validateAddress` (USPS→FedEx→UPS waterfall
72
+ + normalized success shape) and the global `GET /addresses/validateAddress`
73
+ (`Core.RecordScripts` id 17) endpoint, including the dispatch precedence, the
74
+ `data.<record>.<route>` response path, and the dbchanges2 source/DB parity gap
75
+ (registration lives only in the live DB). Prod-verified while building the Rate WH purchase
76
+ guard (TRUE-79533). (mhammontree)
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
17
17
 
18
18
  ## 2.0 framework
19
19
 
20
- - **_underscore** (_Underscore) _(framework core)_ — 22 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
20
+ - **_underscore** (_Underscore) _(framework core)_ — 24 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
21
  - **worker2** (Worker) — 27 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
22
  - **api2** (API) — 10 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)
@@ -8,4 +8,5 @@
8
8
  | [Rate SAML SSO](features/saml-sso.md) | 2.0 | Rate uses Azure AD as its IdP (`login.rate.com`). | _underscore/Model/Rate/ClientAuthentication.php, saml/Controller/Index.php, toga2-view/src/hooks/useAuthenticationFlow.ts |
9
9
  | [Service Card Entitlement Display](features/service-card-entitlements.md) | 2.0 | Rate's home and services pages display one service card per purchased entitlement. | src/components/ServiceCard/ServiceCard.tsx, src/components/ServiceCard/index.ts, src/hooks/useBundleServices.ts, src/pages/Home/api/homeApi.ts, src/pages/Home/view/HomePage.tsx, src/pages/Home/viewModels/useHomePageViewModel.ts, src/pages/Services/view/ServicesPage.tsx, src/pages/Services/viewModels/useServicePageViewModel.ts |
10
10
  | [Rate Service-Purchase Confirmation Emails (Tech / Warranty)](features/service-purchase-emails.md) | 2.0 | When a Rate customer purchases a service, a confirmation email is sent. | _underscore/Model/Rate/Entitlement.php, worker2/Worker/Notification/EmailTemplate.php, dbchanges2/Client_Rate/2026-06-30a - Rate purchase email templates.sql |
11
+ | [Rate Whole Home Warranty Per-Address Purchase Guard](features/whole-home-warranty-purchase-guard.md) | 2.0 | A customer may hold **one active Whole Home Warranty (WH) per validated address, globally** (across all borrowers). | _underscore/Model/Rate/Entitlement.php, _underscore/Model/Client/Address.php, dbchanges2/Client_Rate/2026-07-07a - WholeHomeWarrantyPerAddressGuard.sql |
11
12
  | [Rate](profile.md) | 2.0 | Rate is a mortgage/lending client. | |
@@ -14,6 +14,7 @@ files:
14
14
  - worker2/Worker/Monitors/RateEntitlement.php
15
15
  related:
16
16
  - clients/rate/profile.md
17
+ - clients/rate/features/whole-home-warranty-purchase-guard.md
17
18
  - ../../../2.0/apps/worker2/features/monitoring-framework.md
18
19
  ---
19
20
 
@@ -35,6 +36,13 @@ This is a base-`_underscore` interceptor specific to Rate's product (it lives un
35
36
  `Model/Rate/`); documented here as a Rate client-feature because the behavior and its
36
37
  monitoring are Rate-scoped.
37
38
 
39
+ **As of 2026-07-07 (TRUE-79533)** the same `postPost` also persists the validated **service
40
+ address** (`Entitlements.c_serviceAddressId`, `Addresses.isValidated=1`) — that persistence is
41
+ wrapped so it never blocks this AIG-contract flow or the confirmation email. For Whole Home
42
+ Warranty purchases, a new `prePost` guard now also **carrier-normalizes the address onto the
43
+ payload before save**, so the AIG contract is created with the canonical address. See the
44
+ [Whole Home Warranty per-address purchase guard](whole-home-warranty-purchase-guard.md).
45
+
38
46
  ## How it works
39
47
 
40
48
  1. `postPost` runs after an entitlement is saved. It proceeds **only if** the payload has a
@@ -91,6 +99,10 @@ creation vs. cancellation. This is an accepted, documented limitation — not an
91
99
 
92
100
  ## Change history
93
101
 
102
+ - 2026-07-07 — `postPost` now also persists the validated service address
103
+ (`Entitlements.c_serviceAddressId`, `Addresses.isValidated=1`), wrapped so it never blocks the
104
+ AIG-contract/email flow; a new WH `prePost` guard carrier-normalizes the address onto the
105
+ payload before save (TRUE-79533). See the WH per-address purchase guard doc. (mhammontree)
94
106
  - 2026-06-29 — Documented the silent-failure AIG warranty-contract interceptor and the
95
107
  external log-scan monitor (`_Worker_Monitors_RateEntitlement`, TRUE-79129), including the
96
108
  accepted detection limitation that auth-induced failures are not attributable (shared
@@ -0,0 +1,140 @@
1
+ ---
2
+ title: "Rate Whole Home Warranty Per-Address Purchase Guard"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: rate
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-07-07
10
+ owners: [mhammontree]
11
+ files:
12
+ - _underscore/Model/Rate/Entitlement.php
13
+ - _underscore/Model/Client/Address.php
14
+ - dbchanges2/Client_Rate/2026-07-07a - WholeHomeWarrantyPerAddressGuard.sql
15
+ related:
16
+ - clients/rate/profile.md
17
+ - clients/rate/features/aig-contract-creation.md
18
+ - ../../../2.0/apps/_underscore/features/address-validation.md
19
+ ---
20
+
21
+ ## Summary
22
+
23
+ A customer may hold **one active Whole Home Warranty (WH) per validated address, globally**
24
+ (across all borrowers). This guard enforces that business rule at purchase time on the Rate
25
+ Entitlements route via a `prePost` interceptor on `_Model_Rate_Entitlement`
26
+ (`_underscore/Model/Rate/Entitlement.php`, Core.Records recordId **191**, registered PRE/POST).
27
+
28
+ For **WH purchases only** (WH is identified by the sale-item title containing `"warranty"`,
29
+ consistent with `resolvePurchaseProduct`), the guard:
30
+
31
+ 1. **Requires a service address** from `$payload->contact->primaryContactAddress->address`.
32
+ 2. **Hard-blocks** the purchase if
33
+ [`_Model_Client_Address::validateAddress`](../../../2.0/apps/_underscore/features/address-validation.md)
34
+ (USPS→FedEx→UPS waterfall) returns `success:false` — no entitlement is created; the error
35
+ surfaces to the frontend toaster.
36
+ 3. **Adopts the carrier-normalized address** (`address1`/`address2`/`city`/`state`/`zipCode`)
37
+ and writes it back onto the payload, so both the saved address and the AIG contract use the
38
+ canonical form.
39
+ 4. **Hard-blocks a second active WH at the same physical address globally** via
40
+ `hasActiveWarrantyAtAddress()`.
41
+
42
+ `postPost` then persists the validated service address after save (sets `Addresses.isValidated=1`
43
+ and `Entitlements.c_serviceAddressId`), wrapped so it never blocks the existing AIG-contract /
44
+ confirmation-email flow.
45
+
46
+ Ticket: TRUE-79533. Business rule owner: PM Paulina.
47
+
48
+ ## Business rules (decided this session — Mark / Paulina)
49
+
50
+ - **Invalid address → HARD BLOCK.** An address that fails carrier validation blocks the
51
+ purchase entirely (no entitlement), surfaced as an API error to the frontend toaster.
52
+ - **Uniqueness is GLOBAL per address**, not per-borrower — a second active WH on the same
53
+ physical address is rejected regardless of which borrower buys it.
54
+ - **Address line2 distinguishes units.** A landlord's apartment units are distinct addresses:
55
+ 2A / 2B / 2C = three separate WHs. Uniqueness keys on the full normalized address including
56
+ line2.
57
+ - **Persist the validated address ON the entitlement** (new `c_serviceAddressId` FK) rather
58
+ than deriving it from the fragile contact-primary-address path, and **backfill** existing WH
59
+ entitlements so the global rule covers production data already present.
60
+
61
+ ## How it works
62
+
63
+ 1. **`prePost` (PRE/POST interceptor).** Fires only for WH sale items (title LIKE
64
+ `%warranty%`).
65
+ 2. Reads the service address off `$payload->contact->primaryContactAddress->address`; missing
66
+ address → block.
67
+ 3. Calls `_Model_Client_Address::validateAddress`; `success:false` → block. On success, copies
68
+ the normalized `address1/address2/city/state/zipCode` back onto the payload.
69
+ 4. `hasActiveWarrantyAtAddress()` joins `Entitlements` → `Items` (title LIKE `%warranty%`, with
70
+ WH `Items.id = 3` as an OR fallback) → **active** `Subscriptions` (`isActive = 1 AND
71
+ dateCancelled IS NULL`) → `Addresses` on the new `c_serviceAddressId` column. A match → block
72
+ the second active WH.
73
+ - **The dedup read disables the query cache** (`_Database::useQueryCache(false)`, restored in
74
+ a `finally`) so it sees rows committed by a just-prior purchase rather than a stale cached
75
+ result set.
76
+ 5. **`postPost`.** After save, persists the validated service address: sets
77
+ `Addresses.isValidated = 1` and writes `Entitlements.c_serviceAddressId`. This block is
78
+ wrapped so a failure never interrupts the AIG-contract creation or the confirmation email
79
+ (see the [AIG contract creation doc](aig-contract-creation.md) — the same `postPost`).
80
+ 6. All SQL uses `_Database::escape()` / int-casts — `_Query` has **no bind API** in this path.
81
+
82
+ ## Migration (`dbchanges2/Client_Rate/2026-07-07a - WholeHomeWarrantyPerAddressGuard.sql`)
83
+
84
+ - Adds `Entitlements.c_serviceAddressId INT NULL` + an index.
85
+ - Guarded (`NOT EXISTS`) registration of `CustomRecordFields` for `c_serviceAddressId`
86
+ (recordId 191, type **NUMBER**).
87
+ - Guarded registration of the `ApiPayloadInterceptors` PRE/POST rows.
88
+ - A one-time, idempotent backfill of `c_serviceAddressId` for existing WH entitlements, sourced
89
+ from `Contacts` → `ContactAddresses` → `Addresses`.
90
+
91
+ ## Rate data model (prod-verified 2026-07-07)
92
+
93
+ - **WH product** = `Items.id 3` / `partNumber 1429124` "Whole Home Warranty - Monthly". Tech
94
+ products are `Items.id` 1 and 2.
95
+ - **Active-subscription signal is on `Subscriptions`** (`isActive` tinyint default 1,
96
+ `dateCancelled`, `dateEnd`, `dtRequestToCancel`). `Entitlements` has **no `isActive`**.
97
+ - **Entitlements had no address linkage before this ticket.** `SalesOrders` has
98
+ `shipToAddressId`/`billToAddressId` but they are NULL for Rate. Before `c_serviceAddressId`,
99
+ the address was reachable only via `Entitlement.contactId` → `Contacts.primaryContactAddressId`
100
+ → `ContactAddresses.addressId` → `Addresses`.
101
+ - **Borrower identity** = `Customers.c_borrowerId`.
102
+ - **Core.Records record IDs:** Addresses = 13, Entitlements = 191, Sales orders = 14,
103
+ Entitlement tickets = 212.
104
+
105
+ ## Frontend contract (delivered by Tanner — TRUE-79825 / 79969 / 79905, toga2-view)
106
+
107
+ The frontend UI that this guard honors was delivered separately: `components/ValidateAddressModal`,
108
+ `hooks/useValidateAddressFormViewModel`, `pages/ZipValidation`. The FE calls
109
+ `GET /addresses/validateAddress` and reads **`data.addresses.validateAddress`** typed as
110
+ `{ success, error?, address1?, address2?, city?, state?, zipCode?, country? }`. The modal shows
111
+ suggested-vs-entered when `success && address1`, and a "couldn't verify" toaster when
112
+ `success === false`. See the shared
113
+ [address-validation feature](../../../2.0/apps/_underscore/features/address-validation.md) for the
114
+ endpoint mechanics.
115
+
116
+ ## Gotchas / known issues
117
+
118
+ - **TOCTOU on the uniqueness check (accepted).** The check-then-persist dedup is **not**
119
+ lock-guarded. Two truly concurrent same-address WH purchases could both pass. Accepted for
120
+ sequential purchases; documented in code.
121
+ - **AIG carrier vs. AIG tenant.** "AIG" here is the warranty **carrier** behind Rate's Whole
122
+ Home Warranty — a different codepath from the separate `Client_Aig` / Staples Protection Plan
123
+ tenant. Do not conflate the two.
124
+ - **`ApiPayloadInterceptors` (Client_Rate) has no `phpMethod` column.** The interceptor method
125
+ is resolved **by convention** from `(prePostProcessing, httpMethod)`: `PRE+POST → prePost`,
126
+ `POST+POST → postPost` (matches the framework `[pre|post][HttpMethod]` convention).
127
+ - **`CustomRecordFields.type` enum supports only `STRING`/`NUMBER`** — `c_serviceAddressId` is
128
+ registered as `NUMBER`.
129
+ - **No `_Query` bind API here** — use `_Database::escape()` / int-casts for all interpolated
130
+ values.
131
+
132
+ ## Change history
133
+
134
+ - 2026-07-07 — Built the WH per-address purchase guard (TRUE-79533): `prePost` on
135
+ `_Model_Rate_Entitlement` hard-blocks invalid addresses and second active WH at the same
136
+ normalized address (global), adopts the carrier-normalized address onto the payload, and
137
+ disables the query cache for the committed-read dedup; `postPost` persists the validated
138
+ service address (`Entitlements.c_serviceAddressId`, `Addresses.isValidated=1`) without blocking
139
+ the AIG/email flow. Migration adds `c_serviceAddressId` + index, registers the CustomRecordField
140
+ and PRE/POST interceptors, and backfills existing WH entitlements. (mhammontree)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.283",
3
+ "version": "1.0.284",
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",