toga-ai 1.0.419 → 1.0.421
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/ai-bdr/features/web-funnel-app.md +74 -23
- package/knowledge/2.0/apps/dbchanges2/architecture.md +9 -0
- package/knowledge/2.0/apps/worker2/features/vapi-webhook-handler.md +10 -1
- package/knowledge/clients/rate/INDEX.md +1 -1
- package/knowledge/clients/rate/features/whole-home-warranty-purchase-guard.md +92 -28
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-23
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/PLAN.md
|
|
@@ -374,24 +374,33 @@ PRODUCTION credential set (`TOGA_API_BASE_URL = https://api.togahub.com/v2`).
|
|
|
374
374
|
- **Restart the dev server after creating `.env.local`** — Next reads env files only at
|
|
375
375
|
server startup. `.env.*` is gitignored in BDR.
|
|
376
376
|
|
|
377
|
-
## Deploy status (AWS Amplify —
|
|
377
|
+
## Deploy status (AWS Amplify — LIVE + verified 2026-07-23)
|
|
378
378
|
|
|
379
|
-
**The
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
**The BDR site is LIVE and verified (2026-07-23):** branch `_dev-sandbox` (Amplify default
|
|
380
|
+
domain `d65bvvjrww97g.amplifyapp.com`) serves the funnel at the custom domain
|
|
381
|
+
**https://bdr.dev.sandbox.togatech.com/** — the landing page returns `200` and
|
|
382
|
+
`GET /api/contact?hsContactId=131770626674` returns `200` with the contact JSON (server env is
|
|
383
|
+
now present at runtime). This supersedes the 2026-07-21 "first build shipping / awaiting console
|
|
384
|
+
vars" status. Go-live tracking: ClickUp **868kdf4tj "BDR - create amplify and sandbox route"**
|
|
382
385
|
(urgent, Sprint 82). (ClickUp showed Alex Peterson as assignee; the team said Jeff picked it
|
|
383
386
|
up — confirm the real owner before assuming.)
|
|
384
387
|
|
|
385
388
|
- **CRITICAL — the build-spec must WRITE the console env vars into `.env.production` at build
|
|
386
|
-
time (Amplify Next-SSR recipe)
|
|
387
|
-
`amplify.yml`
|
|
388
|
-
`cp .env.production .env.local`
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
389
|
+
time (Amplify Next-SSR recipe). RESOLVED + verified live 2026-07-23.** Root cause, now confirmed
|
|
390
|
+
with git evidence: the old `amplify.yml` was inherited from the old `info` repo (authored by
|
|
391
|
+
Rohan) and did `cp .env.production .env.local` per branch, with cases only for
|
|
392
|
+
`_production` / `_beta`. That worked in `info` **only because the info repo COMMITS its real
|
|
393
|
+
`.env` files to git** (`git ls-files` shows `.env.beta`, `.env.local`, `.env.production` tracked,
|
|
394
|
+
containing real credentials — see the security gotcha below). The BDR repo correctly gitignores
|
|
395
|
+
all `.env` files (only `.env.example` is tracked, per team git rules), so the `cp` had nothing to
|
|
396
|
+
copy **and** no branch case matched `_dev-sandbox` — every deploy shipped with **ZERO server env
|
|
397
|
+
vars**. Amplify console env vars exist only at **build time**; the Next SSR **runtime** reads
|
|
398
|
+
`.env.production` from the build artifact — so the build must materialize them. Fix (commit
|
|
399
|
+
`e17d804` on `_dev-sandbox`, `amplify.yml` build phase, before `npm run build`):
|
|
400
|
+
`env | grep -E '^(HUBSPOT_|TOGA_|NEXT_PUBLIC_|RATE_LIMIT_)' >> .env.production || true` — the
|
|
401
|
+
AWS-documented pattern for Next SSR on Amplify. It is **branch-agnostic** (no per-branch case to
|
|
402
|
+
miss), and secrets stay in the Amplify console, never in git. The same broken `cp` pattern would
|
|
403
|
+
have bitten the `_production` launch too.
|
|
395
404
|
- **Symptom chain when the env vars are missing (what "no vars" looks like end-to-end):**
|
|
396
405
|
page-prime's HubSpot fetch throws → lead ref comes back empty → the **anonymous-visit guard
|
|
397
406
|
notice shows even with a valid `?hsContactId`** → `GET /api/contact` returns
|
|
@@ -400,18 +409,35 @@ up — confirm the real owner before assuming.)
|
|
|
400
409
|
- **OPERATIONAL — env vars saved AFTER a build starts do NOT reach that build.** They are baked in
|
|
401
410
|
at build time, so after adding/changing console vars you must **"Redeploy this version"** for
|
|
402
411
|
them to take effect — a fresh save alone does nothing for an in-flight or already-built deploy.
|
|
412
|
+
(This bit us 2026-07-23: the first post-fix build ran **before** the console vars existed and
|
|
413
|
+
still `500`'d, which looked like the fix had failed — it was just the pre-vars build.)
|
|
414
|
+
- **OPERATIONAL — a buildspec saved in the console Build settings OVERRIDES the repo `amplify.yml`.**
|
|
415
|
+
If an `amplify.yml` change in the repo seems to have no effect, check **Console → Build settings**
|
|
416
|
+
for an inline buildspec shadowing it, and reconcile them before assuming the repo change is wrong.
|
|
417
|
+
- **OPERATIONAL — adding a custom domain does NOT trigger a rebuild.** Wiring
|
|
418
|
+
`bdr.dev.sandbox.togatech.com` to the app does not by itself redeploy; if the running build
|
|
419
|
+
predates a needed env/spec change, run **"Redeploy this version"** separately.
|
|
403
420
|
- **Amplify must be configured for SSR hosting (NOT static export)** — the app is server-rendered
|
|
404
421
|
(server components, route handlers, and the `proxy.ts` edge proxy all require the SSR/compute
|
|
405
422
|
runtime). Point the app at branch `_dev-sandbox`.
|
|
406
|
-
- **7 env vars go in the Amplify console** (never the values — **
|
|
407
|
-
the production credential set)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
423
|
+
- **7 env vars go in the Amplify console** (never the secret **values** — those **live in
|
|
424
|
+
`info/.env.local`**, the production credential set). Console recipe that completed the fix
|
|
425
|
+
(verified working 2026-07-23): **AWS Amplify console → BDR app → Hosting → Environment variables
|
|
426
|
+
→ Manage variables**, applied to **all branches**. The 7: the 4 secrets `HUBSPOT_ACCESS_TOKEN`,
|
|
427
|
+
`TOGA_CLIENT_ID`, `TOGA_CLIENT_API_UUID`, `TOGA_CLIENT_API_SECRET` (values only in the console +
|
|
428
|
+
`info/.env.local`, never here); plus the non-secret config `TOGA_API_BASE_URL`
|
|
429
|
+
(`https://api.togahub.com/v2`), `TOGA_WORKER_URL` (`https://worker.togahub.com/` — new vs. info;
|
|
430
|
+
enables fast dial), and `NEXT_PUBLIC_GA_MEASUREMENT_ID` (`G-0BE4RFNLY0`, a public GA4 id).
|
|
431
|
+
`TOGA_PUBLIC_TOKEN_URL` and `NEXT_PUBLIC_ENV` are dead even in `info` and are intentionally NOT
|
|
432
|
+
carried over. See the parity audit for the full accounting.
|
|
433
|
+
- **Status (2026-07-23): LIVE and verified.** After the console vars were saved and the version was
|
|
434
|
+
redeployed, the deployed page-prime works: `https://bdr.dev.sandbox.togatech.com/` returns `200`
|
|
435
|
+
and `GET /api/contact?hsContactId=131770626674` returns `200` with contact JSON. The primary-phone
|
|
436
|
+
self-heal (commit `4ac579e`) is live, and fast dial is now wired in the deployed env
|
|
437
|
+
(`TOGA_WORKER_URL` set). **Still pending (deferred to next session):** the deployed **Call Now**
|
|
438
|
+
smoke test — which is also **fast dial's first live run** end-to-end (local testing never
|
|
439
|
+
exercised `TOGA_WORKER_URL`, so the deployed Call Now is the first time the fast-dial POST fires
|
|
440
|
+
in a real deploy).
|
|
415
441
|
- **Phase 8 WAF pairing.** `BDR/PLAN.md` Phase 8 specifies a **per-IP rate rule on the CloudFront
|
|
416
442
|
distribution / WAF** that pairs with the in-app token-bucket limiter (the app limiter is
|
|
417
443
|
defense-in-depth only; the edge WAF rule is the authoritative control — see the enumeration
|
|
@@ -514,6 +540,15 @@ up — confirm the real owner before assuming.)
|
|
|
514
540
|
happen inside the route handler. See "Request-a-call is fully server-side" above. And note
|
|
515
541
|
setting `dtNextContactRequested` does not guarantee a call — the worker2 dialer eligibility
|
|
516
542
|
SQL decides (a phone-less contact is silently never dialed; see that section).
|
|
543
|
+
- **HIGH (security) — the old `info` repo COMMITS real production credentials to git.** `info`'s
|
|
544
|
+
`.env.beta` / `.env.local` / `.env.production` are **tracked** files (confirmed via `git ls-files`)
|
|
545
|
+
containing the real **HubSpot access token** and **Toga API client secret**. Those credentials are
|
|
546
|
+
in git history and must be treated as **compromised → rotate**. The AI-BDR credential-rotation
|
|
547
|
+
ticket now covers the HubSpot token + Toga API secret **in addition to** the hardcoded Vapi bearer
|
|
548
|
+
at `worker2/Worker/Ai/Bdr/Vapi.php:6` (see
|
|
549
|
+
`../../worker2/features/vapi-webhook-handler.md`). BDR itself correctly gitignores all `.env` files
|
|
550
|
+
— this is an `info`-repo problem that BDR touches only because it sources its local-dev values from
|
|
551
|
+
`info/.env.local`. Never record credential **values** in the KB — location only.
|
|
517
552
|
- **Standing rule: never add more PII fields to `ClientLeadRef`.** Phone is the one
|
|
518
553
|
deliberate exception (visitor's own number, shown back to them, never logged). Every field
|
|
519
554
|
on `ClientLeadRef` crosses to the public client and is exposed to the enumeration risk
|
|
@@ -554,6 +589,22 @@ up — confirm the real owner before assuming.)
|
|
|
554
589
|
harness; pull it with `npx toga-ai`.
|
|
555
590
|
|
|
556
591
|
## Change history
|
|
592
|
+
- 2026-07-23 — **BDR site is LIVE + verified** (day 3 of go-live). RESOLVED the deployed 500
|
|
593
|
+
"Failed to fetch contact" (missing server env at runtime): confirmed the full root cause with git
|
|
594
|
+
evidence — the inherited `amplify.yml` (from the old `info` repo, which COMMITS its real `.env`
|
|
595
|
+
files to git) did `cp .env.production .env.local` with cases only for `_production`/`_beta`, but
|
|
596
|
+
BDR correctly gitignores `.env`, so the `cp` copied nothing and `_dev-sandbox` had no case →
|
|
597
|
+
zero server env vars per deploy. Fix (commit `e17d804`, branch-agnostic): build-time
|
|
598
|
+
`env | grep -E '^(HUBSPOT_|TOGA_|NEXT_PUBLIC_|RATE_LIMIT_)' >> .env.production || true`. Recorded
|
|
599
|
+
the exact **Amplify console recipe** that completed it (Hosting → Environment variables → Manage
|
|
600
|
+
variables, all branches; the 7 vars incl. new `TOGA_WORKER_URL=https://worker.togahub.com/`;
|
|
601
|
+
`TOGA_PUBLIC_TOKEN_URL`/`NEXT_PUBLIC_ENV` intentionally dropped) and two new operational gotchas
|
|
602
|
+
(a console buildspec OVERRIDES the repo `amplify.yml`; adding a custom domain does NOT trigger a
|
|
603
|
+
rebuild). Verified live: `https://bdr.dev.sandbox.togatech.com/` = 200 and
|
|
604
|
+
`GET /api/contact?hsContactId=131770626674` = 200 contact JSON. Added a HIGH security gotcha that
|
|
605
|
+
the old `info` repo commits real HubSpot/Toga credentials to git (treat as compromised; rotation
|
|
606
|
+
ticket now covers them alongside the worker2 Vapi bearer). STILL PENDING (deferred): the deployed
|
|
607
|
+
**Call Now** smoke test = fast dial's first live run end-to-end. (tcox)
|
|
557
608
|
- 2026-07-21 — **First Amplify deployment** (app now EXISTS: branch `_dev-sandbox` →
|
|
558
609
|
`dev-sandbox.d65bvvjrww97g.amplifyapp.com`, superseding the "not deployed yet" status).
|
|
559
610
|
DISCOVERED + FIXED (`amplify.yml`, commit `e17d804`) an env-var build-spec bug: the old spec only
|
|
@@ -180,6 +180,15 @@ its own header.)
|
|
|
180
180
|
(`Core/2026-05-08 … CronJobs_WorkerCleanup_insert`) used `UUID()` — that is a known violation,
|
|
181
181
|
not a precedent to copy. First applied correctly in
|
|
182
182
|
`Core/2026-07-21b - Insert - SprintLockScheduled CronJob.sql`.
|
|
183
|
+
7. **Guard every registration/seed insert with `NOT EXISTS` (or `INSERT IGNORE`).** Rows that
|
|
184
|
+
register platform behavior — `ApiPayloadInterceptors`, `Core.RecordFields`/`CustomRecordFields`,
|
|
185
|
+
cron jobs, ACL rows — must be inserted only if not already present. An **unguarded** registration
|
|
186
|
+
is not merely non-idempotent: on a fresh environment it can run **alongside a second migration
|
|
187
|
+
that registers the same behavior**, producing a **double registration** (e.g. two interceptor
|
|
188
|
+
rows for the same record → the interceptor fires twice). A real instance: the Rate WH
|
|
189
|
+
`prePost` interceptor is registered unguarded in `Client_Rate/2026-07-15 - PreInterceptor.sql`
|
|
190
|
+
(record 191, `minDepth NULL`); a second guarded registration would have double-fired it. See
|
|
191
|
+
`clients/rate/features/whole-home-warranty-purchase-guard.md`.
|
|
183
192
|
|
|
184
193
|
## Bulk data loads — batch, and stage large sets in a temp table
|
|
185
194
|
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-23
|
|
10
10
|
owners: [snaredla, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Vapi.php
|
|
@@ -239,6 +239,10 @@ The outbound dialer `Worker/Ai/Bdr/Vapi.php` (**line 6**) declares the Vapi API
|
|
|
239
239
|
rotate it**, and move it into the `[vapi]` section of the worker2 `Config/*.ini` files alongside the
|
|
240
240
|
other Vapi secrets (the pattern the rest of the integration already uses). Config, never source, is
|
|
241
241
|
the home for this credential. (Token value is deliberately not recorded here — location only.)
|
|
242
|
+
**Scope broadened 2026-07-23:** the AI-BDR credential-rotation ticket now also covers the
|
|
243
|
+
**HubSpot access token** and **Toga API client secret** that are committed to git in the old `info`
|
|
244
|
+
repo's tracked `.env.*` files — see the security gotcha in
|
|
245
|
+
[ai-bdr web-funnel-app.md](../../ai-bdr/features/web-funnel-app.md).
|
|
242
246
|
|
|
243
247
|
## Data model
|
|
244
248
|
|
|
@@ -267,6 +271,11 @@ status. See the `ai-bdr` skill / `_Model_True_ContactAttempt` for the full field
|
|
|
267
271
|
- [VAPI Integration — assistants, tools, structured output](../../ai-bdr/features/vapi-integration.md)
|
|
268
272
|
|
|
269
273
|
## Change history
|
|
274
|
+
- 2026-07-23 — Broadened the SECURITY credential-rotation gotcha: the AI-BDR rotation ticket now
|
|
275
|
+
also covers the **HubSpot access token** and **Toga API client secret** committed to git in the
|
|
276
|
+
old `info` repo's tracked `.env.*` files (treat as compromised), alongside the hardcoded Vapi
|
|
277
|
+
bearer at `Worker/Ai/Bdr/Vapi.php:6`. Cross-linked to the ai-bdr web-funnel-app security gotcha.
|
|
278
|
+
(tcox)
|
|
270
279
|
- 2026-07-21 — Full call loop **proven end-to-end in production** on the real campaign 2
|
|
271
280
|
(`26.05 - AI BDR - Ryan Nitti`): UI submit → funnel API → dialer → Vapi call → webhook write-back.
|
|
272
281
|
Live-verified the documented behavior: the `SCHEDULE_CALLBACK` → `c_callOutcome=CALLBACK_SCHEDULED`
|
|
@@ -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/pages/Home/api/homeApi.ts, src/pages/Home/view/HomePage.tsx, src/pages/Home/viewModels/useHomePageViewModel.ts, src/pages/Services/view/ServicesPage.tsx, src/pages/Services/viewModels/useServicePageViewModel.ts |
|
|
10
10
|
| [Rate Service-Purchase Confirmation Emails (Tech / Warranty)](features/service-purchase-emails.md) | 2.0 | When a Rate customer purchases a service, a confirmation email is sent. | _underscore/Model/Rate/Entitlement.php, worker2/Worker/Notification/EmailTemplate.php, dbchanges2/Client_Rate/2026-06-30a - Rate purchase email templates.sql |
|
|
11
|
-
| [Rate Whole Home Warranty Per-Address Purchase Guard](features/whole-home-warranty-purchase-guard.md) | 2.0 | > **⚠
|
|
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, test/@Mark/Rate/verify_wholehome_per_address_guard.php |
|
|
12
12
|
| [Rate](profile.md) | 2.0 | Rate is a mortgage/lending client. | |
|
|
@@ -6,12 +6,14 @@ project: _Underscore
|
|
|
6
6
|
client: rate
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: draft
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-23
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Rate/Entitlement.php
|
|
13
|
+
- _underscore/Model/Client/Entitlement.php
|
|
13
14
|
- _underscore/Model/Client/Address.php
|
|
14
|
-
- dbchanges2/
|
|
15
|
+
- dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql
|
|
16
|
+
- dbchanges2/Core/2026-07-22a - EntitlementServiceAddressIdField.sql
|
|
15
17
|
- test/@Mark/Rate/verify_wholehome_per_address_guard.php
|
|
16
18
|
related:
|
|
17
19
|
- clients/rate/profile.md
|
|
@@ -19,14 +21,15 @@ related:
|
|
|
19
21
|
- ../../../2.0/apps/_underscore/features/address-validation.md
|
|
20
22
|
---
|
|
21
23
|
|
|
22
|
-
> **⚠
|
|
23
|
-
>
|
|
24
|
-
>
|
|
25
|
-
>
|
|
26
|
-
>
|
|
27
|
-
>
|
|
28
|
-
>
|
|
29
|
-
> **
|
|
24
|
+
> **⚠ DEPLOYING beta→production, NOT YET PROD-VERIFIED (as of 2026-07-23).** TRUE-79533 is
|
|
25
|
+
> beta-verified (PM Paulina tested the WH purchase flow on beta) and is now being promoted to
|
|
26
|
+
> `_production`. **The service-address field was redesigned since the beta build:** it is no
|
|
27
|
+
> longer a Rate-custom `c_serviceAddressId` field — it is now a **standard** `serviceAddressId`
|
|
28
|
+
> FK on the **base** `_Model_Client_Entitlement` (see *How it works* and *Migration* below). Prod
|
|
29
|
+
> launches clean (no pre-existing WH entitlements), so **no backfill ships**. Treat everything
|
|
30
|
+
> below as forward-looking until the production purchase flow is verified. See the
|
|
31
|
+
> **shared-interceptor merge hazard** gotcha and the **dangling-pin** / **unguarded prod
|
|
32
|
+
> interceptor** gotchas before merging to `_production`.
|
|
30
33
|
|
|
31
34
|
## Summary
|
|
32
35
|
|
|
@@ -50,7 +53,7 @@ consistent with `resolvePurchaseProduct`), the guard:
|
|
|
50
53
|
`hasActiveWarrantyAtAddress()`.
|
|
51
54
|
|
|
52
55
|
`postPost` then persists the validated service address after save (sets `Addresses.isValidated=1`
|
|
53
|
-
and `Entitlements.
|
|
56
|
+
and `Entitlements.serviceAddressId`), wrapped so it never blocks the existing AIG-contract /
|
|
54
57
|
confirmation-email flow.
|
|
55
58
|
|
|
56
59
|
Ticket: TRUE-79533. Business rule owner: PM Paulina.
|
|
@@ -64,9 +67,11 @@ Ticket: TRUE-79533. Business rule owner: PM Paulina.
|
|
|
64
67
|
- **Address line2 distinguishes units.** A landlord's apartment units are distinct addresses:
|
|
65
68
|
2A / 2B / 2C = three separate WHs. Uniqueness keys on the full normalized address including
|
|
66
69
|
line2.
|
|
67
|
-
- **Persist the validated address ON the entitlement** (
|
|
68
|
-
than deriving it from the fragile contact-primary-address path
|
|
69
|
-
entitlements so
|
|
70
|
+
- **Persist the validated address ON the entitlement** (via the `serviceAddressId` FK) rather
|
|
71
|
+
than deriving it from the fragile contact-primary-address path. **No backfill ships** —
|
|
72
|
+
production launches with no pre-existing WH entitlements, so there is nothing to backfill (the
|
|
73
|
+
earlier beta build carried an idempotent backfill; it was dropped with the standard-field
|
|
74
|
+
redesign).
|
|
70
75
|
|
|
71
76
|
## How it works
|
|
72
77
|
|
|
@@ -78,25 +83,54 @@ Ticket: TRUE-79533. Business rule owner: PM Paulina.
|
|
|
78
83
|
the normalized `address1/address2/city/state/zipCode` back onto the payload.
|
|
79
84
|
4. `hasActiveWarrantyAtAddress()` joins `Entitlements` → `Items` (title LIKE `%warranty%`, with
|
|
80
85
|
WH `Items.id = 3` as an OR fallback) → **active** `Subscriptions` (`isActive = 1 AND
|
|
81
|
-
dateCancelled IS NULL`) → `Addresses
|
|
82
|
-
|
|
86
|
+
dateCancelled IS NULL`) → `Addresses`. A match → block the second active WH.
|
|
87
|
+
- **The address is resolved via `COALESCE(serviceAddressId, contact's primary address)`**
|
|
88
|
+
(CodeRabbit hardening). When `serviceAddressId` is set it wins; when it is NULL the query
|
|
89
|
+
falls back to the contact's primary address (LEFT JOIN `Contacts` → `ContactAddresses`).
|
|
90
|
+
This means an **unpinned** WH — e.g. one whose `postPost` pin silently failed, leaving
|
|
91
|
+
`serviceAddressId` NULL — is **still caught** by the dedup. The fallback was chosen over
|
|
92
|
+
making the `postPost` pin throw, preserving the "never block the save" design.
|
|
83
93
|
- **The dedup read disables the query cache** (`_Database::useQueryCache(false)`, restored in
|
|
84
94
|
a `finally`) so it sees rows committed by a just-prior purchase rather than a stale cached
|
|
85
95
|
result set.
|
|
86
96
|
5. **`postPost`.** After save, persists the validated service address: sets
|
|
87
|
-
`Addresses.isValidated = 1` and writes `Entitlements.
|
|
97
|
+
`Addresses.isValidated = 1` and writes `Entitlements.serviceAddressId`. This block is
|
|
88
98
|
wrapped so a failure never interrupts the AIG-contract creation or the confirmation email
|
|
89
99
|
(see the [AIG contract creation doc](aig-contract-creation.md) — the same `postPost`).
|
|
90
100
|
6. All SQL uses `_Database::escape()` / int-casts — `_Query` has **no bind API** in this path.
|
|
91
101
|
|
|
92
|
-
##
|
|
102
|
+
## Field & model: `serviceAddressId` is now a STANDARD field (reviewer Jeff)
|
|
93
103
|
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
The service-address field was **promoted from a Rate-custom field to a platform standard field**
|
|
105
|
+
during backend review (Jeff). This is the single biggest change since the beta build:
|
|
106
|
+
|
|
107
|
+
- **Model.** `serviceAddressId` is declared as an **FK on the BASE `_Model_Client_Entitlement`**
|
|
108
|
+
(`_underscore/Model/Client/Entitlement.php`) — `[FIELD_FOREIGNKEY, FIELDOPT_FOREIGNKEY_MODEL =>
|
|
109
|
+
'\_Model_Client_Address']` — not on `_Model_Rate_Entitlement`. The old `c_serviceAddressId`
|
|
110
|
+
declaration was removed from the Rate subclass. Because api2 resolves the related Addresses
|
|
111
|
+
model from the FK declaration, declaring it as a proper FK (vs. a plain integer) is what lets
|
|
112
|
+
the frontend/API traverse to the address.
|
|
113
|
+
- **Core RecordField (standard, not custom).** Registered in **`Core.RecordFields`** on the
|
|
114
|
+
Entitlements record (**id 191**): type **NUMBER**, `isIdentifier 0`, `childPolicy MATCH`,
|
|
115
|
+
`precision 0` — mirroring sibling FKs `saleItemId` / `vendorId`. Note `Core.RecordFields` has
|
|
116
|
+
**no `foreignRecordId` column**; FK resolution is model-side (the model declaration above),
|
|
117
|
+
not in the RecordField row.
|
|
118
|
+
|
|
119
|
+
## Migrations (standard-field fan-out — replaces the deleted Rate-only migration)
|
|
120
|
+
|
|
121
|
+
- **`dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql`** — a **`Client/` fan-out**
|
|
122
|
+
(every tenant's `Client_<Tenant>.Entitlements`) adding `serviceAddressId INT UNSIGNED` with
|
|
123
|
+
index `idx_Entitlements_serviceAddressId`, positioned `AFTER subscriptionId`. Because the field
|
|
124
|
+
lives on the **base** model, **every** client's Entitlements table must have the column, so it
|
|
125
|
+
is a `Client/` fan-out, not a `Client_Rate/` change.
|
|
126
|
+
- **`dbchanges2/Core/2026-07-22a - EntitlementServiceAddressIdField.sql`** — registers the
|
|
127
|
+
`Core.RecordFields` row for `serviceAddressId` on record 191, guarded (`NOT EXISTS`).
|
|
128
|
+
- **Deploy order matters:** the column migration must reach **all tenants before** the
|
|
129
|
+
`_underscore` code that references `serviceAddressId` — otherwise every client breaks.
|
|
130
|
+
- **`dbchanges2/Client_Rate/2026-07-07a - WholeHomeWarrantyPerAddressGuard.sql` is DELETED.** Its
|
|
131
|
+
column + `CustomRecordFields` moved to the standard field above; its `ApiPayloadInterceptors`
|
|
132
|
+
registration was redundant (see the **unguarded prod interceptor** gotcha) and its backfill was
|
|
133
|
+
dropped (prod launches clean).
|
|
100
134
|
|
|
101
135
|
## Rate data model (prod-verified 2026-07-07)
|
|
102
136
|
|
|
@@ -105,9 +139,10 @@ Ticket: TRUE-79533. Business rule owner: PM Paulina.
|
|
|
105
139
|
- **Active-subscription signal is on `Subscriptions`** (`isActive` tinyint default 1,
|
|
106
140
|
`dateCancelled`, `dateEnd`, `dtRequestToCancel`). `Entitlements` has **no `isActive`**.
|
|
107
141
|
- **Entitlements had no address linkage before this ticket.** `SalesOrders` has
|
|
108
|
-
`shipToAddressId`/`billToAddressId` but they are NULL for Rate. Before `
|
|
142
|
+
`shipToAddressId`/`billToAddressId` but they are NULL for Rate. Before `serviceAddressId`,
|
|
109
143
|
the address was reachable only via `Entitlement.contactId` → `Contacts.primaryContactAddressId`
|
|
110
|
-
→ `ContactAddresses.addressId` → `Addresses
|
|
144
|
+
→ `ContactAddresses.addressId` → `Addresses` (this is exactly the COALESCE fallback path the
|
|
145
|
+
dedup still uses when `serviceAddressId` is NULL).
|
|
111
146
|
- **Borrower identity** = `Customers.c_borrowerId`.
|
|
112
147
|
- **Core.Records record IDs:** Addresses = 13, Entitlements = 191, Sales orders = 14,
|
|
113
148
|
Entitlement tickets = 212.
|
|
@@ -186,13 +221,42 @@ live carrier waterfall is opt-in via `RUN_LIVE=1` (defaults off). Verified **18/
|
|
|
186
221
|
- **`ApiPayloadInterceptors` (Client_Rate) has no `phpMethod` column.** The interceptor method
|
|
187
222
|
is resolved **by convention** from `(prePostProcessing, httpMethod)`: `PRE+POST → prePost`,
|
|
188
223
|
`POST+POST → postPost` (matches the framework `[pre|post][HttpMethod]` convention).
|
|
189
|
-
-
|
|
190
|
-
|
|
224
|
+
- **Dedup is defeated by a DANGLING pin (not just a NULL one).** The COALESCE fallback covers a
|
|
225
|
+
**NULL** `serviceAddressId`, but if `serviceAddressId` points at a **deleted `Addresses` row**,
|
|
226
|
+
the join drops that entitlement and it becomes **invisible to the guard** — a duplicate WH slips
|
|
227
|
+
through. On **beta this actually happened** through test-data churn: one reused test contact
|
|
228
|
+
(`contactId 8`) with addresses created/deleted per purchase left dangling pins, so **manual
|
|
229
|
+
repeat-purchase testing on beta is unreliable**. Test the dedup with a **fresh contact** or the
|
|
230
|
+
deterministic `verify_wholehome_per_address_guard.php` script. Prod launches clean (no
|
|
231
|
+
pre-existing WH entitlements), so no dangling-pin backfill is needed.
|
|
232
|
+
- **The WH `prePost` interceptor is ALREADY registered in production — via an UNGUARDED insert.**
|
|
233
|
+
`dbchanges2/Client_Rate/2026-07-15 - PreInterceptor.sql` (author rgirish97) registers the
|
|
234
|
+
record-191 PRE/POST interceptor with `minDepth NULL` and **no `NOT EXISTS` guard**. This is why
|
|
235
|
+
the deleted `2026-07-07a` interceptor registration was redundant — and why keeping it would have
|
|
236
|
+
been a latent hazard: a fresh environment running both would **double-register**, firing
|
|
237
|
+
`prePost` twice. **Recommend adding a `NOT EXISTS` guard to the `2026-07-15` file.**
|
|
238
|
+
- **`Core.RecordFields` has no `foreignRecordId` column** — FK resolution for `serviceAddressId`
|
|
239
|
+
is entirely **model-side** (the `_Model_Client_Entitlement` FK declaration). The RecordField row
|
|
240
|
+
only carries type/identifier/childPolicy/precision, mirroring sibling FKs `saleItemId`/`vendorId`.
|
|
191
241
|
- **No `_Query` bind API here** — use `_Database::escape()` / int-casts for all interpolated
|
|
192
242
|
values.
|
|
193
243
|
|
|
194
244
|
## Change history
|
|
195
245
|
|
|
246
|
+
- 2026-07-23 — **Service-address field promoted CUSTOM → STANDARD** (TRUE-79533, reviewer Jeff)
|
|
247
|
+
and CodeRabbit hardenings, deploying beta→prod. `c_serviceAddressId` on `_Model_Rate_Entitlement`
|
|
248
|
+
is gone; `serviceAddressId` is now an FK on the **base** `_Model_Client_Entitlement`, registered
|
|
249
|
+
as a standard `Core.RecordFields` row on record 191 (mirrors `saleItemId`/`vendorId`; no
|
|
250
|
+
`foreignRecordId` — FK resolution is model-side). Migrations changed to a `Client/` fan-out
|
|
251
|
+
(`2026-07-22a` adds the column to every tenant) + `Core/2026-07-22a` (RecordField); the Rate-only
|
|
252
|
+
`Client_Rate/2026-07-07a` was **deleted** (column/field moved to standard, its interceptor
|
|
253
|
+
registration redundant, backfill dropped — prod launches clean). Guard hardened: `serviceAddressId`
|
|
254
|
+
declared a proper FK so api2 resolves Addresses; `hasActiveWarrantyAtAddress()` now resolves via
|
|
255
|
+
`COALESCE(serviceAddressId, contact primary address)` so an unpinned WH is still deduped. Recorded
|
|
256
|
+
the **dangling-pin** gotcha (deleted-address pin makes an entitlement invisible to the dedup;
|
|
257
|
+
beta test-data churn on `contactId 8`) and the **unguarded prod interceptor** gotcha
|
|
258
|
+
(`Client_Rate/2026-07-15 - PreInterceptor.sql` registers record-191 PRE/POST with no `NOT EXISTS`
|
|
259
|
+
→ double-register risk; add a guard). (mhammontree)
|
|
196
260
|
- 2026-07-17 — **Beta-verified** (TRUE-79533): corrected code merged to `_beta` (`_underscore` +
|
|
197
261
|
`dbchanges2`), `2026-07-07a` migration applied to the beta `Client_Rate` DB, beta redeployed, and
|
|
198
262
|
**PM Paulina successfully tested the WH purchase flow on beta.** Recorded the **beta unblock
|
package/package.json
CHANGED