toga-ai 1.0.441 → 1.0.442
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/architecture.md +11 -1
- package/knowledge/2.0/apps/api2/architecture.md +9 -5
- package/knowledge/2.0/apps/api2/features/v2-api-error-codes.md +5 -1
- package/knowledge/2.0/apps/api2/workflows/codepipeline-codeconnections-deploy.md +17 -1
- package/knowledge/2.0/standards/backend-php.md +32 -2
- package/knowledge/clients/rate/INDEX.md +2 -2
- package/knowledge/clients/rate/features/aig-contract-creation.md +14 -6
- package/knowledge/clients/rate/features/service-card-entitlements.md +17 -2
- package/knowledge/clients/rate/features/whole-home-warranty-purchase-guard.md +48 -19
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-27
|
|
10
10
|
owners: ["jcardinal", "rgirish", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/_underscore.php
|
|
@@ -341,6 +341,15 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
|
|
|
341
341
|
so folder reads always returned null; fixed. **Remaining follow-up (separate ticket):** `__get()`
|
|
342
342
|
still refetches storage unconditionally, so a dirty read (read-after-assign, pre-save) discards the
|
|
343
343
|
unsaved value.
|
|
344
|
+
- **In-request reads can't see the transaction's own uncommitted nested writes — resolve just-created
|
|
345
|
+
records from the hydrated `$payload`, not a same-request DB read-back.** A `postPost` interceptor
|
|
346
|
+
runs **inside the still-open request transaction**. A fresh `_Query` SELECT — even one forced onto
|
|
347
|
+
the write host (the api2 V2 engine already disables the read host for the whole create block) — does
|
|
348
|
+
**not** reliably resolve the rows the same transaction just inserted, especially nested/child rows.
|
|
349
|
+
Read the in-memory `$payload` node hydrated by `getFullModelData()` instead. Pairs with the
|
|
350
|
+
lazy-transaction gotcha below. (TRUE-79533: the Rate WH service-address pin — fingerprint of a lost
|
|
351
|
+
pin is `Addresses.isValidated` and `Entitlements.serviceAddressId` both-or-neither. Relevant code:
|
|
352
|
+
`_underscore/Model/Rate/Entitlement.php`, `api2/Component/Api/V2/V2.php::getFullModelData`.)
|
|
344
353
|
- **`_Database::register()` auto-starts a lazy transaction (since Apr 2 2026, commit `fa7835ed`).** Any code that calls `register()` and then writes to that DB must call `_Database::transactionCommit()` before the request ends — otherwise MySQL silently rolls back all writes when the connection closes. Lazy transactions only materialise on the first write, so read-only callers are unaffected. See `_underscore/Database.php:48`. First discovered when Rate SAML user provisioning silently discarded all new user INSERTs (Jun 2026).
|
|
345
354
|
- **PHP "Unclosed '{'" parse errors report a MISLEADING line number.** When a `.php` file loaded by the autoloader (`Loader.php`) has a dropped/unbalanced brace, PHP reports `Unclosed '{' on line N` where N is the **outermost `class X {` line** and fails at EOF — NOT at the true location of the missing `}`. Worse, because the file loads lazily via the SPL autoloader, the runtime trace points at the **caller** that triggered the autoload (e.g. a `new _Email()` call site), not the broken file. **Triage rule:** for an "Unclosed '{'" error, the real culprit is a missing `}` somewhere between the reported line and EOF of the file that failed to load — run `php -l <file>` (it reports the EOF line) and scan the whole file. **Merge-conflict resolutions are a common source of a single dropped brace** — review the entire merge, not just the one file the error appears to name. (First hit: production 500 EO-1, Jul 2026 — a `}` dropped from `_Email::send()` during merge `685e4a14` surfaced as a trace pointing at the `new _Email()` caller.)
|
|
346
355
|
|
|
@@ -368,6 +377,7 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
|
|
|
368
377
|
stack trace) to API consumers. Not yet done — tracked as a follow-up.
|
|
369
378
|
|
|
370
379
|
## Change history
|
|
380
|
+
- 2026-07-27 — Added the gotcha that **in-request reads cannot see the transaction's own uncommitted nested writes** — a `postPost` interceptor must resolve just-created records from the `getFullModelData`-hydrated `$payload`, not a same-request `_Query` read-back (even on the write host). Found on the Rate WH service-address pin (TRUE-79533). (mhammontree)
|
|
371
381
|
- 2026-06-11 — Documented lazy transaction gotcha in `_Database::register()` (rgirish)
|
|
372
382
|
- 2026-06-25 — Added the Surface platform UI presentation/configuration layer (DB-driven UI config replacing `Page::meta()`, CTO-reviewed AGREE-WITH-ADJUSTMENTS) (jcardinal)
|
|
373
383
|
- 2026-06-29 — Surface made the enforced (un-flagged) presentation path; reserved id blocks renumbered to Records 333–341 / RecordFields 2246–2433 (known seed-vs-provisioned drift). (jcardinal)
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
10
|
-
owners: [jcardinal, bala]
|
|
9
|
+
updated: 2026-07-27
|
|
10
|
+
owners: [jcardinal, bala, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Controller/Index.php
|
|
13
13
|
- api2/Component/Api/V2/V2.php
|
|
@@ -162,9 +162,12 @@ subject ≤80 chars, capitalized, no trailing period, **imperative mood** (rejec
|
|
|
162
162
|
|
|
163
163
|
`Config/production.ini` (and dev configs) contain **plaintext production credentials** —
|
|
164
164
|
Core/Client/Logs DB passwords, **AWS access key id + secret**, and third-party API keys
|
|
165
|
-
(FedEx, UPS, PayPal, NetSuite, Cal.com, AIG, OptimumDesk, VAPI).
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
(FedEx, UPS, PayPal, NetSuite, Cal.com, AIG, OptimumDesk, VAPI). The **`.ebextensions/git.*.json`**
|
|
166
|
+
files (per-env — e.g. `git.json`, `git.sandbox-dev.json`) each carry a **plaintext GitHub Personal
|
|
167
|
+
Access Token**, used by the `.platform/hooks/prebuild/git.sh` clone hook to pull `_underscore` at
|
|
168
|
+
build. These must be **rotated** (treat the committed tokens as compromised) and moved to SSM
|
|
169
|
+
Parameter Store / EB env properties. **Flag this if you touch config or deploy.** (Location +
|
|
170
|
+
remediation only — do not record the token value anywhere.)
|
|
168
171
|
|
|
169
172
|
**Follow-up (deferred, separate ticket): raw exception disclosure to clients.** The error paths that
|
|
170
173
|
surface a caught `\Throwable` (now that Route.php rethrows and the bootstrap guard reports failures)
|
|
@@ -188,4 +191,5 @@ internal paths, schema names, and stack frames to API consumers. Sanitize the cl
|
|
|
188
191
|
or `execute()`.
|
|
189
192
|
|
|
190
193
|
## Change history
|
|
194
|
+
- 2026-07-27 — Sharpened the committed-secret note: the plaintext GitHub PAT lives in the per-env **`.ebextensions/git.*.json`** files (used by the `prebuild/git.sh` clone hook to pull `_underscore`), must be rotated and moved to SSM / EB env properties (location + remediation only, no value). (mhammontree)
|
|
191
195
|
- 2026-07-23 — Documented the now-guarded Core/Logs DB bootstrap in the front controller: the pre-execute block runs before the `execute()` try/catch, the Core Logs schema name is resolved from a `Core.Database` row (`id = CORE_LOGS_DATABASE_ID`) so a name-mismatched local Logs DB reads as missing, and the failure is now wrapped in `try/catch (\Throwable)` returning `INVALID_CONFIGURATION` + Sentry instead of a fatal (guarded no-op rollback, `Database.php:219–226`). Added the deferred raw-getMessage/getTrace client-disclosure follow-up to the Security note. (jcardinal)
|
|
@@ -37,7 +37,7 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
|
|
|
37
37
|
| **EO-1** | Operation failed — identifier "There is no field called 'X' in the '_Model_Client_Y' model" | The DB column **and** `Core.RecordFields` exist, but the **generated model class** `_underscore/Model/Client/<Name>.php` doesn't declare the field. This is the **4th** requirement beyond the 3-file migration — and most often it's a **cross-repo git branch mismatch** (`_underscore` on a branch whose generated model lacks a field the DB/RecordFields already carry) | Declare the field in the generated model class (`public $field = self::FIELD_*`) and put all related repos (`_underscore`, `api2`, `dbchanges2`, `toga2-supply`) on the **same** feature branch — see the ACL doc's writable-field recipe |
|
|
38
38
|
| **EO-1** | Operation failed — **surfaced from a PHP warning/notice, not a real op error** (e.g. "Attempt to read property 'id' on bool", undefined variable) | A latent PHP warning escalates to a 500 because **Sentry's `ErrorHandler` in api2 promotes warnings/notices into thrown exceptions** (see diagnosis note 5). The known instance: in `V2.php::processRoutePairs()` an **unresolved route** leaves the local `$record = false`, and the post-processing payload interceptor layer then dereferenced `$record->id` → warning → 500 | Guard before dereferencing an unresolved record. The fix added a guard clause `if (!$record) return [$rawRequestedRouteName => $outData];` **before** the interceptor/logging layer (so a bad route returns a clean envelope, not a fatal), and initializes `$record = false;` at the top of the `foreach ($lookupByRouteNames ...)` loop so the invalid-HTTP-method / null-`$action` branch can't leave `$record` undefined (an undefined-variable warning would itself escalate to a 500) |
|
|
39
39
|
| **EZ-1** | Unauthorized record/script dispatch | Missing **`AclRecordScripts`** (scripted APIs) or the **`AclRecordPermissions`** four-table chain (records) for the caller's role | Grant `AclRecordScripts` (scripts) or complete the record-CRUD chain |
|
|
40
|
-
| **EZ-2** | Field-level authorization denied (READ) | The field is registered (`Core.RecordFields` present → no `EV-8`) but the caller's role has no **`AclFieldPermissions`** grant to **read** it. The read-side counterpart of `EV-9` (write) | Add the `AclFieldPermissions` row for the role (`isWritable=0` if the field is server-written). For a **custom** `c_` field use `AclCustomFieldPermissions` instead — see the ACL doc's standard-vs-custom table |
|
|
40
|
+
| **EZ-2** | Field-level authorization denied (READ) | The field is registered (`Core.RecordFields` present → no `EV-8`) but the caller's role has no **`AclFieldPermissions`** grant to **read** it. The read-side counterpart of `EV-9` (write). **This applies to the `id` field too:** fetching a record by its numeric `id` (`GET /v2/<record>?fields=id,...`) 403s `EZ-2` if `id` has no read grant — the `id` field is ACL-gated like any other, not implicitly readable | Add the `AclFieldPermissions` row for the role (`isWritable=0` if the field is server-written). For a **custom** `c_` field use `AclCustomFieldPermissions` instead — see the ACL doc's standard-vs-custom table |
|
|
41
41
|
| **EV-5** | Duplicate `transactionId` | The globally-unique `transactionId` was reused | Send a fresh unique `transactionId` per request |
|
|
42
42
|
| **EV-12** | Record's parent not synced | (Fulfill & Ship) POST `/item-fulfillments` when the Sales Order isn't synced into Toga yet | `GET /sales-orders/syncNetsuiteSalesOrder?netsuiteInternalSalesOrderId=<id>` first |
|
|
43
43
|
|
|
@@ -81,6 +81,10 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
|
|
|
81
81
|
|
|
82
82
|
## Change history
|
|
83
83
|
|
|
84
|
+
- 2026-07-27 — TRUE-79533: noted the **`id`-field** case of `EZ-2` — fetching a record by its numeric
|
|
85
|
+
`id` (`GET /v2/addresses?fields=id,...`) 403s `EZ-2` when `id` has no `AclFieldPermissions` read
|
|
86
|
+
grant; the `id` field is ACL-gated like any field. Found fixing Rate's service-card address fetch
|
|
87
|
+
(`dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql`). (mhammontree)
|
|
84
88
|
- 2026-07-24 — Added a second **`EO-1`** case: a latent PHP **warning** (unresolved route →
|
|
85
89
|
`$record->id` on `bool` in `V2.php::processRoutePairs()`, or an undefined loop variable)
|
|
86
90
|
escalates to an HTTP 500 because api2's Sentry `ErrorHandler` promotes warnings/notices into
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-27
|
|
10
10
|
owners: ["jcardinal", "mhammontree"]
|
|
11
11
|
files: []
|
|
12
12
|
related: []
|
|
@@ -30,6 +30,21 @@ the team's process**, decoupled from the app branches; a code deploy does not ru
|
|
|
30
30
|
full feature promotion to an env = apply the `dbchanges2` migrations to that env **and** deploy the
|
|
31
31
|
app-repo `_<env>` branches.
|
|
32
32
|
|
|
33
|
+
> **Not every EB env pulls `_underscore` from its `_<env>` branch — check `git.<env>.json`.** The
|
|
34
|
+
> beta env **`API-Sandbox-Dev`** does **not** deploy `_underscore` from `_beta`. Its
|
|
35
|
+
> `.platform/hooks/prebuild/git.sh` `rm -Rf`s and **re-clones `_underscore` from the branch named in
|
|
36
|
+
> `.ebextensions/git.<env>.json`** — for this env, `git.sandbox-dev.json` → branch **`_sandbox-dev`** —
|
|
37
|
+
> **overwriting the CodePipeline-supplied copy.** So merging a change into `_beta` does nothing for
|
|
38
|
+
> this env; you must push it to **`_sandbox-dev`**. Always confirm the exact env↔branch mapping in the
|
|
39
|
+
> env's `git.<env>.json` before assuming `_<env>` applies.
|
|
40
|
+
|
|
41
|
+
### Debugging a beta env without Sentry — `Logs_<Client>.Api`
|
|
42
|
+
`Logs_<Client>.Api` is a **full server-side request log**: `queryString`, request/response payloads,
|
|
43
|
+
`direction` (`IN`/`OUT`), `instanceId`, and `executionTime`. It is the reliable way to see what a
|
|
44
|
+
beta env actually received and returned when Sentry isn't wired up. (The Rate AIG-contract monitor
|
|
45
|
+
scans this same `Logs_Rate.Api` for failed outbound calls — see
|
|
46
|
+
`clients/rate/features/aig-contract-creation.md`.)
|
|
47
|
+
|
|
33
48
|
## Steps
|
|
34
49
|
1. A push to the watched branch (e.g. `_stage`) triggers the pipeline.
|
|
35
50
|
2. The Source stage uses `CodeStarSourceConnection` actions, each referencing:
|
|
@@ -113,6 +128,7 @@ aws codeconnections get-connection --connection-arn "<CONN_ARN>" --region "$REGI
|
|
|
113
128
|
```
|
|
114
129
|
|
|
115
130
|
## Change history
|
|
131
|
+
- 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)
|
|
116
132
|
- 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)
|
|
117
133
|
- 2026-06-18 — Added two EB-instance gotchas surfaced during the TOGa Supply beta/prod label deploys: (1) terminated instances must be **manually re-registered** as LB targets (we don't pay for auto-registration) — until then the new code never serves traffic and looks like deploy-lag; (2) on-instance `composer require` stopgap syntax (no space after the colon, fix root cause by committing `composer.lock`). (mhammontree)
|
|
118
134
|
- 2026-06-16 — Documented after a pipeline (API-QC-Security, account 975050298201) failed every
|
|
@@ -5,8 +5,8 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
9
|
-
owners: [jcardinal]
|
|
8
|
+
updated: 2026-07-27
|
|
9
|
+
owners: [jcardinal, mhammontree]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- ../apps/_underscore/architecture.md
|
|
@@ -542,6 +542,16 @@ $c = 'Hello ' . $world;
|
|
|
542
542
|
|
|
543
543
|
* Always use exceptions to handle unexpected conditions.
|
|
544
544
|
|
|
545
|
+
### Interceptor / model client-error rejections → throw `_Exception_Validation`, not `_Exception`
|
|
546
|
+
|
|
547
|
+
A hard-block raised inside a pre/post interceptor (or model op) to reject a **bad client request**
|
|
548
|
+
must throw **`_Exception_Validation`** (`_underscore/Exception/Validation.php`), which
|
|
549
|
+
`api2/Controller/Index.php` (≈lines 204–222) maps to `DEFINED_MESSAGE_ERROR_BAD_REQUEST` →
|
|
550
|
+
**HTTP 400** with the message carried in the response envelope (so the front end can drive a
|
|
551
|
+
toaster). Throwing a plain `_Exception` yields an **HTTP 500 + stack trace** — wrong for a
|
|
552
|
+
client-input rejection and an information leak. First applied: the three Rate WH hard-blocks
|
|
553
|
+
(missing / invalid / duplicate service address), TRUE-79533.
|
|
554
|
+
|
|
545
555
|
### Comprehensive Error Handling
|
|
546
556
|
|
|
547
557
|
* Implement comprehensive error handling to gracefully manage exceptions and provide meaningful error messages.
|
|
@@ -658,6 +668,26 @@ public static function prePut(&$api, &$payload) {
|
|
|
658
668
|
}
|
|
659
669
|
```
|
|
660
670
|
|
|
671
|
+
#### Interceptor payloads are bounded by request `depth` — a shallow request silently starves a POST interceptor
|
|
672
|
+
|
|
673
|
+
A `POST` payload interceptor (`postPost`) only receives the nested records the **built response
|
|
674
|
+
payload** includes. The V2 engine builds that payload to the request's `depth`/`calcDepth`; a low or
|
|
675
|
+
`-1` `depth` yields a shallow payload (`meta.calcDepth 1`) and any nested node the interceptor reads
|
|
676
|
+
(e.g. `contact.primaryContactAddress.address`) is **absent** — so interceptor logic that depends on
|
|
677
|
+
it silently no-ops. Because a deep-payload dev/manual request passes, this fails **only** for the
|
|
678
|
+
client sending a shallow depth.
|
|
679
|
+
|
|
680
|
+
- **`ApiPayloadInterceptors.minDepth` is the intended floor** (defaults to 3 when NULL; an interceptor
|
|
681
|
+
row can raise it — e.g. the entitlements `postPost` row = 5). **Known latent bug (needs its own
|
|
682
|
+
ticket):** for a `depth=-1` request the clamp did **not** actually deepen the payload — the `WO-1`
|
|
683
|
+
warning logged *"raised to minDepth 5"* while `calcDepth` stayed 1. Until fixed, do not rely on
|
|
684
|
+
`minDepth` to guarantee an interceptor sees its nested nodes.
|
|
685
|
+
- **Rule:** any `postPost`/`prePost` that reads nested payload data must not assume it is present —
|
|
686
|
+
guard for the node, and be aware that a client's shallow `depth` (or a front end that hard-codes
|
|
687
|
+
`depth:-1`) will starve it. First hit: the Rate WH `serviceAddressId` pin + AIG contract block
|
|
688
|
+
(TRUE-79533) — both read the same starved node. Relevant code: `api2/Component/Api/V2/V2.php`,
|
|
689
|
+
`api2/Controller/Index.php`.
|
|
690
|
+
|
|
661
691
|
### API Versioning
|
|
662
692
|
|
|
663
693
|
* Implement a new version (ie: `/v1`, `/v2`, `/v3`) when a major API framework is introduced to manage the new API framework without breaking existing clients.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| [Rate Monthly Reconciliation Report](features/monthly-reconciliation-report.md) | 1.0 | A monthly cron that emails an Excel reconciliation report covering all Rate subscription sales orders and their linked PayPal payments for the prior calendar mo | worker/crons/notifications/reports/rate/send_monthly_rate_purchases_report.php, worker/schedules/cron.worker.notification.json |
|
|
7
7
|
| [Rate SalesOrder → NetSuite CashSale Export (postPost)](features/netsuite-cashsale-export.md) | 2.0 | Rate sells home-warranty / home-tech-support products. | _underscore/Model/Rate/SalesOrder.php, _underscore/Model/Rate/Item.php |
|
|
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
|
-
| [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 |
|
|
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-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 |
|
|
12
12
|
| [Rate](profile.md) | 2.0 | Rate is a mortgage/lending client. | |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: rate
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-27
|
|
10
10
|
owners: [mhammontree, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Rate/Entitlement.php
|
|
@@ -36,11 +36,14 @@ This is a base-`_underscore` interceptor specific to Rate's product (it lives un
|
|
|
36
36
|
`Model/Rate/`); documented here as a Rate client-feature because the behavior and its
|
|
37
37
|
monitoring are Rate-scoped.
|
|
38
38
|
|
|
39
|
-
**As of 2026-07-
|
|
40
|
-
address**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
**As of TRUE-79533 (beta-verified 2026-07-27)** the same `postPost` also persists the validated
|
|
40
|
+
**service address** — now via the **standard** `Entitlements.serviceAddressId` FK (the Rate-custom
|
|
41
|
+
`c_serviceAddressId` was promoted to a base-model standard field) plus `Addresses.isValidated=1` —
|
|
42
|
+
wrapped so it never blocks this AIG-contract flow or the confirmation email. For Whole Home Warranty
|
|
43
|
+
purchases, a `prePost` guard also **carrier-normalizes the address onto the payload before save**, so
|
|
44
|
+
the AIG contract is created with the canonical address. Note both this AIG block and the WH pin read
|
|
45
|
+
the **same** nested `contact.primaryContactAddress.address` payload node, so a shallow request
|
|
46
|
+
`depth` starves both — see the WH guard doc's depth-starvation gotcha. See the
|
|
44
47
|
[Whole Home Warranty per-address purchase guard](whole-home-warranty-purchase-guard.md).
|
|
45
48
|
|
|
46
49
|
## How it works
|
|
@@ -107,6 +110,11 @@ creation vs. cancellation. This is an accepted, documented limitation — not an
|
|
|
107
110
|
|
|
108
111
|
## Change history
|
|
109
112
|
|
|
113
|
+
- 2026-07-27 — TRUE-79533 beta-verified: updated the service-address note to the **standard**
|
|
114
|
+
`Entitlements.serviceAddressId` FK (the Rate-custom `c_serviceAddressId` was promoted to a
|
|
115
|
+
base-model standard field), and noted that this AIG block and the WH pin read the **same**
|
|
116
|
+
`contact.primaryContactAddress.address` payload node — so a shallow request `depth` starves both
|
|
117
|
+
(cross-linked the WH guard's depth-starvation gotcha). (mhammontree)
|
|
110
118
|
- 2026-07-23 — Documented that the **AIG-success branch reassigns `$payload->uuid`** (as the contract
|
|
111
119
|
number) inside the shared `postPost`, which silently broke the WH service-address pin on the
|
|
112
120
|
pre-redesign build (pin keyed off the now-reassigned uuid). Fixed by snapshotting the entitlement
|
|
@@ -6,8 +6,8 @@ project: TOGa View Frontend
|
|
|
6
6
|
client: rate
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
10
|
-
owners: ["bala", "tcox"]
|
|
9
|
+
updated: 2026-07-27
|
|
10
|
+
owners: ["bala", "tcox", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- src/components/ServiceCard/ServiceCard.tsx
|
|
13
13
|
- src/components/ServiceCard/index.ts
|
|
@@ -20,6 +20,7 @@ files:
|
|
|
20
20
|
- src/pages/Services/viewModels/useServicePageViewModel.ts
|
|
21
21
|
- src/api/serviceAddressApi.ts
|
|
22
22
|
- src/api/apiErrors.ts
|
|
23
|
+
- dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql
|
|
23
24
|
related:
|
|
24
25
|
- clients/rate/profile.md
|
|
25
26
|
- whole-home-warranty-purchase-guard.md
|
|
@@ -118,6 +119,14 @@ product types would need `detectServiceType` extended or overridden.
|
|
|
118
119
|
`contact.primaryContactAddress` fallback made every unpinned card display the same (latest)
|
|
119
120
|
address because that field is one shared, purchase-overwritten value per contact. An unpinned
|
|
120
121
|
entitlement must render addressless.
|
|
122
|
+
- **The `id` field itself needs a read ACL when the FE fetches records by id (Client_Rate, fixed
|
|
123
|
+
2026-07-24).** `fetchAddressesByIds` issues `GET /v2/addresses?fields=id,line1,...` — but without a
|
|
124
|
+
read grant on the **`Addresses.id` field** the whole call returns **403 `EZ-2`** and no address
|
|
125
|
+
renders (the numeric `id` field is ACL-gated like any other field, not implicitly readable). Fixed
|
|
126
|
+
by `dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql`, granting READ
|
|
127
|
+
(`isWritable=0`) on `Addresses.id` to the same role(s) that already read the sibling display fields
|
|
128
|
+
(`line1` etc.). **Reusable lesson:** whenever the FE reads a record by its numeric `id` field, that
|
|
129
|
+
`id` field needs its own `AclFieldPermissions` read grant.
|
|
121
130
|
- **The services fetch degrades gracefully on field-permission errors.** `apiErrors.ts`
|
|
122
131
|
`isFieldPermissionError()` treats **`EV-8`** (field not registered — `identifiers.field`,
|
|
123
132
|
singular) the **same as `EZ-2`** (no read ACL — `identifiers.fields`, array). When the
|
|
@@ -128,6 +137,12 @@ product types would need `detectServiceType` extended or overridden.
|
|
|
128
137
|
|
|
129
138
|
## Change history
|
|
130
139
|
|
|
140
|
+
- 2026-07-27 — TRUE-79533: recorded that `fetchAddressesByIds`
|
|
141
|
+
(`GET /v2/addresses?fields=id,line1,...`) returned **403 `EZ-2`** because the **`Addresses.id`
|
|
142
|
+
field** had no read ACL grant in `Client_Rate` — the numeric `id` is ACL-gated like any field. Fixed
|
|
143
|
+
by `dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql` (READ, `isWritable=0`, to the
|
|
144
|
+
roles that already read the sibling display fields). Captured the reusable lesson: reading a record
|
|
145
|
+
by its `id` field needs a read grant on that `id` field itself. (mhammontree)
|
|
131
146
|
- 2026-07-23 — TRUE-79533 (FE side): warranty card address now comes from the entitlement's **own**
|
|
132
147
|
`serviceAddressId` pin (fetched by id via `fetchAddressesByIds`), renamed all FE
|
|
133
148
|
`c_serviceAddressId` refs to the standard `serviceAddressId`, and **removed the
|
|
@@ -5,13 +5,14 @@ repo: _underscore
|
|
|
5
5
|
project: _Underscore
|
|
6
6
|
client: rate
|
|
7
7
|
type: client-feature
|
|
8
|
-
status:
|
|
9
|
-
updated: 2026-07-
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-27
|
|
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
|
+
- toga2-view/src/pages/CheckOut/api/checkoutApi.ts
|
|
15
16
|
- dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql
|
|
16
17
|
- dbchanges2/Core/2026-07-22a - EntitlementServiceAddressIdField.sql
|
|
17
18
|
- dbchanges2/Client_Rate/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql
|
|
@@ -22,23 +23,20 @@ related:
|
|
|
22
23
|
- ../../../2.0/apps/_underscore/features/address-validation.md
|
|
23
24
|
---
|
|
24
25
|
|
|
25
|
-
>
|
|
26
|
-
>
|
|
27
|
-
>
|
|
28
|
-
>
|
|
29
|
-
>
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
>
|
|
33
|
-
>
|
|
34
|
-
>
|
|
35
|
-
>
|
|
36
|
-
>
|
|
37
|
-
>
|
|
38
|
-
>
|
|
39
|
-
> now ships as **`dbchanges2/Client_Rate/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql`**
|
|
40
|
-
> (committed `b74f42a` on TRUE-79533) — see
|
|
41
|
-
> **[Launch blocker: standard field needs a READ ACL grant](#launch-blocker-true-79533--the-standard-field-needs-a-read-acl-grant)**.
|
|
26
|
+
> **STATUS (2026-07-27): beta-verified end-to-end; promoting to `_production`.** The final blocker
|
|
27
|
+
> to a correct service-address pin turned out to be a **front-end request-`depth` bug**: Rate's
|
|
28
|
+
> checkout was sending `{depth:-1}` on `POST /entitlements`, so V2 built a **shallow** payload
|
|
29
|
+
> (`meta.calcDepth 1`) and `postPost` never received the nested
|
|
30
|
+
> `contact.primaryContactAddress.address` node it pins from — the pin silently no-op'd on every
|
|
31
|
+
> purchase (in a way that still passed in a deep-payload dev context). **Tanner** (toga2-view FE)
|
|
32
|
+
> removed the `depth:-1` override in `checkoutApi.ts createService`; verified on **beta 2026-07-27**
|
|
33
|
+
> — post-fix WH purchases pin `Entitlements.serviceAddressId` + set `Addresses.isValidated=1` with
|
|
34
|
+
> **no backfill**. See the **"low request `depth` starves `postPost`"** gotcha below (and the 2.0
|
|
35
|
+
> framework lesson it points to). `serviceAddressId` is a **standard** FK on the **base**
|
|
36
|
+
> `_Model_Client_Entitlement` (no longer the Rate-custom `c_serviceAddressId`), and its READ ACL
|
|
37
|
+
> grant shipped (`2026-07-23a`, committed `b74f42a`). Prod launches clean (no pre-existing WH
|
|
38
|
+
> entitlements), so **no backfill ships**. See the **shared-interceptor merge hazard** and the
|
|
39
|
+
> **dangling-pin** / **unguarded prod interceptor** gotchas before merging to `_production`.
|
|
42
40
|
|
|
43
41
|
## Summary
|
|
44
42
|
|
|
@@ -330,8 +328,39 @@ live carrier waterfall is opt-in via `RUN_LIVE=1` (defaults off). Verified **18/
|
|
|
330
328
|
only carries type/identifier/childPolicy/precision, mirroring sibling FKs `saleItemId`/`vendorId`.
|
|
331
329
|
- **No `_Query` bind API here** — use `_Database::escape()` / int-casts for all interpolated
|
|
332
330
|
values.
|
|
331
|
+
- **Low request `depth` silently starved `postPost` of the address node (FE bug — root cause of the
|
|
332
|
+
final beta pin failures; fixed 2026-07-27).** A V2 `postPost` interceptor only receives the nested
|
|
333
|
+
records the **built response payload** includes. Rate's `toga2-view` checkout was POSTing
|
|
334
|
+
`{depth:-1}` on `POST /entitlements`, so V2 built a shallow payload (`meta.calcDepth 1`) and
|
|
335
|
+
`persistWarrantyServiceAddress()` — which reads `$payload->contact->primaryContactAddress->address`,
|
|
336
|
+
the **same node the AIG contract block reads** — got nothing, so the pin no-op'd on every purchase
|
|
337
|
+
(yet passed in deep-payload dev contexts, masking it). `ApiPayloadInterceptors.minDepth` is meant to
|
|
338
|
+
floor this (entitlements `postPost` row = 5), but the clamp did **not** actually deepen a `depth=-1`
|
|
339
|
+
request — the `WO-1` warning logged *"raised to minDepth 5"* while `calcDepth` stayed 1 (a **latent
|
|
340
|
+
framework bug worth its own ticket**). Fix that shipped: **Tanner** removed the `depth:-1` override
|
|
341
|
+
in `checkoutApi.ts createService` so the default deep payload reaches `postPost`. This starvation
|
|
342
|
+
broke BOTH the WH pin AND the pre-existing AIG contract-creation block (both read that node). See
|
|
343
|
+
the 2.0 framework lesson on payload-interceptor depth starvation.
|
|
344
|
+
- **Resolve the just-created entitlement from the hydrated `$payload`, NOT a DB read-back.** `postPost`
|
|
345
|
+
runs inside the still-open request transaction, so a fresh `_Query` SELECT (even forced onto the
|
|
346
|
+
write host — V2 already disables the read host for the whole create block) does **not** reliably
|
|
347
|
+
resolve the just-created nested rows in-request. Pin from the in-memory `$payload` node (hydrated by
|
|
348
|
+
`getFullModelData` — the same node the AIG block reads), not a read-back. Fingerprint of a lost pin:
|
|
349
|
+
`Addresses.isValidated=1` and `Entitlements.serviceAddressId` are **both-or-neither** (same persist
|
|
350
|
+
block) — check `isValidated` to see whether the pin fired. (General 2.0 framework lesson.)
|
|
333
351
|
|
|
334
352
|
## Change history
|
|
353
|
+
- 2026-07-27 — **Shipped working on beta end-to-end; status draft→active** (TRUE-79533). Found and
|
|
354
|
+
fixed the **final** pin failure: the FE was POSTing `{depth:-1}` on `POST /entitlements`, building a
|
|
355
|
+
shallow V2 payload (`calcDepth 1`) that **starved `postPost`** of the nested
|
|
356
|
+
`contact.primaryContactAddress.address` node (same node the AIG block reads), so the pin silently
|
|
357
|
+
no-op'd on every purchase — and `ApiPayloadInterceptors.minDepth` (=5) did **not** actually deepen a
|
|
358
|
+
`depth=-1` request (latent framework bug, own ticket). **Tanner** (toga2-view FE, kb-username not yet
|
|
359
|
+
known) removed the `depth:-1` override in `checkoutApi.ts createService`; verified on beta 2026-07-27
|
|
360
|
+
— WH purchases now pin `serviceAddressId` + set `Addresses.isValidated=1`, no backfill. Also recorded
|
|
361
|
+
the general lesson that `postPost` must resolve just-created records from the hydrated `$payload`, not
|
|
362
|
+
a same-request DB read-back (uncommitted nested writes aren't visible in-request). (mhammontree, with
|
|
363
|
+
Tanner)
|
|
335
364
|
|
|
336
365
|
- 2026-07-23 — **READ-ACL grant BUILT — launch blocker resolved** (TRUE-79533, committed `b74f42a`).
|
|
337
366
|
Added `dbchanges2/Client_Rate/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql`: a
|
package/package.json
CHANGED