toga-ai 1.0.441 → 1.0.443

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.
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-07-23
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-23
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). `.ebextensions/git.json`
166
- carries a **GitHub PAT**. These should be rotated and moved to SSM Parameter Store / EB env
167
- properties. **Flag this if you touch config or deploy.**
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-14
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-06-15
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 | > **⚠ DEPLOYING beta→production, NOT YET PROD-VERIFIED (as of 2026-07-23).** TRUE-79533 is > beta-verified (PM Paulina tested the WH purchase flow on beta) and | _underscore/Model/Rate/Entitlement.php, _underscore/Model/Client/Entitlement.php, _underscore/Model/Client/Address.php, dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql, dbchanges2/Core/2026-07-22a - EntitlementServiceAddressIdField.sql, dbchanges2/Client_Rate/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql, test/@Mark/Rate/verify_wholehome_per_address_guard.php |
11
+ | [Rate Whole Home Warranty Per-Address Purchase Guard](features/whole-home-warranty-purchase-guard.md) | 2.0 | > **STATUS (2026-07-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-23
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-07 (TRUE-79533)** the same `postPost` also persists the validated **service
40
- address** (`Entitlements.c_serviceAddressId`, `Addresses.isValidated=1`) — that persistence is
41
- wrapped so it never blocks this AIG-contract flow or the confirmation email. For Whole Home
42
- Warranty purchases, a new `prePost` guard now also **carrier-normalizes the address onto the
43
- payload before save**, so the AIG contract is created with the canonical address. See the
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-23
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: draft
9
- updated: 2026-07-23
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
- > **⚠ DEPLOYING beta→production, NOT YET PROD-VERIFIED (as of 2026-07-23).** TRUE-79533 is
26
- > beta-verified (PM Paulina tested the WH purchase flow on beta) and is now being promoted to
27
- > `_production`. **The service-address field was redesigned since the beta build:** it is no
28
- > longer a Rate-custom `c_serviceAddressId` field — it is now a **standard** `serviceAddressId`
29
- > FK on the **base** `_Model_Client_Entitlement` (see *How it works* and *Migration* below). Prod
30
- > launches clean (no pre-existing WH entitlements), so **no backfill ships**. Treat everything
31
- > below as forward-looking until the production purchase flow is verified. See the
32
- > **shared-interceptor merge hazard** gotcha and the **dangling-pin** / **unguarded prod
33
- > interceptor** gotchas before merging to `_production`.
34
- >
35
- > **🚨 LAUNCH BLOCKER — RESOLVED (migration built 2026-07-23, mhammontree; found 2026-07-23,
36
- > tcox) — the standard `serviceAddressId` field had NO READ ACL grant.** The custom→standard
37
- > redesign shipped the column + `Core.RecordFields` row but **not** the `AclFieldPermissions`
38
- > read grant, so `GET /v2/entitlements?fields=serviceAddressId` returned **403 `EZ-2`**. The fix
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
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: session
3
3
  slug: true-79533-pin-depth
4
- title: TRUE-79533 WH service-address pin blocked by depth=-1 starving postPost
4
+ title: TRUE-79533 WH service-address pin — SHIPPED & verified on beta (was blocked by depth=-1)
5
5
  author: mhammontree
6
6
  repos: [_underscore, dbchanges2, toga2-view]
7
7
  framework: "2.0"
@@ -14,60 +14,61 @@ updated: 2026-07-27
14
14
  # Session: true-79533-pin-depth
15
15
  **Date:** 2026-07-27
16
16
  **Project/Repo:** _underscore / dbchanges2 / toga2-view (2.0, client Rate)
17
- **Task:** Get the validated Whole Home Warranty service address to pin (`Entitlements.serviceAddressId`) and render on beta. Backend is done and correct; the blocker was the beta purchase POST sending `depth=-1`, which starved the `postPost` interceptor of the just-created address id. **FIX CHOSEN: front-end drops `depth`; deploying to beta now — awaiting verification.**
17
+ **Task:** Pin the validated Whole Home Warranty service address (`Entitlements.serviceAddressId`) and render it on beta. **DONE + verified on beta 2026-07-27.** The blocker was the purchase POST sending `depth=-1`, which starved the `postPost` interceptor of the created address id; the front-end dropped `depth` and it now works. Remaining = deploy/merge close-out + knowledge captured.
18
18
 
19
19
  ---
20
20
 
21
21
  ## What WORKED
22
22
  <!-- Include specific file paths and evidence -->
23
- - **Backend pin implemented** in `_underscore/Model/Rate/Entitlement.php` `postPost` → `persistWarrantyServiceAddress`: reads the created address id from the in-memory hydrated payload (`$payload->contact->primaryContactAddress->address->id`), then `UPDATE Addresses SET isValidated=1` + `UPDATE Entitlements SET serviceAddressId=… WHERE uuid=…`. Committed `dff1fb5e`. **Proven working AND already on the beta backend**: ET100057 — a purchase whose request had **no `depth` param** (query string `transactionId=…`, 64,650-byte deep response) — pinned `serviceAddressId=56`, address 56 (STE 400) `isValidated=1`, on the same beta instance that fails for `depth=-1` calls. This is the exact config the FE fix reproduces.
24
- - **`serviceAddressId` is a STANDARD field** on base `_underscore/Model/Client/Entitlement.php:20` (`FIELD_FOREIGNKEY` → `_Model_Client_Address`), inherited by the Rate model.
25
- - **Display EZ-2 fixed**: `dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql` grants READ on `Addresses.id` for roles 1/2/3. Committed `ea1e0fb`. Applied on beta manually and verified.
26
- - **Hard-blocks return HTTP 400** via `_Exception_Validation` (committed `41a5dc1a`) — mapped by `api2/Controller/Index.php:204-222`. (NOT yet on beta — see blockers.)
27
- - **Dedup** `hasActiveWarrantyAtAddress` forces WRITE host + disables query cache; `COALESCE(serviceAddressId, ContactAddresses.addressId)` fallback.
28
- - **Root cause proven via `Logs_Rate.Api`** (MCP env `dev-sandbox`): local-FE and beta-FE purchases hit the SAME beta instance (`i-0ce5624547cfecc93`). Only difference = request query string depth: no-depth → 64 KB deep → **pins**; `depth=-1` → 644 B shallow (`meta.calcDepth: 1`, no `address` node) → **starved**.
29
- - **`-1` source = front-end**: `toga2-view/src/pages/CheckOut/api/checkoutApi.ts:21` (`createService` passed `{ 'depth': -1 }`). Confirmed in the logged query string.
30
- - **Interceptor config (Rohan-confirmed):** `ApiPayloadInterceptors` `minDepth` defaults to **3** when null; entitlements `postPost` (id 2) is explicitly **5**; `prePost` (id 4) & sales-orders (id 3) are null→3. Omitting the request `depth` yields a full/deep build that clears the 5 floor and includes the `address` node.
23
+ - **END-TO-END VERIFIED ON BETA (2026-07-27):** after Tanner removed `depth` from the FE purchase call, both post-fix purchases pinned on their own, no backfill — ET100068 (`serviceAddressId=67` → 233 S WACKER DR, `isValidated=1`) and ET100067 (`serviceAddressId=66` → 1864 HIGH GROVE LN STE 1900, `isValidated=1`). Everything before the FE deploy is still null (clean cutoff). Paulina is PM-testing beta.
24
+ - **FE fix = drop the `depth` param** at `toga2-view/src/pages/CheckOut/api/checkoutApi.ts` (`createService` no longer sends `{depth:-1}`). Deployed to beta. This reproduces the proven-good no-depth config (ET100057 had pinned the same way pre-fix).
25
+ - **Backend pin** in `_underscore/Model/Rate/Entitlement.php` `postPost`→`persistWarrantyServiceAddress`: reads the created address id from the in-memory hydrated payload (`$payload->contact->primaryContactAddress->address->id`), sets `Addresses.isValidated=1` + `Entitlements.serviceAddressId`. Committed `dff1fb5e`. Already live on beta (that's why ET100057/67/68 pinned).
26
+ - **`serviceAddressId` = STANDARD FK field** on base `_underscore/Model/Client/Entitlement.php:20`.
27
+ - **Display EZ-2 fixed**: `dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql` grants read on `Addresses.id` (roles 1/2/3). Committed `ea1e0fb`; applied+verified on beta.
28
+ - **Hard-blocks → HTTP 400** via `_Exception_Validation` (committed `41a5dc1a`), mapped by `api2/Controller/Index.php:204-222`. (On `TRUE-79533`, NOT yet on beta — see blockers.)
29
+ - **Root cause proven via `Logs_Rate.Api`**: local-FE and beta-FE calls hit the SAME beta instance (`i-0ce5624547cfecc93`); only the request `depth` differed (no-depth → 64 KB deep → pins; `depth=-1` → 644 B shallow, `calcDepth 1`, no `address` node → starved). The `-1` came from the FE (`checkoutApi.ts`), not the interceptor.
30
+ - **Interceptor config (Rohan-confirmed):** `ApiPayloadInterceptors.minDepth` defaults to 3 (null); entitlements `postPost` = 5; `prePost`/sales-orders = null→3.
31
+ - **Knowledge CAPTURED + PUSHED (2026-07-27):** 9 docs — Rate WH guard (draft→active), service-card Addresses.id ACL, aig-contract standard-field correction, api2 EZ-2 (`id` case), and 5 elevated standards/architecture (depth-starvation + `_Exception_Validation` in `2.0/standards/backend-php.md`; uncommitted-nested-write in `_underscore/architecture.md`; `_sandbox-dev` branch map + `Logs.Api` in the api2 deploy workflow; committed-PAT security note in `api2/architecture.md`).
31
32
 
32
33
  ## What did NOT work — DO NOT RETRY THESE
33
34
  <!-- Exact failure reasons — do not vague-ify -->
34
- - **Forcing the write host inside `persistWarrantyServiceAddress`** (commit `7b63955d`, removed in `dff1fb5e`): NO EFFECT. V2 already disables the read host for the whole create block (`V2.php:5024`) — the lookup was already on the writer.
35
- - **The 3-table JOIN lookup** (`Entitlement → Contacts.primaryContactAddressId → ContactAddresses → addressId`) in postPost: returns 0 rows *in-request* even on the writer; resolves only *post-commit*. Superseded by the payload approach.
36
- - **Payload-based `$address->id` on beta with `depth=-1`**: null — the shallow payload (`calcDepth 1`) has NO `contact.primaryContactAddress.address` node at all, so there's nothing to read. Code is correct; depth is the cause.
37
- - **Merging `_underscore` to `_beta` did NOT deploy to beta.** API-Sandbox-Dev EB pulls `_underscore` from **`_sandbox-dev`** via `api2/.platform/hooks/prebuild/git.sh` + `.ebextensions/git.sandbox-dev.json`.
38
- - **"Removed depth" curl test (earlier)**: the edited curl STILL had `depth=-1` (per logged `queryString`) — proved nothing.
39
- - **Single-host vs multi-host connection theory**: DISPROVEN — same beta instance both ways; the variable is the request `depth`, not topology.
35
+ - **Forcing the write host inside `persistWarrantyServiceAddress`** (commit `7b63955d`, removed in `dff1fb5e`): NO EFFECT — V2 already disables the read host for the whole create block (`V2.php:5024`).
36
+ - **The 3-table JOIN lookup** (`Entitlement → Contacts.primaryContactAddressId → ContactAddresses → addressId`) in postPost: returns 0 rows *in-request* even on the writer (resolves only post-commit). Superseded by the payload approach.
37
+ - **Payload-based `$address->id` while the FE sent `depth=-1`**: null — the shallow payload (`calcDepth 1`) has NO `contact.primaryContactAddress.address` node. Code was correct; depth was the cause.
38
+ - **Merging `_underscore` to `_beta`**: does NOT deploy to beta — API-Sandbox-Dev pulls `_underscore` from **`_sandbox-dev`** via `api2/.platform/hooks/prebuild/git.sh` + `.ebextensions/git.sandbox-dev.json`.
39
+ - **"Removed depth" curl test (earlier)**: the edited curl STILL had `depth=-1` in the logged query string — proved nothing.
40
+ - **Single-host vs multi-host connection theory**: DISPROVEN — same beta instance both ways; the variable was request `depth`.
41
+ - **Relying on `minDepth` to protect the interceptor**: it's set to 5 on the postPost row yet the payload still built at `calcDepth 1` for a `depth=-1` request — the clamp is a latent no-op (framework bug to ticket). The real fix was the FE dropping `depth`.
40
42
 
41
43
  ## Not tried yet (candidates for next session)
42
- - **PRIMARY (in flight):** Tanner removed `depth` from `checkoutApi.ts:21` on the front-end; the `toga2-view` change is **deploying to beta now**. Verify a fresh beta purchase pins (see Exact next step). This reproduces the proven-good ET100057 config.
43
- - **FALLBACK if the FE fix somehow doesn't take:** on beta `Client_Rate`, `UPDATE ApiPayloadInterceptors SET minDepth = 10 WHERE id = 2;`, fire a purchase, check whether `calcDepth` rises and it pins. (Open Q: does the `minDepth` clamp engage for the `-1` sentinel? It's 5 yet built at `calcDepth 1`.)
44
- - Merge `_underscore` `TRUE-79533` → `_sandbox-dev` + deploy (ships the `_Exception_Validation` 400 fix + full backend; pin itself is already on beta).
45
- - Merge `dbchanges2` `TRUE-79533` for prod parity (beta already has the migrations).
46
- - Run `/capture`.
47
- - File side tickets: (1) framework `minDepth`-clamp no-op for low/`-1` depth (starves ALL payload interceptors); (2) AIG contract block has the SAME `depth` starvation — silently skipped for beta-FE purchases; (3) SECURITY: plaintext GitHub PAT committed in `api2/.ebextensions/git.*.json` — rotate + move to env config.
44
+ - **Merge `_underscore` `TRUE-79533` → `_sandbox-dev` + deploy** — puts the `_Exception_Validation` 400-fix (`41a5dc1a`) on beta so Paulina's *blocked-purchase* testing (duplicate / unverifiable address) shows a clean 400 toaster instead of a 500 + stack trace. Do this before she hits those paths.
45
+ - **Merge `dbchanges2` `TRUE-79533`** to its deploy branch for prod parity (beta already has the migrations applied).
46
+ - **File the framework ticket:** `ApiPayloadInterceptors.minDepth` clamp does not deepen the built payload for a `depth=-1` request (WO-1 says "raised to 5" but `calcDepth` stays 1) — starves ANY payload interceptor. Plus: AIG contract block had the same starvation; and the committed GitHub PAT in `api2/.ebextensions/git.*.json` needs rotating + moving to SSM.
47
+ - Optionally credit Tanner as an `owners` on the FE-touching Rate doc once his kb-username is known.
48
48
 
49
49
  ## Current file state
50
50
  | File | Status | Notes |
51
51
  |------|--------|-------|
52
- | `_underscore/Model/Rate/Entitlement.php` | Committed on `TRUE-79533` (HEAD `41a5dc1a`) | payload pin (`dff1fb5e`) + dedup write-host + `_Exception_Validation` (`41a5dc1a`). Pin already on beta; the 400-fix is NOT (needs `_sandbox-dev` merge). |
52
+ | `toga2-view/src/pages/CheckOut/api/checkoutApi.ts` | **FIXED + deployed to beta (Tanner)** | dropped `{depth:-1}` → deep payload reaches `postPost`. THE unblocker; pin now works. |
53
+ | `_underscore/Model/Rate/Entitlement.php` | Committed on `TRUE-79533` (HEAD `41a5dc1a`) | payload pin (`dff1fb5e`, already on beta) + dedup write-host + `_Exception_Validation` (`41a5dc1a`, NOT yet on beta). |
53
54
  | `_underscore/Model/Client/Entitlement.php` | Committed | `serviceAddressId` standard FK (line 20). |
54
- | `dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql` | Committed (`ea1e0fb`) | `Addresses.id` read ACL. Applied on beta. |
55
- | `dbchanges2` serviceAddressId col + `Core` field + ACL migrations | Committed on `TRUE-79533` | Applied on beta manually; branch needs merge for prod. |
56
- | `toga2-view/src/pages/CheckOut/api/checkoutApi.ts` | **Tanner removed `depth` — DEPLOYING to beta now** | Line 21 `{ 'depth': -1 }` dropped so `postPost` gets a deep payload. THE unblocker. |
57
- | `test/@Mark/Rate/verify_wholehome_per_address_guard.php` | Updated (personal, not in a repo) | `c_serviceAddressId` → `serviceAddressId`; documents the depth caveat. |
55
+ | `dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql` | Committed (`ea1e0fb`) | Applied on beta. Needs branch merge for prod. |
56
+ | `dbchanges2` serviceAddressId col + `Core` field + ACL | Committed on `TRUE-79533` | Applied on beta manually; needs merge for prod. |
57
+ | team KB (`~/toga-tech/knowledge/…`) | **9 docs published to `_main` (capture)** | Rate WH guard active + 5 elevated standards/arch docs. |
58
+ | `test/@Mark/Rate/verify_wholehome_per_address_guard.php` | Updated (personal, not in a repo) | Not team knowledge. |
58
59
 
59
60
  ## Decisions made
60
- - **Fix depth on the FRONT-END (drop the `depth` param)** rather than bumping the interceptor `minDepth`. Rationale: it reproduces the already-proven ET100057 (no-depth) config that pinned on beta, needs no DB config change, and un-starves the AIG block for free. The `minDepth` bump remains the fallback / the proper systemic guardrail. (Rohan confirmed minDepth 3-default / 5 on postPost.)
61
- - **Pin from the in-memory hydrated payload, not a DB read-back** (in-request reads can't see uncommitted nested writes; `getFullModelData` hydrates the payload before `postPost`).
61
+ - **Fix depth on the FRONT-END (drop the `depth` param)** rather than bumping the interceptor `minDepth`. Rationale: reproduces the proven ET100057 no-depth config, no DB config change, and un-starves the AIG block too. Verified working on beta. `minDepth` bump remains the fallback / the systemic guardrail (whose clamp bug is now a separate ticket).
62
+ - **Pin from the in-memory hydrated payload, not a DB read-back** (in-request reads can't see the txn's own uncommitted nested writes).
62
63
  - **`serviceAddressId` = STANDARD field** (per Jeff), not `c_serviceAddressId`.
63
- - **Hard-blocks throw `_Exception_Validation`** → HTTP 400 + surfaced message (FE checkout expects 400).
64
+ - **Hard-blocks throw `_Exception_Validation`** → HTTP 400 + message (FE expects 400).
64
65
 
65
66
  ## Blockers
66
- - **Verification pending** — the front-end `depth` removal is mid-deploy to beta; can't confirm the pin until it lands and a fresh purchase is made.
67
- - The `_Exception_Validation` 400-fix (`41a5dc1a`) and the rest of `TRUE-79533` are on `TRUE-79533` but NOT yet on beta (beta deploys `_underscore` from `_sandbox-dev`). Does NOT block the pin test (the pin code is already on beta), but needs merging for the 400/toaster behavior + prod.
67
+ - **None blocking the pin** — it's verified working on beta.
68
+ - Soft/pending: the `_Exception_Validation` 400-fix (`41a5dc1a`) is not yet on beta (beta deploys `_underscore` from `_sandbox-dev`), so Paulina's blocked-purchase paths currently return 500 until the `_sandbox-dev` merge+deploy. Not a pin blocker.
68
69
 
69
70
  ## Exact next step
70
- > As soon as Tanner's `toga2-view` deploy lands, make ONE fresh WH purchase on beta and verify in the DB (MCP `dev-sandbox`, `Client_Rate`): the newest WH entitlement has `serviceAddressId` populated AND its `Addresses.isValidated = 1`, with NO manual backfill. Also confirm the create response no longer shows the `WO-1` depth warning (and `meta.calcDepth` is higher), and that AIG outbound calls resume in `Logs_Rate.Api`. If it pins → success; then merge `_underscore TRUE-79533` → `_sandbox-dev` (for the 400 fix), merge `dbchanges2`, and run `/capture`. If it still returns null → apply the FALLBACK (`UPDATE ApiPayloadInterceptors SET minDepth=10 WHERE id=2` on beta) and re-test.
71
+ > Merge `_underscore` `TRUE-79533` → `_sandbox-dev` and deploy API-Sandbox-Dev (prebuild hook re-clones `_sandbox-dev`), so the `_Exception_Validation` hard-block 400s reach beta before Paulina tests duplicate/invalid-address rejections. Then merge `dbchanges2` `TRUE-79533` for prod parity, and file the framework `minDepth`-clamp-no-op ticket (+ AIG-starvation + committed-PAT follow-ups). Verify a deliberate duplicate-address purchase on beta returns HTTP 400 with the toaster message (not 500).
71
72
 
72
73
  ---
73
74
  _Saved by /session-save on 2026-07-27_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.441",
3
+ "version": "1.0.443",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",