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.
- package/knowledge/2.0/apps/_underscore/INDEX.md +3 -1
- package/knowledge/2.0/apps/_underscore/features/address-uniqueness-normalization.md +96 -0
- package/knowledge/2.0/apps/_underscore/features/address-validation.md +56 -3
- package/knowledge/2.0/apps/_underscore/features/usps-dpv-deliverability.md +103 -0
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/record-scripts.md +70 -0
- package/knowledge/2.0/apps/api2/workflows/codepipeline-codeconnections-deploy.md +35 -2
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +32 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/rate/INDEX.md +1 -1
- package/knowledge/clients/rate/features/whole-home-warranty-purchase-guard.md +214 -8
- package/knowledge/clients/rate/profile.md +2 -1
- package/knowledge/sessions/2026-07-30-TRUE-80487-Debug-Fix-Rate-WH-Warranty-mhammontree.md +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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-
|
|
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.
|
|
72
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ —
|
|
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 | >
|
|
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-
|
|
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
|
-
>
|
|
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**
|
|
49
|
-
|
|
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
|
|
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 —
|
|
227
|
-
|
|
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-
|
|
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