toga-ai 1.0.489 → 1.0.490

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,8 @@
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
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
- | [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 |
8
+ | [Address Uniqueness Normalization (unit identifier + 5-digit ZIP comparison)](features/address-uniqueness-normalization.md) | When a business rule says *"only one X per physical address"*, comparing address rows field-for-field does **not** work: the same dwelling is spelled many diffe | _underscore/Model/Rate/Entitlement.php |
9
+ | [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, _underscore/Component/Library/Carriers/Usps/Usps.php |
9
10
  | [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php |
10
11
  | [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 |
11
12
  | [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
@@ -31,5 +32,6 @@
31
32
  | [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
32
33
  | [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 |
33
34
  | [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 | |
35
+ | [USPS DPV Deliverability Verdict (is this address actually insurable/shippable?)](features/usps-dpv-deliverability.md) | **USPS returning HTTP 200 with a populated address is NOT evidence that the address is deliverable.** The authoritative signal is USPS's **DPV (Delivery Point V | _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Model/Client/Address.php, _underscore/Model/Rate/Entitlement.php |
34
36
  | [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 |
35
37
  | [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 |
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: "Address Uniqueness Normalization (unit identifier + 5-digit ZIP comparison)"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-31
10
+ owners: [mhammontree]
11
+ files:
12
+ - _underscore/Model/Rate/Entitlement.php
13
+ related:
14
+ - ./address-validation.md
15
+ - ./usps-dpv-deliverability.md
16
+ - ../../../clients/rate/features/whole-home-warranty-purchase-guard.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ When a business rule says *"only one X per physical address"*, comparing address rows
22
+ field-for-field does **not** work: the same dwelling is spelled many different ways
23
+ (`Suite 500` / `STE 500` / `Ste. 500` / `#500`), and carrier validation returns a **ZIP+4**
24
+ while every legacy row holds 5 digits. This doc records the two normalization primitives
25
+ built for the Rate Whole Home Warranty per-address guard, both of which are **reusable for any
26
+ "one per address" rule** on the platform.
27
+
28
+ Both live as private helpers on `_Model_Rate_Entitlement` today (first use); promote them to a
29
+ shared helper when a second consumer appears.
30
+
31
+ ## `normalizeUnitIdentifier()` — compare the UNIT, discard the designator
32
+
33
+ Line 2 is compared by **unit identifier only**. Everything that is not `A–Z0–9` is stripped,
34
+ the result upper-cased, and any leading designator word removed:
35
+
36
+ | Input `line2` | Normalized unit |
37
+ |---|---|
38
+ | `Suite 500`, `STE 500`, `Ste. 500`, `#500`, `Apt 500`, `No. 500`, `500` | `500` |
39
+ | `4B`, `12A`, `PH2`, `101-A` | `4B`, `12A`, `PH2`, `101A` |
40
+ | `""`, `NULL`, invisible-only (see below) | *(no unit)* |
41
+
42
+ **Why discard the designator instead of maintaining a synonym list.** Within a single
43
+ `line1 + city + ZIP5`, the designator carries **no distinguishing information** — there is no
44
+ real building with both a "Suite 500" and an "Apt 500". Discarding it removes any synonym
45
+ table to maintain and errs toward **MATCHING**, which is the correct bias for a *uniqueness
46
+ guard* (a false match blocks a purchase and a human resolves it; a false non-match silently
47
+ creates the duplicate the rule exists to prevent).
48
+
49
+ **An EMPTY unit means "the whole building"** and therefore conflicts with **ANY** unit at the
50
+ same street address, **in both directions**. So a building-wide warranty and a per-unit
51
+ warranty can never coexist.
52
+
53
+ **`trim()` alone is insufficient.** The `[^A-Z0-9]` strip is what collapses the invisible
54
+ characters that survive `trim()` — NBSP `U+00A0`, ZWSP `U+200B`, ZWJ `U+200D`, BOM `U+FEFF`,
55
+ ideographic space `U+3000`. An invisible-only `line2` normalizes to *no unit*, identical to
56
+ `NULL`, which is the intent.
57
+
58
+ ## `comparableZip()` — compare the first 5 digits only
59
+
60
+ USPS returns **ZIP+4** (verified live: `60503-7832`) while every pre-existing `Addresses` row
61
+ holds 5 digits. An exact ZIP comparison therefore missed **every legacy row**. Uniqueness
62
+ compares `substr(digits, 0, 5)`.
63
+
64
+ ## Where the comparison happens: SQL narrows, PHP decides
65
+
66
+ The dedup query narrows candidates on `line1 + city + 5-digit ZIP + state`, and the **unit
67
+ comparison runs in PHP** over the returned candidates.
68
+
69
+ **Why not do it in SQL.** Real normalization cannot be expressed in SQL without a nested
70
+ `REPLACE()` chain, which is both unreadable and **unindexable** (no index can serve it, so it
71
+ degrades to a full scan). Narrow in SQL on indexable columns, then compare in PHP.
72
+
73
+ The candidate fetch is capped at **`DEDUP_CANDIDATE_LIMIT = 500`** with an `error_log` canary
74
+ if the cap is ever hit — a single street address with 500+ address rows means something else
75
+ is wrong.
76
+
77
+ ## Gotchas / known issues
78
+
79
+ - **Never judge address duplication from a query that omits `line2`.** During this session
80
+ three beta entitlements looked like duplicates on a `line1 + zip` projection and were in
81
+ fact **distinct units** (`STE 1900` / `# 4` / `STE 500`). Always select `line2` before
82
+ concluding two addresses are the same — and certainly before deleting anything.
83
+ - **Bias is deliberately toward matching.** If a legitimate distinct unit is ever blocked, fix
84
+ it by making the *guard's* message actionable, not by adding designator sensitivity back.
85
+ - **Promote before the second copy.** These helpers are Rate-private only because Rate is the
86
+ first consumer. Copy-pasting them into a second client model is how normalization rules
87
+ drift apart.
88
+
89
+ ## Change history
90
+ - 2026-07-31 — Built `normalizeUnitIdentifier()` (designator-discarding unit comparison,
91
+ invisible-character collapse, empty-unit-conflicts-with-everything semantics) and
92
+ `comparableZip()` (5-digit comparison, because USPS returns ZIP+4 and legacy rows hold 5)
93
+ for the Rate WH per-address guard (TRUE-80487). Recorded the SQL-narrows/PHP-decides split
94
+ (real normalization is unindexable in SQL), `DEDUP_CANDIDATE_LIMIT = 500` + canary, and the
95
+ "never judge duplication without `line2`" lesson from three beta rows misread as duplicates.
96
+ (mhammontree)
@@ -6,13 +6,16 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-07
9
+ updated: 2026-07-31
10
10
  owners: [mhammontree]
11
11
  files:
12
12
  - _underscore/Model/Client/Address.php
13
+ - _underscore/Component/Library/Carriers/Usps/Usps.php
13
14
  related:
14
15
  - ../../api2/features/scripted-api-post-body-args.md
15
16
  - ../../api2/architecture.md
17
+ - ./usps-dpv-deliverability.md
18
+ - ./address-uniqueness-normalization.md
16
19
  - ../../../clients/rate/features/whole-home-warranty-purchase-guard.md
17
20
  ---
18
21
 
@@ -34,6 +37,16 @@ Whole Home Warranty purchase guard).
34
37
  `zip4`/`zip5`.
35
38
  - **On failure** (all carriers fail) returns the **last (UPS) response** with
36
39
  `success:false` (and an `error`).
40
+ - **Since 2026-07-31** the USPS leg also returns `DPVConfirmation`, `vacant`, `business` and
41
+ `corrections[]`, and `_Model_Client_Address` exposes `DPV_*` constants +
42
+ `withValidationDefaults()` — see
43
+ [USPS DPV deliverability](./usps-dpv-deliverability.md).
44
+
45
+ > **`success:true` does NOT mean the address is deliverable.** USPS returns HTTP 200 with a
46
+ > populated address even when the secondary unit is **missing or invented**. Any caller whose
47
+ > business rule depends on the address being real (insurance, warranty, shipment to a unit)
48
+ > must read the **DPV verdict**, not just `success`. See
49
+ > [USPS DPV deliverability](./usps-dpv-deliverability.md).
37
50
 
38
51
  ## The scripted endpoint is registered in the DB, not in source (parity gap)
39
52
 
@@ -68,8 +81,19 @@ in `dbchanges2` source** (closing the parity gap noted below) rather than only i
68
81
  distinct from the older global `validateAddress` route's `data.addresses.validateAddress`).
69
82
 
70
83
  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`).
84
+ simply **unrouted in source** until this migration set exposed it.
85
+
86
+ > **⚠ CORRECTION (2026-07-31): that grant migration never took effect.**
87
+ > `Client/2026-07-16c - AddressValidateScriptAcl.sql` **silently no-ops** — Core script **28**
88
+ > (`addresses/validate`) has **ZERO** `AclRecordScripts` rows in `Client_Rate`. The file resolves
89
+ > the Core script id with a cross-database `Core.*` read from a fan-out `Client/` folder, which
90
+ > cannot work: in production `Core` is a **separate cluster**, and on a developer machine the
91
+ > literal schema `Core` is the **1.0** database (the 2.0 core is `Core_2`) which has no
92
+ > `RecordScripts` table at all. Either way the `SET @var` stays NULL and the `NOT EXISTS`-guarded
93
+ > INSERT inserts nothing. **Do not copy this file as the pattern.** For a *client-specific*
94
+ > scripted API, register it as a **client-side `CustomRecordScripts`** instead — registration and
95
+ > grant then live in one database (see
96
+ > [Record Scripts → client-side CustomRecordScripts](../../api2/features/record-scripts.md)).
73
97
 
74
98
  ## Client variations
75
99
 
@@ -83,8 +107,37 @@ per the standard client carrier config.
83
107
  migrations into a fresh DB. Register scripted endpoints in `dbchanges2` going forward.
84
108
  - **Waterfall requires agreement on success.** The success shape is only returned when the
85
109
  carriers agree; a `success:false` result carries the last (UPS) response, not a merged one.
110
+ - **Scripted-API parity gaps (audited 2026-07-31).** Source and live DB disagree in both
111
+ directions — verify against the live `Core.RecordScripts` rows, never source alone:
112
+ - **`addresses/validateAddress` (id 17), the endpoint `toga2-view` actually calls, exists ONLY
113
+ as a live DB row** — there is **no `dbchanges2` migration for it**. A freshly provisioned
114
+ environment would **404** on the endpoint the frontend depends on.
115
+ - **`tracking-numbers/reprint` is MISSING ENTIRELY from production `Core.RecordScripts`** — so
116
+ `Core/2026-07-01a - TrackingNumberReprintRecordScript.sql` never took effect in production.
117
+ - **`addresses/validate` (id 28) has zero `AclRecordScripts` grants in `Client_Rate`** (see the
118
+ correction above).
119
+ - **`Core.RecordScripts` ids are NOT stable across environments** — max id is 27 in production,
120
+ 28 local, 28 client-sandbox, 30 dev-sandbox, and the *same* script has different ids per
121
+ environment. Never hardcode a `RecordScripts` id, and never assume "the next available id"
122
+ means the same thing anywhere else. (`Core.Records` ids, by contrast, **are** seeded
123
+ identically everywhere — Addresses = 13.)
124
+ - **USPS OAuth credentials are committed in plaintext** in
125
+ `_underscore/Component/Library/Carriers/Usps/Usps.php` (constants near the top of the file);
126
+ they belong in the `_Config` INI. Rotate and relocate — see the
127
+ [DPV doc](./usps-dpv-deliverability.md). *(Location only; no value recorded.)*
86
128
 
87
129
  ## Change history
130
+ - 2026-07-31 — TRUE-80487: recorded that **`success:true` does not mean deliverable** (USPS
131
+ returns 200 for a missing/invented unit) and split the DPV verdict out into
132
+ [usps-dpv-deliverability.md](./usps-dpv-deliverability.md); the USPS leg now also returns
133
+ `DPVConfirmation`/`vacant`/`business`/`corrections`. **Corrected the 2026-07-17 entry below:**
134
+ `Client/2026-07-16c - AddressValidateScriptAcl.sql` **silently no-ops** (script 28 has zero
135
+ `AclRecordScripts` rows in `Client_Rate`) because it reads `Core.*` from a fan-out `Client/`
136
+ folder — unreachable in prod (separate cluster) *and* locally (`Core` is the **1.0** DB; the 2.0
137
+ core is `Core_2`). It is **not** the pattern to copy. Added the audited scripted-API parity gaps
138
+ (script 17 exists only in the live DB with no migration; `tracking-numbers/reprint` absent from
139
+ production) and the `Core.RecordScripts` id-drift fact. Also recorded the committed plaintext
140
+ USPS credential location. (mhammontree)
88
141
 
89
142
  - 2026-07-17 — TRUE-79191: added a **`dbchanges2`-registered** `GET /addresses/validate` route to the
90
143
  same `validateAddress` method (Core `RecordScripts` route `validate` + Client `AclRecordScripts`
@@ -0,0 +1,103 @@
1
+ ---
2
+ title: "USPS DPV Deliverability Verdict (is this address actually insurable/shippable?)"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-31
10
+ owners: [mhammontree]
11
+ files:
12
+ - _underscore/Component/Library/Carriers/Usps/Usps.php
13
+ - _underscore/Model/Client/Address.php
14
+ - _underscore/Model/Rate/Entitlement.php
15
+ related:
16
+ - ./address-validation.md
17
+ - ./address-uniqueness-normalization.md
18
+ - ../../../clients/rate/features/whole-home-warranty-purchase-guard.md
19
+ ---
20
+
21
+ ## Summary
22
+
23
+ **USPS returning HTTP 200 with a populated address is NOT evidence that the address is
24
+ deliverable.** The authoritative signal is USPS's **DPV (Delivery Point Validation)**
25
+ confirmation code, which the carrier waterfall previously **discarded**. This doc records what
26
+ DPV means, the live-verified verdicts, and the platform mapping now applied.
27
+
28
+ USPS will happily echo back an **invented** secondary unit and still return 200 — so adopting
29
+ the "recommended" address alone does **not** fix a bad unit. **The DPV field must be read.**
30
+
31
+ ## The verdicts (VERIFIED against live USPS responses 2026-07-31)
32
+
33
+ | Address probed | `DPVConfirmation` | Meaning |
34
+ |---|---|---|
35
+ | `1904 IONE LN, AURORA IL 60503` (no unit) | **`Y`** | deliverable exactly as given |
36
+ | `1904 IONE LN` + `UNIT 200` (invented; single-family) | **`S`** | secondary present but **not confirmed** |
37
+ | `2511 VERSAILLES AVE, NAPERVILLE IL 60540` (no unit) | **`D`** | secondary **MISSING** — building is subdivided |
38
+ | `2511 VERSAILLES AVE` + `# 1` (invalid) | **`S`** | secondary not confirmed |
39
+
40
+ The `D` case additionally returns `corrections[].code` **`32`** — *"Default address: … more
41
+ information is needed (such as an apartment, suite, or box number)."*
42
+
43
+ **Platform mapping:**
44
+
45
+ | DPV | Action |
46
+ |---|---|
47
+ | `Y` | proceed |
48
+ | `D` | reject — **unit required** |
49
+ | `S` | reject — **unit not verified** |
50
+ | `N` | reject |
51
+ | `''` / absent | **unknown → PROCEED** (see decision below) |
52
+
53
+ ## DECIDED: unknown DPV proceeds (fails OPEN, deliberately)
54
+
55
+ FedEx and UPS — the waterfall's fallbacks — **do not report DPV at all**. If a fallback carrier
56
+ answered, the DPV verdict is unknown. That case **proceeds**: failing closed would block
57
+ **every** purchase during a USPS outage, and for the uniqueness use case the
58
+ [dedup guard](./address-uniqueness-normalization.md) still prevents duplicates independently.
59
+ This is a conscious availability-over-strictness trade; do not "harden" it without a
60
+ business-owner decision.
61
+
62
+ ## What was captured
63
+
64
+ - **`Usps.php`** now captures `DPVConfirmation`, `vacant`, `business`, and `corrections[]` off
65
+ the USPS response. `vacant` and `business` are **free** in `additionalInfo` and are now
66
+ carried through as potential underwriting inputs (no rule consumes them yet).
67
+ - **`_Model_Client_Address`** gained `DPV_*` constants plus **`withValidationDefaults()`**,
68
+ which is **purely additive**: it is implemented as `$response + [defaults]`, so the **left
69
+ operand wins** and a real carrier verdict can never be clobbered by a default.
70
+
71
+ ## Carriers cannot tell you DWELLING TYPE
72
+
73
+ A recurring ask ("is this an apartment building?") is **not answerable from carrier data**:
74
+
75
+ - UPS `AddressClassification` and FedEx classification only distinguish **Residential vs
76
+ Commercial** — an apartment building is *Residential*.
77
+ - The only actionable signal available is USPS DPV **"secondary unit required"** (`D`).
78
+ - **True dwelling type requires property data** — an assessor/parcel feed or a
79
+ Melissa/Smarty-class vendor. That is a **procurement decision, not a code one.**
80
+
81
+ ## Gotchas / known issues
82
+
83
+ - **HTTP 200 ≠ deliverable.** This is the whole point of the doc. Success + a populated
84
+ address is the *normal* response for a missing or invented unit.
85
+ - **USPS does not strip an invented unit.** It echoes `UNIT 200` back verbatim. Adopting the
86
+ recommended address does not sanitize it.
87
+ - **Plaintext USPS OAuth credentials are committed in source** — the client id and secret are
88
+ hardcoded as constants near the top of
89
+ `_underscore/Component/Library/Carriers/Usps/Usps.php`. They belong in the `_Config` INI
90
+ alongside the DB credentials. **Treat them as compromised: rotate and move to SSM / EB
91
+ environment properties.** Pre-existing, not introduced by this work. *(Location +
92
+ remediation only — no value is recorded anywhere.)*
93
+
94
+ ## Change history
95
+ - 2026-07-31 — Discovered that the carrier waterfall was **discarding** USPS DPV, and built
96
+ capture + enforcement (TRUE-80487): `Usps.php` now returns `DPVConfirmation`/`vacant`/
97
+ `business`/`corrections`, `_Model_Client_Address` gained `DPV_*` constants and the additive
98
+ `withValidationDefaults()`, and Rate's WH guard enforces `Y` proceed / `D` unit-required /
99
+ `S` unit-unverified / `N` reject / unknown proceed. Verdicts verified against four live USPS
100
+ probes; recorded that USPS echoes an invented unit back with a 200, that unknown DPV fails
101
+ **open** by decision, and that carriers cannot supply dwelling type (only Residential vs
102
+ Commercial) so an apartment-vs-house rule needs property data. Also recorded the committed
103
+ plaintext USPS OAuth credential location for rotation. (mhammontree)
@@ -20,5 +20,5 @@
20
20
  | [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, api2/Component/Api/V2/Response/Response.php, api2/Component/Api/V2/Response/Oauth/Oauth.php, api2/Controller/Index.php, toga2-supply/src/globalTypes.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, _underscore/Model/Client/TrackingNumber.php |
21
21
  | [V2 REST query contract (params, where grammar, encoding, ACL behavior)](features/v2-rest-query-contract.md) | What an **HTTP client** has to get right to query the Toga v2 REST API: which query params are recognized, the exact `where` grammar, how the query string is (n | api2/Component/Api/V2/V2.php |
22
22
  | [V2 reverse hasMany collections must be named in the fetch fields whitelist](features/v2-reverse-hasmany-fields-whitelist.md) | In the V2 JSON engine, a **reverse hasMany** relationship — the collection of child records that foreign-key back to a parent (e.g. | api2/Component/Api/V2/V2.php |
23
- | [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, api2/ebs/register_instance_to_shared_application_load_balancer.php |
23
+ | [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, api2/ebs/register_instance_to_shared_application_load_balancer.php, api2/.platform/hooks/prebuild/git.sh, api2/.ebextensions/git.php, api2/.ebextensions/git.sandbox-dev.json |
24
24
  | [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, dbchanges2/Logs/, _underscore/Route.php |
@@ -136,6 +136,56 @@ metadata model.
136
136
  endpoint goes in `Client_True`. The generic **`Client/`** folder runs against **every**
137
137
  tenant DB, so putting an ACL grant there hands the endpoint to all clients.
138
138
 
139
+ > **⚠ The "resolve the Core id by route from the client file" convention above is NOT runnable
140
+ > for the `AclRecordScripts` grant.** `AclRecordScripts.recordScriptId` links to
141
+ > `Core.RecordScripts` by **NUMERIC id** (there is no uuid reference column), so a `Client_*`
142
+ > migration must read `Core` to resolve it — which is impossible in production (separate
143
+ > clusters) **and equally broken in local dev**: locally the 2.0 core schema is **`Core_2`**, and
144
+ > the schema literally named `Core` is the **1.0** database, which has no `RecordScripts` and no
145
+ > `RecordFields`. So a literal `Core.<Table>` reference resolves to the wrong (or missing) schema
146
+ > in *both* environments, and a `NOT EXISTS`-guarded INSERT inserts nothing without erroring.
147
+ > **A clean local run is therefore never proof that such a migration worked.** This is not
148
+ > theoretical: `Client/2026-07-16c - AddressValidateScriptAcl.sql` **silently no-ops**, leaving
149
+ > Core script 28 with zero grants in `Client_Rate`. For a **client-specific** script, use the CustomRecordScripts pattern below.
150
+
151
+ ## ✅ REUSABLE PATTERN — register a CLIENT-SPECIFIC scripted API as a `CustomRecordScript`
152
+
153
+ **When a scripted API is declared on a client model (`_Model_<Slug>_X`), register it in the
154
+ CLIENT database as a `CustomRecordScript` — NOT in `Core.RecordScripts`.** First applied in
155
+ `dbchanges2/Client_Rate/2026-07-31a - WarrantyAvailabilityCustomRecordScript.sql`
156
+ (`_Model_Rate_Entitlement::warrantyAvailability`).
157
+
158
+ **Why (three independent reasons):**
159
+
160
+ 1. **One database, so the grant is resolvable.** `CustomRecordScripts` **and**
161
+ `AclCustomRecordScripts` are **both** `DB_CLIENT`
162
+ (`_Model_Client_CustomRecordScript` / `_Model_Client_AclCustomRecordScript`). Registration and
163
+ grant therefore live in the **same** database, and the grant can resolve the script id from a
164
+ **hardcoded v4 UUID literal** via a same-database lookup — **no id needs pinning and no
165
+ cross-team coordination is required.** A `Core.RecordScripts` registration puts the id in
166
+ `Core` while the grant lives in the client DB and links by numeric id, forcing exactly the
167
+ impossible cross-tenant read described above.
168
+ 2. **api2 reads both from `DB_CLIENT`.** In `api2/Component/Api/V2/V2.php`, the
169
+ `lookupCustomRecordScriptsByRecordIdAndMethodAndRoute` map is built from
170
+ `SELECT … FROM CustomRecordScripts` (~line 8613) and authorization goes through
171
+ `AclCustomRecordScripts` (~line 5919). **`Core.RecordScripts` are checked FIRST, with a
172
+ fallback to custom** (~line 5906) — so a custom script cannot shadow a global one, and a
173
+ global one always wins.
174
+ 3. **It is semantically correct.** The method is declared on the *client* model, not the base
175
+ model. Registering it in `Core` would advertise the route to **every** tenant, including
176
+ tenants whose model has no such method.
177
+
178
+ **Authoring notes from the first use:**
179
+ - `recordId` is written as the **literal** (191 for Entitlements) per team convention —
180
+ `Core.Records` ids **are** deliberately seeded identically in every environment
181
+ (Addresses 13, Sales orders 14, Entitlements 191, Subscriptions 253). This is safe;
182
+ `Core.RecordScripts` ids are **not** (they have drifted per environment).
183
+ - **Grant `Base` only, resolved by NAME** — role ids differ per client.
184
+ - **Do NOT grant `Public`.** A warranty-availability endpoint reveals whether a warranty exists
185
+ at an arbitrary address; unauthenticated probing must be prevented.
186
+ - Verified by applying the file **twice** to two environments: exactly one script row and one
187
+ grant row each.
188
+
139
189
  ## Data source note (core DB hosts/credentials)
140
190
 
141
191
  The core cluster hosts and credentials this endpoint reads through are **not** documented here;
@@ -149,6 +199,12 @@ an `AclRecordScripts` grant in each client DB.
149
199
 
150
200
  ## Gotchas / known issues
151
201
 
202
+ - **The response key is the ROUTE VERBATIM, not the `phpMethod`.** A script registered with
203
+ route `warranty-availability` and `phpMethod` `warrantyAvailability` returns
204
+ **`data.entitlements["warranty-availability"]`** — *not* `data.entitlements.warrantyAvailability`.
205
+ The sibling `addresses/validateAddress` reader looks camelCase only because **that script's
206
+ route is itself `validateAddress`.** Reading the wrong key yields `undefined` (and, in a
207
+ fail-closed consumer, correctly denies) — this cost a debug cycle in beta.
152
208
  - **No bound params in `_Query`.** Sanitize with `\_Database::escape()` (strings) and `(int)`
153
209
  casts (numerics) for every value placed into the query text — query-string args are user
154
210
  input.
@@ -169,6 +225,20 @@ an `AclRecordScripts` grant in each client DB.
169
225
 
170
226
  ## Change history
171
227
 
228
+ - 2026-07-31 — TRUE-80487: added the **reusable client-side `CustomRecordScript` pattern** for a
229
+ scripted API declared on a client model — `CustomRecordScripts` + `AclCustomRecordScripts` are
230
+ **both** `DB_CLIENT`, so registration and grant land in **one** database and the grant resolves
231
+ the script id from a hardcoded uuid (no id pinning, no cross-team coordination); api2 reads both
232
+ from `DB_CLIENT` (`V2.php` ~8613 / ~5919) and checks `Core.RecordScripts` **first** with a
233
+ fallback to custom (~5906). Recorded **why the existing `Core.RecordScripts` + `Client_*`
234
+ `AclRecordScripts` convention is unrunnable** for a client-scoped script
235
+ (`AclRecordScripts.recordScriptId` links by numeric id with no uuid reference → forced
236
+ cross-cluster read; `Client/2026-07-16c - AddressValidateScriptAcl.sql` silently no-ops, script
237
+ 28 has zero grants in `Client_Rate`). Also recorded that the response key is the **route
238
+ verbatim** (`data.entitlements["warranty-availability"]`, not the camelCase `phpMethod`), that
239
+ `Core.Records` ids are environment-stable literals while `Core.RecordScripts` ids have drifted,
240
+ and the grant-Base-by-name / never-Public guidance. First use:
241
+ `dbchanges2/Client_Rate/2026-07-31a - WarrantyAvailabilityCustomRecordScript.sql`. (mhammontree)
172
242
  - 2026-07-24 — Refined the engine contract from productionizing the sprint dashboard (verified
173
243
  in `V2.php` + `_underscore/Query.php`): the return value **nests under the route-segment key**
174
244
  (`data[<route>]`, not flat — consumers must unwrap); there is **no current-sprint/context
@@ -6,12 +6,16 @@ project: API
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-07-28
9
+ updated: 2026-07-31
10
10
  owners: ["jcardinal", "mhammontree"]
11
11
  files:
12
12
  - api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh
13
13
  - api2/ebs/register_instance_to_shared_application_load_balancer.php
14
- related: []
14
+ - api2/.platform/hooks/prebuild/git.sh
15
+ - api2/.ebextensions/git.php
16
+ - api2/.ebextensions/git.sandbox-dev.json
17
+ related:
18
+ - ../../../standards/frontend-deploy.md
15
19
  ---
16
20
 
17
21
  ## Summary
@@ -40,6 +44,27 @@ app-repo `_<env>` branches.
40
44
  > this env; you must push it to **`_sandbox-dev`**. Always confirm the exact env↔branch mapping in the
41
45
  > env's `git.<env>.json` before assuming `_<env>` applies.
42
46
 
47
+ ### "beta" is a DIFFERENT git branch per repo — the full map (verified 2026-07-31)
48
+
49
+ The word *beta* names an **environment**, not a branch, and each tier resolves it differently.
50
+ Getting this wrong cost **two wasted deploys** in one session (TRUE-80487). Confirm the tier you
51
+ are actually changing before you merge anything:
52
+
53
+ | Tier | What "beta" is | Branch actually built | Notes |
54
+ |---|---|---|---|
55
+ | `api2` / `_underscore` | EB env **`API-Sandbox-Dev`** behind `api.beta.togahub.com` | `_underscore` **`_sandbox-dev`** (per `.ebextensions/git.sandbox-dev.json`) | **Not `_beta`.** Database environment is **`dev-sandbox`**. |
56
+ | `toga2-view` | AWS Amplify target **named "production"** | git branch **`_beta`**, bundling `.env.beta` (`VITE_API=https://api.beta.togahub.com/v2`) | The target *name* is misleading; proven by a string present only in `_beta` appearing in the deployed UI and absent from `_production`. |
57
+
58
+ - **`Client_Rate` exists in `prod`, `dev-sandbox` and `client-sandbox` — NOT in `client-beta`.** So
59
+ "the beta database" is ambiguous too; name the concrete DB environment.
60
+ - **`_underscore` is NOT vendored into `api2`.** `.platform/hooks/prebuild/git.sh` +
61
+ `.ebextensions/git.php` clone it at build time (branch = the per-env json's `branch`, else
62
+ `_<environment>`). Consequence: **a framework-only change requires REDEPLOYING THE CONSUMING
63
+ TIER even when `api2` itself has zero changes** — an instance **restart will not pick it up**,
64
+ because the clone happens in prebuild.
65
+ - **`api2` has no deploy workflow.** Its only GitHub Action is the `TRUE-#####:` commit-message
66
+ linter, so **EB deploys are manual**. Do not wait for CI to ship a framework change.
67
+
43
68
  ### Debugging a beta env without Sentry — `Logs_<Client>.Api`
44
69
  `Logs_<Client>.Api` is a **full server-side request log**: `queryString`, request/response payloads,
45
70
  `direction` (`IN`/`OUT`), `instanceId`, and `executionTime`. It is the reliable way to see what a
@@ -139,6 +164,14 @@ aws codeconnections get-connection --connection-arn "<CONN_ARN>" --region "$REGI
139
164
  ```
140
165
 
141
166
  ## Change history
167
+ - 2026-07-31 — TRUE-80487: added the **full cross-tier "beta" environment map** after it cost two
168
+ wasted deploys. `api.beta.togahub.com` is EB env **`API-Sandbox-Dev`** on the **`dev-sandbox`**
169
+ database, cloning `_underscore` **`_sandbox-dev`**; `toga2-view`'s Amplify target **named
170
+ "production"** actually builds branch **`_beta`** with `.env.beta` pointing at that API. Recorded
171
+ that `Client_Rate` exists in prod/dev-sandbox/client-sandbox but **not** client-beta, that
172
+ `_underscore` is cloned in **prebuild** (so a framework-only change needs the consuming tier
173
+ **redeployed**, not restarted), and that `api2` has **no deploy workflow** — EB deploys are
174
+ manual. (mhammontree)
142
175
  - 2026-07-28 — Manual LB re-registration is now automated on **non-production** tiers carrying the `.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh` hook pair (api2 already had it; ported to worker2 this session). Linked the new worker2 feature doc as the reference implementation and flagged that api2's copy has known security defects. (jcardinal)
143
176
  - 2026-07-27 — Noted that not every EB env pulls `_underscore` from its `_<env>` branch: **`API-Sandbox-Dev`** re-clones `_underscore` from the branch in `.ebextensions/git.sandbox-dev.json` (**`_sandbox-dev`**), overwriting the pipeline copy — so a `_beta` merge doesn't reach it. Added the `Logs_<Client>.Api` server-side request-log note as the reliable way to debug a beta env without Sentry (TRUE-79533). (mhammontree)
144
177
  - 2026-07-14 — Documented the branch model: app repos (`_underscore`/`api2`/`toga2-supply`) deploy from long-lived `_beta`/`_production` branches (not `_main`); api2 pulls `_underscore`'s `_<env>` branch at EB build; `dbchanges2` has only `_main` and its migrations are applied per-env by the team process (not by a code deploy). (mhammontree)
@@ -467,6 +467,27 @@ rule resumes.
467
467
  earlier `2026-06-25c` / `2026-06-29c` / `2026-06-25d` surface seeds **likely share the same
468
468
  anti-pattern** and need auditing. Core→Core seeds are fine (same cluster). Also still open:
469
469
  re-seeding NYCHH/Prudential/SPGlobal (deferred).
470
+ - **✅ AUDIT DONE (2026-07-31, TRUE-80487) — and the failure is WORSE, but the fix is CHEAPER
471
+ than assumed.** A full sweep of `dbchanges2` found **24 of 505 non-`HISTORIC` `.sql` files**
472
+ referencing a database outside their folder's tenant (comment-only mentions excluded): **13 in
473
+ `Client/`** — including `2026-06-03 BLANK_CLIENT_DATABASE`, the **new-client template** — **3
474
+ in `Client_Rate/`**, **2 in `Core/` reaching into `Client.*`**, **2 in `Team/`**, plus
475
+ `Client_Adyen`, `Client_Compass`, `Client_Pcmaticb2b`, `Client_Quad`.
476
+ - **PROVEN broken, not theoretical:** `Client/2026-07-16c - AddressValidateScriptAcl.sql`
477
+ silently no-ops — Core script 28 (`addresses/validate`) has **zero** `AclRecordScripts` rows
478
+ in `Client_Rate`.
479
+ - **The root cause is not only the production tenant split.** A literal `Core.<Table>` does
480
+ **not** resolve to the 2.0 core on a developer machine either: **locally the 2.0 core is
481
+ `Core_2`, while `Core` is the *1.0* database**, which has no `RecordScripts` and no
482
+ `RecordFields` tables. So the statement fails **locally too** — `SET @var` stays NULL and the
483
+ `NOT EXISTS`-guarded INSERT silently inserts nothing. **Both environments break, by different
484
+ mechanisms.** This makes "it worked locally" impossible as a defense.
485
+ - **The fix is a literals swap, not a schema change.** Follow the prod-safe pattern already
486
+ documented at `Client/2026-06-23b - ItemTranslationsAcl.sql` (jcardinal): hardcode Core ids as
487
+ **literals**, resolve only client-local ids by name. And for a **client-specific scripted
488
+ API**, moving the registration to client-side `CustomRecordScripts` removes the cross-tenant
489
+ need **entirely** — see
490
+ [Record Scripts → client-side CustomRecordScripts](../../api2/features/record-scripts.md).
470
491
  - **A `VISIBILITY_RULE`/`ENABLED_RULE` override row can exist in the DB with the CORRECT `c_longValue`
471
492
  yet be silently ignored because its `attribute` coerced to `''`.** The `SurfaceOverrides.attribute`
472
493
  ENUM must be widened per client (`Client/2026-07-15 - SurfaceOverridesAttributeRuleValues.sql`)
@@ -500,6 +521,17 @@ rule resumes.
500
521
  override is added). **Open follow-up.**
501
522
 
502
523
  ## Change history
524
+ - 2026-07-31 — **Closed the "audit the cross-DB surface seeds" open item** (TRUE-80487): swept all
525
+ 505 non-`HISTORIC` `.sql` files and found **24** cross-tenant references (13 in `Client/`,
526
+ including the `BLANK_CLIENT_DATABASE` new-client template; 3 in `Client_Rate/`; 2 in `Core/`
527
+ reaching into `Client.*`; 2 in `Team/`; plus Adyen/Compass/Pcmaticb2b/Quad). Confirmed one
528
+ **proven-broken** instance (`Client/2026-07-16c - AddressValidateScriptAcl.sql` → Core script 28
529
+ has zero `AclRecordScripts` in `Client_Rate`) and recorded the **second, previously unknown
530
+ failure mechanism**: locally `Core` is the **1.0** database (the 2.0 core is `Core_2`), so a
531
+ literal `Core.<Table>` fails in local dev too — both environments break, by different causes.
532
+ Noted the remediation is cheaper than this doc assumed (hardcode Core ids as literals per
533
+ `Client/2026-06-23b`, or move a client-scoped scripted API to client-side `CustomRecordScripts`),
534
+ not a schema change. (mhammontree)
503
535
  - 2026-07-23 — Repointed the Order Details **"PO Number"** detail field (all clients, Core seed).
504
536
  `Core/2026-07-23a - PoNumberDetailFieldValueKey.sql` updates the `SurfaceElements` row (uuid
505
537
  `d45c51d8-8211-11f1-bfa7-a30f63c3a801`, label `salesOrder.field.poNumber`) `config` from
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
18
18
 
19
19
  ## 2.0 framework
20
20
 
21
- - **_underscore** (_Underscore) _(framework core)_ — 40 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
+ - **_underscore** (_Underscore) _(framework core)_ — 42 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 38 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 20 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
24
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -8,5 +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/hooks/useActiveServices.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, src/api/serviceAddressApi.ts, src/api/apiErrors.ts, dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql |
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 | > **STATUS (2026-07-27): beta-verified end-to-end; promoting to `_production`.** The final blocker > to a correct service-address pin turned out to be a **front | _underscore/Model/Rate/Entitlement.php, _underscore/Model/Client/Entitlement.php, _underscore/Model/Client/Address.php, toga2-view/src/pages/CheckOut/api/checkoutApi.ts, dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql, dbchanges2/Core/2026-07-22a - EntitlementServiceAddressIdField.sql, dbchanges2/Client_Rate/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql, test/@Mark/Rate/verify_wholehome_per_address_guard.php |
11
+ | [Rate Whole Home Warranty Per-Address Purchase Guard](features/whole-home-warranty-purchase-guard.md) | 2.0 | > # ⚠ STATUS (2026-07-31, TRUE-80487) THE GUARD HAD NEVER EXECUTED, IN ANY ENVIRONMENT > > Everything below the next section described a guard that **never ra | _underscore/Model/Rate/Entitlement.php, _underscore/Model/Client/Entitlement.php, _underscore/Model/Client/Address.php, _underscore/Component/Library/Carriers/Usps/Usps.php, toga2-view/src/pages/CheckOut/api/checkoutApi.ts, toga2-view/src/pages/CheckOut/viewModel/useCheckoutPageViewModel.ts, toga2-view/src/pages/ZipValidation/api/zipValidation.ts, dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql, dbchanges2/Core/2026-07-22a - EntitlementServiceAddressIdField.sql, dbchanges2/Client_Rate/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql, dbchanges2/Client_Rate/2026-07-31a - WarrantyAvailabilityCustomRecordScript.sql, test/@Mark/Rate/verify_wholehome_per_address_guard.php, test/@Mark/Rate/verify_wh_gate_and_unit_dedup.php |
12
12
  | [Rate](profile.md) | 2.0 | Rate is a mortgage/lending client. | |
@@ -6,24 +6,45 @@ project: _Underscore
6
6
  client: rate
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-07-27
9
+ updated: 2026-07-31
10
10
  owners: [mhammontree, tcox]
11
11
  files:
12
12
  - _underscore/Model/Rate/Entitlement.php
13
13
  - _underscore/Model/Client/Entitlement.php
14
14
  - _underscore/Model/Client/Address.php
15
+ - _underscore/Component/Library/Carriers/Usps/Usps.php
15
16
  - toga2-view/src/pages/CheckOut/api/checkoutApi.ts
17
+ - toga2-view/src/pages/CheckOut/viewModel/useCheckoutPageViewModel.ts
18
+ - toga2-view/src/pages/ZipValidation/api/zipValidation.ts
16
19
  - dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql
17
20
  - dbchanges2/Core/2026-07-22a - EntitlementServiceAddressIdField.sql
18
21
  - dbchanges2/Client_Rate/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql
22
+ - dbchanges2/Client_Rate/2026-07-31a - WarrantyAvailabilityCustomRecordScript.sql
19
23
  - test/@Mark/Rate/verify_wholehome_per_address_guard.php
24
+ - test/@Mark/Rate/verify_wh_gate_and_unit_dedup.php
20
25
  related:
21
26
  - clients/rate/profile.md
22
27
  - clients/rate/features/aig-contract-creation.md
23
28
  - ../../../2.0/apps/_underscore/features/address-validation.md
29
+ - ../../../2.0/apps/_underscore/features/usps-dpv-deliverability.md
30
+ - ../../../2.0/apps/_underscore/features/address-uniqueness-normalization.md
31
+ - ../../../2.0/apps/api2/features/record-scripts.md
24
32
  ---
25
33
 
26
- > **STATUS (2026-07-27): beta-verified end-to-end; promoting to `_production`.** The final blocker
34
+ > # ⚠ STATUS (2026-07-31, TRUE-80487) THE GUARD HAD NEVER EXECUTED, IN ANY ENVIRONMENT
35
+ >
36
+ > Everything below the next section described a guard that **never ran**. `isWholeHomeWarranty()`
37
+ > read `$payload->saleItem->title`, but the real `toga2-view` checkout payload sends
38
+ > `{"saleItem":{"uuid":"…"}}` with **no title** — so `stripos('')` was always false and `prePost`
39
+ > returned **before** the guard on **every** purchase. **The guard's rejection message appears in
40
+ > NO log, in ANY environment, ever.** Live proof: dev-sandbox held **3 active WH entitlements at
41
+ > one address** (ET100057 / ET100067 / ET100073), two of them created **2026-07-27** — the day the
42
+ > feature was marked "beta-verified". Three latent defects, the DPV enforcement, unit-level
43
+ > address normalization, and a server-side availability endpoint all landed on TRUE-80487; read
44
+ > **[What TRUE-80487 changed](#what-true-80487-changed-2026-07-31)** first, then the historical
45
+ > detail below.
46
+ >
47
+ > **STATUS (2026-07-27, historical): beta-verified end-to-end; promoting to `_production`.** The final blocker
27
48
  > to a correct service-address pin turned out to be a **front-end request-`depth` bug**: Rate's
28
49
  > checkout was sending `{depth:-1}` on `POST /entitlements`, so V2 built a **shallow** payload
29
50
  > (`meta.calcDepth 1`) and `postPost` never received the nested
@@ -45,8 +66,9 @@ A customer may hold **one active Whole Home Warranty (WH) per validated address,
45
66
  Entitlements route via a `prePost` interceptor on `_Model_Rate_Entitlement`
46
67
  (`_underscore/Model/Rate/Entitlement.php`, Core.Records recordId **191**, registered PRE/POST).
47
68
 
48
- For **WH purchases only** (WH is identified by the sale-item title containing `"warranty"`,
49
- consistent with `resolvePurchaseProduct`), the guard:
69
+ For **WH purchases only** WH is identified by **resolving `saleItem.uuid` `Items`** and
70
+ matching on the **same authority the DB dedup uses** (title keyword **OR** `WH_SALE_ITEM_ID`);
71
+ see [the identification defect](#what-true-80487-changed-2026-07-31) — the guard:
50
72
 
51
73
  1. **Requires a service address** from `$payload->contact->primaryContactAddress->address`.
52
74
  2. **Hard-blocks** the purchase if
@@ -65,6 +87,102 @@ confirmation-email flow.
65
87
 
66
88
  Ticket: TRUE-79533. Business rule owner: PM Paulina.
67
89
 
90
+ ## What TRUE-80487 changed (2026-07-31)
91
+
92
+ ### 1. The guard never fired — payload-shape mismatch on the WH test (FIXED)
93
+
94
+ `isWholeHomeWarranty()` read **`$payload->saleItem->title`**. The real checkout payload is
95
+ **`{"saleItem":{"uuid":"…"}}`** — **no `title`** — so the test was always false and `prePost`
96
+ returned before the guard **in every environment since the feature shipped**.
97
+
98
+ Now the guard **resolves `saleItem.uuid` → `Items`** and matches on the **same authority the DB
99
+ dedup uses**: title keyword **OR** `WH_SALE_ITEM_ID`. It **deliberately does NOT fall back to
100
+ `$payload->name`** — that is freeform client text the dedup never consults, so matching on it
101
+ would make the guard and the dedup disagree.
102
+
103
+ ### 2. Second latent defect in the same path — `state.code` vs `state.uuid` (FIXED)
104
+
105
+ The guard read `$address->state->code`, but the payload sends **`state.uuid`**. **Fixing #1
106
+ alone would have thrown *"A valid service address is required"* on every valid purchase.**
107
+ Added `resolveStateCode()` (prefers `code`, falls back to resolving the `uuid`).
108
+
109
+ Also: the canonical write-back now **only overwrites `state->code` when the payload ALREADY
110
+ carried that key**. Adding a `code` key to a uuid-only nested object is an **`EV-8` risk that
111
+ rejects the whole POST.**
112
+
113
+ ### 3. Address comparison is now unit-level and ZIP5-level
114
+
115
+ See [address-uniqueness-normalization](../../../2.0/apps/_underscore/features/address-uniqueness-normalization.md):
116
+ `normalizeUnitIdentifier()` collapses `Suite 500` / `STE 500` / `#500` / `Apt 500` / `500` to
117
+ `500` (an **empty** unit means the whole building and conflicts with **any** unit at that street
118
+ address, in both directions), and `comparableZip()` compares the first 5 digits because USPS
119
+ returns **ZIP+4** (`60503-7832`) while every legacy `Addresses` row holds 5 digits — an exact
120
+ comparison had been missing **every legacy row**. SQL narrows on
121
+ `line1 + city + ZIP5 + state`; the unit comparison runs in **PHP** over up to
122
+ `DEDUP_CANDIDATE_LIMIT = 500` candidates (with an `error_log` canary).
123
+
124
+ ### 4. USPS DPV is now enforced — this is what implements the coverage rule
125
+
126
+ See [usps-dpv-deliverability](../../../2.0/apps/_underscore/features/usps-dpv-deliverability.md).
127
+ The waterfall was **discarding** DPV, and USPS returns **HTTP 200 with a populated address even
128
+ when the unit is missing or invented** (it echoes `UNIT 200` back verbatim), so success alone was
129
+ insufficient. Mapping: **`Y`** proceed / **`D`** reject *unit required* / **`S`** reject *unit not
130
+ verified* / **`N`** reject / **unknown proceeds** (FedEx/UPS report no DPV; failing closed would
131
+ block every purchase during a USPS outage, and the dedup still prevents duplicates).
132
+
133
+ **This is what implements the business rule the developer specified: coverage is per UNIT when
134
+ the building is subdivided, and per BUILDING when it is not.**
135
+
136
+ ### 5. `evaluateWholeHomeAddress()` is the SINGLE authority, exposed as an API
137
+
138
+ One method performs: **completeness → carrier validation → DPV verdict → canonical adoption →
139
+ global duplicate check**. `prePost()` **throws** on it; the new **`warrantyAvailability`**
140
+ scripted API returns **the same verdict as data**. They cannot drift, because it is one method.
141
+
142
+ Machine-readable reasons: `UNAVAILABLE_INCOMPLETE_ADDRESS`, `UNAVAILABLE_ADDRESS_UNVERIFIED`,
143
+ `UNAVAILABLE_UNIT_REQUIRED`, `UNAVAILABLE_UNIT_INVALID`, `UNAVAILABLE_DUPLICATE_WARRANTY`, plus
144
+ the canonical address.
145
+
146
+ **Why it had to move server-side.** The availability check previously ran in the React app
147
+ against **`useActiveServices(borrowerId)`** — a query scoped to the **logged-in borrower** — and
148
+ could only see a warranty whose `serviceAddressId` had been **pinned**. **A warranty is per
149
+ DWELLING, not per customer**, so it reported "eligible" whenever the existing warranty belonged
150
+ to **someone else** or was **unpinned**; the customer then **paid through PayPal** and only then
151
+ hit the server guard. Widening the FE query is **not** an option (cross-borrower data leak), so
152
+ the question must be answered server-side. The response is a **boolean + generic reason**,
153
+ leaking nothing about who holds an existing warranty. `toga2-view`'s local dedup in
154
+ `pages/ZipValidation/*` was **DELETED, not patched**, and the page **fails CLOSED** if the call
155
+ errors.
156
+
157
+ ### 6. Registration: a client-side `CustomRecordScript` (reusable pattern)
158
+
159
+ `dbchanges2/Client_Rate/2026-07-31a - WarrantyAvailabilityCustomRecordScript.sql` registers the
160
+ endpoint in the **client** database, **not** `Core.RecordScripts` — see
161
+ [Record Scripts → client-side CustomRecordScripts](../../../2.0/apps/api2/features/record-scripts.md)
162
+ for the full rationale (both tables are `DB_CLIENT`, so the grant resolves the script id from a
163
+ hardcoded uuid in **one** database; a `Core` registration would force an impossible cross-tenant
164
+ read). `recordId` is the literal **191**. Granted to **`Base` only, resolved by name** (role ids
165
+ differ per client) and **never `Public`** — the endpoint reveals whether a warranty exists at an
166
+ arbitrary address, so unauthenticated probing must be prevented. Verified by applying it twice to
167
+ two environments: exactly one script row + one grant row.
168
+
169
+ **Read the response at `data.entitlements["warranty-availability"]`** — the route verbatim, not
170
+ the camelCase `phpMethod`. Reading `data.entitlements.warrantyAvailability` yields `undefined`
171
+ and (correctly) fails closed; this cost a debug cycle in beta.
172
+ (`toga2-view/src/pages/ZipValidation/api/zipValidation.ts`)
173
+
174
+ ### 7. Why the "works in beta, fails in production" report was FALSE
175
+
176
+ Same code, same migrations, **byte-identical `ApiPayloadInterceptors` rows** in both environments
177
+ (verified). The backend guard had **never run anywhere**. Beta only *appeared* correct because
178
+ the **frontend** check happened to work there: every WH entitlement in beta belongs to a
179
+ **single contact (`contactId 8`)** and all of its data was created **after** the pin shipped —
180
+ satisfying both of the FE check's hidden preconditions (**same borrower**, **already pinned**).
181
+ Production satisfied **neither**.
182
+
183
+ > **Verification lesson (general):** *beta passing proves nothing if the dataset cannot exercise
184
+ > the failing path.* Test as a **different borrower** and against **legacy / unpinned** data.
185
+
68
186
  ## Business rules (decided this session — Mark / Paulina)
69
187
 
70
188
  - **Invalid address → HARD BLOCK.** An address that fails carrier validation blocks the
@@ -72,8 +190,18 @@ Ticket: TRUE-79533. Business rule owner: PM Paulina.
72
190
  - **Uniqueness is GLOBAL per address**, not per-borrower — a second active WH on the same
73
191
  physical address is rejected regardless of which borrower buys it.
74
192
  - **Address line2 distinguishes units.** A landlord's apartment units are distinct addresses:
75
- 2A / 2B / 2C = three separate WHs. Uniqueness keys on the full normalized address including
76
- line2.
193
+ 2A / 2B / 2C = three separate WHs. Uniqueness keys on the **normalized unit identifier** of
194
+ line2 (see [#3 above](#3-address-comparison-is-now-unit-level-and-zip5-level)) — designators are
195
+ discarded, and an **empty** unit means the whole building and conflicts with every unit.
196
+ - **Coverage is per UNIT when the building is subdivided, per BUILDING when it is not** — and the
197
+ only signal that distinguishes those cases is **USPS DPV** (`D` = secondary unit required). See
198
+ [#4 above](#4-usps-dpv-is-now-enforced--this-is-what-implements-the-coverage-rule).
199
+ - **`dateEnd` stays OUT of the dedup predicate (DECIDED 2026-07-31).** The active test is
200
+ `Subscriptions.isActive = 1 AND dateCancelled IS NULL`. Adding `dateEnd` would let **real**
201
+ duplicates through whenever a renewal has not rolled `dateEnd` forward — and renewals are not
202
+ advancing it: **production entitlement 40 (ET100014, Kimberly Stearns) has `dateEnd`
203
+ 2026-06-01 yet was charged 2026-07-01.** That is a **separate defect worth its own ticket**;
204
+ `dateEnd` is currently unreliable as an indicator of anything.
77
205
  - **Persist the validated address ON the entitlement** (via the `serviceAddressId` FK) rather
78
206
  than deriving it from the fragile contact-primary-address path. **No backfill ships** —
79
207
  production launches with no pre-existing WH entitlements, so there is nothing to backfill (the
@@ -223,15 +351,66 @@ endpoint mechanics.
223
351
 
224
352
  A self-contained backend verification script lives at
225
353
  `test/@Mark/Rate/verify_wholehome_per_address_guard.php` (run via PHP CLI with `ENVIRONMENT`
226
- set, pinned to the real migrated **`Client_Rate`** schema — see the
227
- [2.0 backend-testing standard](../../../2.0/standards/backend-testing.md)). It applies
354
+ set, pinned to the real migrated **`Client_Rate`** schema — there is no PHPUnit here, so
355
+ backend verification is a standalone CLI script pinned to a real client database). It applies
228
356
  `c_serviceAddressId` idempotently, seeds a token-tagged scratch WH graph (2 addresses / 2 items /
229
357
  2 active subscriptions / 2 entitlements), then exercises the pure helpers, the dedup matrix (via
230
358
  reflection on the private methods), and the `prePost` branches, cleaning up in a `finally`. The
231
359
  live carrier waterfall is opt-in via `RUN_LIVE=1` (defaults off). Verified **18/18 pass**.
232
360
 
361
+ > **⚠ That script passes 18/18 against a payload shape production NEVER SENDS** (`saleItem.title`
362
+ > present, `state.code` present). **That is precisely why all three defects survived review.**
363
+ > The replacement — **`test/@Mark/Rate/verify_wh_gate_and_unit_dedup.php` (77 assertions, all
364
+ > passing)** — asserts against the **real uuid-only `saleItem` / `state` payload shape**, and its
365
+ > `RUN_LIVE=1` section asserts the four live USPS DPV verdicts and that `prePost` **throws** for
366
+ > `D` and `S`. **Verification scripts must use the real payload shape.**
367
+
368
+ ## Production + beta data cleanup (2026-07-31, done by the developer, query-verified)
369
+
370
+ **Production** — three active WH warranties are now pinned and validated: **43 (ET100017) →
371
+ address 9 Naperville**, **40 (ET100014) → address 6 Nashua**, **38 (ET100012) → address 4
372
+ Wailuku**. Subscription **22 (ET100015, Daniel Moran)** closed out (`isActive = 0`,
373
+ `dateCancelled` 2026-06-03). Address **7** city corrected from `"60657"` to `"Chicago"`. The
374
+ **both-or-neither invariant** (`Entitlements.serviceAddressId` set ⟺ `Addresses.isValidated = 1`)
375
+ holds on all **nine** rows.
376
+
377
+ **Beta** — 23 subscriptions deactivated, leaving **0 active unpinned WH entitlements**.
378
+ Previously **22 unpinned entitlements all belonged to `contactId 8`** and collapsed onto **ONE**
379
+ address (`1864 HIGH GROVE LN STE 500`) through the `COALESCE(serviceAddressId, contact primary
380
+ address)` fallback — which is what made beta's data look coherent.
381
+
382
+ > **Near-miss worth remembering:** beta entitlements **100 / 105 / 106** were initially
383
+ > misidentified as duplicates from a query that selected only `line1 + zip`. They are in fact
384
+ > **DISTINCT units** (`STE 1900` / `# 4` / `STE 500`) and must **NOT** be deleted.
385
+ > **Always fetch `line2` before judging address duplication.**
386
+
233
387
  ## Gotchas / known issues
234
388
 
389
+ - **Apartment / property-type eligibility is CLIENT-SIDE ONLY and bypassable.** The property
390
+ questionnaire's `propertyType` lives in **router state / `sessionStorage`**
391
+ (`toga2-view/src/pages/CheckOut/viewModel/useCheckoutPageViewModel.ts`) and is **never sent to
392
+ the API** — the purchase payload carries no `propertyType` / `isEligible`. `prePost` requires
393
+ `line1`/`city`/`state`/`zip` but **not `line2`**, and the `requireUnitNumber` bounce-back is
394
+ **frontend navigation state only**. So an apartment building could be insured with **no unit at
395
+ single-dwelling pricing**. Live evidence: beta **ET100073** carries `line2` `"STE 500"` on a
396
+ **single-family** home. The **DPV `D`/`S` enforcement closes this server-side.** Note that
397
+ carriers cannot supply dwelling type at all (UPS/FedEx classification is only Residential vs
398
+ Commercial, and an apartment building is *Residential*) — true dwelling type needs property
399
+ data (assessor/parcel or a Melissa/Smarty-class vendor), a **procurement decision, not a code
400
+ one**.
401
+ - **⚠ CORRECTION to the 2026-07-30 session doc.**
402
+ `knowledge/sessions/2026-07-30-TRUE-80487-Debug-Fix-Rate-WH-Warranty-mhammontree.md` records
403
+ *"cross-cluster `Core.*` refs from `Client*/` folders fail in prod — DISPROVEN … the ACL grants
404
+ landed correctly."* **That conclusion is WRONG.** The grants **exist** in production but
405
+ **cannot have been created by those migrations** — `Client/2026-07-16c -
406
+ AddressValidateScriptAcl.sql` silently no-ops, and Core script 28 (`addresses/validate`) has
407
+ **zero** `AclRecordScripts` rows in `Client_Rate`. Do not trust that line. See
408
+ [address-validation](../../../2.0/apps/_underscore/features/address-validation.md) and the
409
+ [dbchanges2 surface-layer audit](../../../2.0/apps/dbchanges2/features/surface-layer-schema.md).
410
+ - **Renewals are not advancing `Subscriptions.dateEnd`** (production entitlement 40 charged a
411
+ month past its `dateEnd`). Separate defect, separate ticket — and the reason `dateEnd` is
412
+ excluded from the dedup predicate.
413
+
235
414
  - **Shared-interceptor merge hazard — TRUE-79533 and TRUE-79251 edit the SAME methods.** Both
236
415
  this guard and the [purchase-email workflow](service-purchase-emails.md) (TRUE-79251) modify the
237
416
  **same** `prePost`/`postPost` interceptors on `_Model_Rate_Entitlement`, and TRUE-79251 also adds
@@ -350,6 +529,33 @@ live carrier waterfall is opt-in via `RUN_LIVE=1` (defaults off). Verified **18/
350
529
  block) — check `isValidated` to see whether the pin fired. (General 2.0 framework lesson.)
351
530
 
352
531
  ## Change history
532
+ - 2026-07-31 — **THE GUARD HAD NEVER EXECUTED IN ANY ENVIRONMENT** (TRUE-80487, follow-up to the
533
+ already-deployed TRUE-79533). `isWholeHomeWarranty()` tested `$payload->saleItem->title` but the
534
+ real checkout payload sends `{"saleItem":{"uuid"}}` with no title, so `prePost` returned before
535
+ the guard on every purchase — its rejection message appears in **no log in any environment**
536
+ (proof: dev-sandbox held 3 active WH entitlements at one address, two created on the very day
537
+ the feature was called "beta-verified"). Fixed by resolving `saleItem.uuid → Items` and matching
538
+ on the same authority as the DB dedup (title keyword OR `WH_SALE_ITEM_ID`; deliberately **not**
539
+ `$payload->name`). Fixed a **second latent defect** in the same path — the guard read
540
+ `$address->state->code` while the payload sends `state.uuid`, so fixing the first alone would
541
+ have thrown "A valid service address is required" on every valid purchase (added
542
+ `resolveStateCode()`; the canonical write-back now only overwrites `state->code` when the payload
543
+ already carried that key, since adding it to a uuid-only node is an `EV-8` risk). Built
544
+ **unit-level + ZIP5 address comparison**, enforced **USPS DPV** (`Y`/`D`/`S`/`N`, unknown
545
+ proceeds) as the per-unit-vs-per-building signal, consolidated everything into the single
546
+ authority **`evaluateWholeHomeAddress()`**, and exposed it as the **`warrantyAvailability`**
547
+ scripted API (registered as a client-side `CustomRecordScript`, `Base`-only, never `Public`) so
548
+ the FE no longer answers a per-dwelling question with a per-borrower query — the old check
549
+ reported "eligible" whenever the existing warranty belonged to another borrower or was unpinned,
550
+ and the customer paid through PayPal before hitting the server guard. Recorded that the
551
+ "works in beta, fails in prod" report was **false** (identical code/migrations; beta only looked
552
+ right because all its WH data belonged to one already-pinned contact), that apartment eligibility
553
+ is **client-side only and bypassable**, that `dateEnd` stays out of the dedup (renewals are not
554
+ advancing it — separate defect), the production/beta data cleanup, and the **correction to the
555
+ 2026-07-30 session doc's "cross-cluster refs DISPROVEN" claim**. New verification script
556
+ `test/@Mark/Rate/verify_wh_gate_and_unit_dedup.php` (77 assertions) asserts the **real** payload
557
+ shape — the old 18/18 script passed against a shape production never sends, which is why all
558
+ three defects survived review. (mhammontree)
353
559
  - 2026-07-27 — **Shipped working on beta end-to-end; status draft→active** (TRUE-79533). Found and
354
560
  fixed the **final** pin failure: the FE was POSTing `{depth:-1}` on `POST /entitlements`, building a
355
561
  shallow V2 payload (`calcDepth 1`) that **starved `postPost`** of the nested
@@ -9,11 +9,12 @@ apps:
9
9
  - worker
10
10
  - worker2
11
11
  - dbchanges2
12
+ - test
12
13
  project: SAML SSO Gateway
13
14
  client: rate
14
15
  type: profile
15
16
  status: active
16
- updated: 2026-07-23
17
+ updated: 2026-07-31
17
18
  owners: ["rgirish", "bala", "mhammontree", "tcox"]
18
19
  files: []
19
20
  related:
@@ -37,7 +37,7 @@ updated: 2026-07-30
37
37
  - **"TRUE-79251 merge damage deleted the WH helper methods."** DISPROVEN. `git diff origin/_beta -- Model/Rate/Entitlement.php` → zero diff. `PASSTHROUGH_KEY_FULFILLMENT_PREFERENCE` (line 32) and all three private helpers (`normalizedField` 107, `isWholeHomeWarranty` 116, `hasActiveWarrantyAtAddress` 137) present.
38
38
  - **"`_underscore` prod wasn't redeployed past the uuid-clobber fix `9c2e4b1c`."** DISPROVEN by the same zero diff + PR #661 merged.
39
39
  - **"`toga2-view` prod still sends `{depth:-1}` and starves `postPost`."** DISPROVEN. `genericApi.ts:54,68` only append `?depth=-1` when `returnPayloadDepth` is explicitly set; no checkout override. Diff vs `_beta` touches only GetSupport files.
40
- - **"Cross-cluster `Core.RecordFields` references from `Client*/` migration folders fail in prod."** DISPROVEN for this case. Despite prod splitting `prod-core` / `prod-client` / `prod-logs` / `prod-archive` onto separate clusters, the ACL grants landed correctly. ~20 long-standing ACL migrations use this same pattern. (Note: the `dbchanges2-cluster-isolation` hook was NOT found under `.claude/hooks/toga/` — separate follow-up, not this bug.)
40
+ - **"Cross-cluster `Core.RecordFields` references from `Client*/` migration folders fail in prod."** **⚠ CORRECTED 2026-07-31 — THIS CONCLUSION WAS WRONG.** The grants *exist* in production but **cannot have been created by those migrations**: `Client/2026-07-16c - AddressValidateScriptAcl.sql` silently no-ops and Core script 28 (`addresses/validate`) has **zero** `AclRecordScripts` rows in `Client_Rate`. A literal `Core.<Table>` also fails **locally**, where `Core` is the **1.0** database (2.0 core is `Core_2`). An audit found 24 such files. See `clients/rate/features/whole-home-warranty-purchase-guard.md` and `2.0/apps/dbchanges2/features/surface-layer-schema.md`. Original (incorrect) text follows: DISPROVEN for this case. Despite prod splitting `prod-core` / `prod-client` / `prod-logs` / `prod-archive` onto separate clusters, the ACL grants landed correctly. ~20 long-standing ACL migrations use this same pattern. (Note: the `dbchanges2-cluster-isolation` hook was NOT found under `.claude/hooks/toga/` — separate follow-up, not this bug.)
41
41
  - **"The interceptor row is missing, inactive, or duplicated in prod."** DISPROVEN — prod rows are identical to dev-sandbox, same ids and uuids.
42
42
  - **"`POST /v2/entitlement-sales-orders` is the prod purchase path (record 257, no interceptor)."** DISPROVEN. It is only a bridge link: payload is `{"entitlement":{"uuid"},"salesOrder":{"uuid"}}`. It creates no entitlement and fires no interceptor.
43
43
  - **"Production simply hasn't processed a purchase since deploy, so there's nothing to debug."** TRUE as a fact (last `POST /v2/entitlements` in prod = 2026-07-10, two × 400 `EV-8`; newest `Entitlements` row 2026-06-15) but WRONG as a conclusion — the developer then reproduced the failure live through the UI. Do not stop at this observation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.489",
3
+ "version": "1.0.490",
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",