toga-ai 1.0.468 → 1.0.470

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: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-28
9
+ updated: 2026-07-29
10
10
  owners: ["mhammontree"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -66,8 +66,39 @@ fact AIG **does** post live entitlements — the writes were **failing (HTTP 500
66
66
  core `Logs`**, not `Logs_Aig`. See
67
67
  [AIG entitlement intake → Intake is a live API feed](../../../../clients/aig/features/entitlement-intake.md).
68
68
 
69
+ ## Recovering an intake backlog from logged payloads
70
+
71
+ Because the core/writer `Logs.Api` stores **every** inbound request — including failed writes —
72
+ each row retains the **complete original `requestPayload`** (full contact / device / pricing /
73
+ coverage). That makes the log a **replayable source of truth** for a client's dropped-intake
74
+ backlog and a diagnostic for *which* upstream programs actually attempted intake:
75
+
76
+ - **Rebuild the backlog** by replaying the stored `requestPayload`s, **deduped to one row per
77
+ contract** via a MySQL 8 window function keyed on
78
+ `JSON_UNQUOTE(JSON_EXTRACT(requestPayload,'$.number'))` (many failed attempts exist per contract
79
+ when a feed re-sends — see below).
80
+ - **Determine which programs/vendors reached the API** with a split on
81
+ `JSON_UNQUOTE(JSON_EXTRACT(requestPayload,'$.vendor.number'))` — a vendor present in the failed
82
+ writes attempted intake; a vendor entirely absent never routed to api2 at all.
83
+
84
+ This is both a **fallback recovery path** and a **scope diagnostic**. In the TRUE-80179 AIG
85
+ incident it answered the scope question — the failed writes contained only vendor `STS_001` and
86
+ **zero `SA_001`**, proving `SA_001` never reached api2 — while the *replay* itself was ultimately
87
+ **not needed**, because AIG's scheduled feed re-sent the outstanding contracts and self-healed the
88
+ backlog once intake succeeded. Keep the replay technique as the fallback for feeds that do **not**
89
+ re-send.
90
+
91
+ > **Never copy a logged row's values into a doc or a replay artifact you commit** — these rows
92
+ > carry live Bearer tokens (in `requestHeaders`) and customer PII. Reference the row by location;
93
+ > replay operationally, not by pasting payloads into the KB.
94
+
69
95
  ## Change history
70
96
 
97
+ - 2026-07-29 — Documented (TRUE-80179) that the core/writer `Logs.Api` retains the **full original
98
+ `requestPayload`** on failed writes, making it a **replayable backlog source** and a scope
99
+ diagnostic: dedupe to one-per-contract with a window function on
100
+ `JSON_EXTRACT(requestPayload,'$.number')`, and split on `$.vendor.number` to see which programs
101
+ actually attempted intake. (mhammontree)
71
102
  - 2026-07-28 — Refined (TRUE-80179): a **failed write (non-2xx `POST`/`PUT`) lands in the core/writer
72
103
  `Logs`** on `api-writer.togahub.com` **even when the request is fully client-scoped** — not just
73
104
  pre-scope auth failures. `Logs_<Client>.Api` therefore shows reads + successful writes; failed
@@ -3,6 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [TOGa View Frontend (toga2-view) Architecture](architecture.md) | `toga2-view` is the **React/TypeScript single-page frontend** for the TOGa 2.0 platform — the customer-facing web app (home warranty / tech-support portals). | toga2-view/src/main.tsx, toga2-view/src/App.tsx, toga2-view/src/routes.tsx, toga2-view/src/api/axiosInstance.ts, toga2-view/src/api/apiFunctions.ts, toga2-view/src/utils/queryHelpers.ts, toga2-view/src/contexts/AuthContext.tsx, toga2-view/src/contexts/useUserStore.ts, toga2-view/src/hooks/useAuthenticationFlow.ts, toga2-view/vite.config.ts, toga2-view/package.json |
6
+ | [Get Support — Subscription Details & Cancel Subscription Gating](features/get-support-cancel-subscription.md) | The Get Support page (`/get-support?itemUuid=…`) shows a **subscription details card** (plan · price · renewal date) for the service the user clicked, and — **o | toga2-view/src/pages/GetSupport/viewModels/getSupportPageViewModel.ts, toga2-view/src/pages/GetSupport/view/GetSupportPage.tsx, toga2-view/src/pages/GetSupport/viewModels/DUMMYFIELDS/SUPPORTDUMMYFIELDS.json, toga2-view/src/constants/featureFlags.ts, toga2-view/src/hooks/useBundleServices.ts |
6
7
  | [Mobile Nav Header](features/mobile-nav-header.md) | The `MobileNavToggle` component renders the fixed 72 px header (hamburger + Rate logo) and the slide-in drawer nav. | toga2-view/src/components/MobileNav/MobileNav.tsx, toga2-view/public/assets/Rate_Logo.svg |
7
8
  | [/v2 Query-String Builder (assembleOptions / where coercion)](features/query-string-builder.md) | `src/utils/queryHelpers.ts` converts a structured JS options object (`fields`, `where`, `join`/`ojoin`, `sort`, …) into the `api2` `/v2` query string. | toga2-view/src/utils/queryHelpers.ts, toga2-view/src/utils/queryHelpers.test.ts |
8
9
  | [Service Card Component](features/service-card.md) | The `ServiceCard` component renders a single service subscription (tech support or home warranty) on both the Home and Services pages. | toga2-view/src/components/ServiceCard/ServiceCard.tsx, toga2-view/src/pages/Services/view/ServicesPage.tsx, toga2-view/src/pages/Home/view/HomePage.tsx, toga2-view/src/constants/bundleConstants.ts, toga2-view/src/pages/Services/viewModels/DUMMYFIELDS/SERVICESDUMMYFIELDS.json |
@@ -0,0 +1,145 @@
1
+ ---
2
+ title: "Get Support — Subscription Details & Cancel Subscription Gating"
3
+ framework: "2.0"
4
+ repo: toga2-view
5
+ project: TOGa View Frontend
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-28
10
+ owners: ["tcox"]
11
+ files:
12
+ - toga2-view/src/pages/GetSupport/viewModels/getSupportPageViewModel.ts
13
+ - toga2-view/src/pages/GetSupport/view/GetSupportPage.tsx
14
+ - toga2-view/src/pages/GetSupport/viewModels/DUMMYFIELDS/SUPPORTDUMMYFIELDS.json
15
+ - toga2-view/src/constants/featureFlags.ts
16
+ - toga2-view/src/hooks/useBundleServices.ts
17
+ related:
18
+ - ../architecture.md
19
+ - ../../../../clients/rate/features/service-card-entitlements.md
20
+ - ../../../../clients/rate/profile.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ The Get Support page (`/get-support?itemUuid=…`) shows a **subscription details card**
26
+ (plan · price · renewal date) for the service the user clicked, and — **only for tech-support
27
+ services** — a **Cancel Subscription** button plus a confirmation modal. Whole Home (WH) warranty
28
+ services and unclassified services get the subscription details card **without** any cancel path.
29
+
30
+ Two things about this flow are easy to get wrong and are the reason it is documented:
31
+
32
+ - The cancel affordance exists **only on this page** — `ServiceCard` has none.
33
+ - **There is no backend cancellation endpoint yet.** The viewModel's `cancelSubscription()` is a
34
+ stub that always rejects, so confirming surfaces an error instead of faking success.
35
+
36
+ ## Key files / entry points
37
+
38
+ - `pages/GetSupport/viewModels/getSupportPageViewModel.ts` — resolves `activeService`, builds the
39
+ `subscription` object, exposes `canCancelSubscription` and the `cancelSubscription()` stub
40
+ - `pages/GetSupport/view/GetSupportPage.tsx` — the **only** place a cancel affordance is rendered;
41
+ computes `showCancelUi` and owns the confirm-modal state (`showCancelModal`, `cancelError`)
42
+ - `constants/featureFlags.ts` — `CANCEL_SUBSCRIPTION_ENABLED` (currently `true`); flipping it to
43
+ `false` hides the whole cancel path in one place
44
+ - `viewModels/DUMMYFIELDS/SUPPORTDUMMYFIELDS.json` — copy: `renewsPrefix`, `cancelNote`
45
+ ("You can cancel anytime. Coverage continues until the end of your current period."),
46
+ `confirm.*` (including `confirm.errorMessage`)
47
+ - `hooks/useBundleServices.ts` — `detectServiceType()`, the classification the gate reuses
48
+
49
+ ## How it works
50
+
51
+ 1. **`activeService` resolution.** The viewModel picks the bundle service whose `uuid` matches the
52
+ URL's `itemUuid`, falls back to the first service with `isActive`, else `null`:
53
+ ```ts
54
+ const activeService =
55
+ bundleServices?.find((service) => service.uuid === itemUuid) ??
56
+ bundleServices?.find((service) => service.isActive) ??
57
+ null;
58
+ ```
59
+ The subscription card **and** the cancel gate both read this same `activeService`, so older links
60
+ without a resolvable `itemUuid` behave consistently across both.
61
+
62
+ 2. **Subscription details.** `subscription` = `{ plan, price, renewalDate, entitlementUuid }` from
63
+ `activeService`, else `null`. The card renders whenever `subscription` exists — **including for
64
+ warranty services**. Gating cancel does not hide the plan/price/renewal information.
65
+
66
+ 3. **The gate (positive, not negative).**
67
+ ```ts
68
+ const canCancelSubscription = activeService?.serviceType === "tech";
69
+ ```
70
+ `'warranty'` **and** `'other'` therefore both hide it — an unclassified service fails closed.
71
+ `serviceType` comes from `detectServiceType(bundleName || itemTitle)` in `useBundleServices`
72
+ (title-substring classification: contains "warranty"/"whole home" and not "tech" → `'warranty'`;
73
+ contains "tech"/"support" → `'tech'`; else `'other'`). This is the same signal that switches
74
+ `ServiceCard`'s second data row between **Price** (tech) and **Address** (warranty), so the cancel
75
+ gate and the service cards agree by construction.
76
+
77
+ 4. **The view gates three things with one boolean.**
78
+ ```ts
79
+ const showCancelUi = CANCEL_SUBSCRIPTION_ENABLED && canCancelSubscription;
80
+ ```
81
+ - the Cancel Subscription button
82
+ - the confirm modal (`showCancelUi && showCancelModal`)
83
+ - the `cancelNote` sentence appended to the `subscriptionDetails` string
84
+
85
+ The note is gated deliberately: leaving *"You can cancel anytime…"* on a card with no cancel
86
+ button is self-contradictory copy.
87
+
88
+ 5. **Confirm flow.** `handleConfirmCancel()` awaits `cancelSubscription()`; it closes the modal
89
+ **only** on a genuine resolve, and on rejection keeps the modal open and renders
90
+ `subscriptionFields.confirm.errorMessage`. Since the stub always throws, today's real-world
91
+ outcome is always the error state — intentional, so nothing reports a cancellation that did not
92
+ happen.
93
+
94
+ ## Decision — gate on `serviceType`, not on `isRateWarranty`
95
+
96
+ The page already carries a separate warranty signal: `isRateWarranty = bundleUuid ===
97
+ HOME_WARRANTY_BUNDLE_UUID`, used to route the **TICKETS** support method to the claims-style
98
+ create-ticket link. It was deliberately **not** reused for the cancel gate:
99
+
100
+ - `serviceType` is a *product classification*, so any service classified `'tech'` gets cancel —
101
+ it does not need a bundle uuid allowlist maintained per product.
102
+ - It keeps this page consistent with the service cards, which already branch on `serviceType`.
103
+ - A bundle-uuid check would silently fail open for any new warranty bundle uuid.
104
+
105
+ **Why WH is excluded at all:** Whole Home warranty contracts (the AIG-backed warranty product) are
106
+ not self-service cancellable from the portal; only tech-support subscriptions are.
107
+
108
+ ## Gotchas / known issues
109
+
110
+ - **The cancel UI lives only on `GetSupportPage.tsx`.** `ServiceCard` has no cancel affordance —
111
+ do not go looking for one in `components/`, and do not add a second entry point without
112
+ reproducing the same `showCancelUi` gate.
113
+ - **No backend endpoint exists.** `cancelSubscription()` throws
114
+ `"Subscription cancellation is not available yet."` by design (reject, never resolve). Swap the
115
+ body for the real API call when the endpoint ships; do not "fix" it by resolving.
116
+ - **`detectServiceType` now has a second consumer with a policy consequence.** Widening its
117
+ `'tech'` keywords does not just change a card row any more — it **exposes the cancel button** for
118
+ whatever newly matches. Check both call sites before touching the classifier.
119
+ - **The gate fails closed on `'other'`.** If a legitimately cancellable service stops matching
120
+ "tech"/"support" in its bundle/item title, its cancel button silently disappears. The title is the
121
+ only input.
122
+ - **`cancelNote` is not standalone copy** — it is concatenated into `subscriptionDetails` and gated
123
+ by `showCancelUi`. Any new surface rendering that string must gate it the same way.
124
+ - **`CANCEL_SUBSCRIPTION_ENABLED = false` hides button + modal + note but keeps the subscription
125
+ details card** — that is the intended kill-switch behavior.
126
+ - **Never close the confirm modal on failure.** Closing it reads to the user as a successful
127
+ cancellation.
128
+
129
+ ## Client variations
130
+
131
+ Rate is the consumer of this page today. Its product mix — Whole Home Warranty (AIG-backed) plus
132
+ tech support — is what makes the gate load-bearing: both products appear as active services for the
133
+ same user, so the page must distinguish them per service rather than per account. See
134
+ [Rate profile](../../../../clients/rate/profile.md) and
135
+ [Service Card Entitlement Display](../../../../clients/rate/features/service-card-entitlements.md).
136
+
137
+ ## Change history
138
+
139
+ - 2026-07-28 — Cancel Subscription UI gated by service type: viewModel exposes
140
+ `canCancelSubscription` (`activeService?.serviceType === "tech"`), and the view's `showCancelUi`
141
+ now gates the button, the confirm modal, and the `cancelNote` sentence. WH warranty and
142
+ unclassified services keep the subscription details card with no cancel path. Also first capture of
143
+ the pre-existing flow: cancel exists only on `GetSupportPage`, is flagged by
144
+ `CANCEL_SUBSCRIPTION_ENABLED`, and `cancelSubscription()` is a stub that always rejects until the
145
+ backend endpoint ships. (tcox)
@@ -23,7 +23,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
24
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
25
25
  - **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
26
- - **toga2-view** (TOGa View Frontend) — 6 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
26
+ - **toga2-view** (TOGa View Frontend) — 7 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
27
27
  - **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
28
28
  - **talos** (TOGa IQ) — 7 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
29
29
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
@@ -2,6 +2,6 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
- | [AIG Contract Reconciliation & Dealer Programs (STS_001 / SA_001)](features/contract-reconciliation.md) | 2.0 | How to reconcile an **AIG contract sales sheet** against the `Client_Aig` tenant, and the durable finding that the **Staples Advantage.com (`SA_001`) program is | _underscore/Model/Client/Entitlement.php, _underscore/Model/Aig/Entitlement.php, _underscore/Model/Client/Contact.php, _underscore/Model/Aig/Contact.php |
5
+ | [AIG Contract Reconciliation & Dealer Programs (STS_001 / SA_001)](features/contract-reconciliation.md) | 2.0 | How to reconcile an **AIG contract sales sheet** against the `Client_Aig` tenant, and the durable finding that the **Staples Advantage.com (`SA_001`) program do | _underscore/Model/Client/Entitlement.php, _underscore/Model/Aig/Entitlement.php, _underscore/Model/Client/Contact.php, _underscore/Model/Aig/Contact.php |
6
6
  | [AIG Entitlement Intake & SaleItem Code Resolution](features/entitlement-intake.md) | 2.0 | The `_Model_Aig_Entitlement::prePost` interceptor below is the code path for AIG protection-plan entitlements arriving as `api2` V2 JSON POSTs — and that path * | _underscore/Model/Aig/Entitlement.php, dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql |
7
7
  | [AIG (Staples Protection Plan)](profile.md) | 2.0 | AIG is the warranty underwriter behind the **Staples Protection Plan** retail program. | |
@@ -5,7 +5,7 @@ project: API
5
5
  client: aig
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-07-28
8
+ updated: 2026-07-29
9
9
  owners: ["mhammontree"]
10
10
  files:
11
11
  - _underscore/Model/Client/Entitlement.php
@@ -20,17 +20,18 @@ related:
20
20
  ## Summary
21
21
 
22
22
  How to reconcile an **AIG contract sales sheet** against the `Client_Aig` tenant, and the
23
- durable finding that the **Staples Advantage.com (`SA_001`) program is entirely absent from
24
- `Client_Aig`** (TRUE-80179). AIG runs **two dealer programs under the one `Client_Aig`
25
- tenant**, distinguished by `DealerId` in AIG's own data:
23
+ durable finding that the **Staples Advantage.com (`SA_001`) program does not route into
24
+ `Client_Aig` via api2 at all** (TRUE-80179). AIG runs **two dealer programs under the one
25
+ `Client_Aig` tenant**, distinguished by `DealerId` in AIG's own data:
26
26
 
27
- | DealerId | Program |
28
- |---|---|
29
- | `STS_001` | **Staples Technology Solutions** |
30
- | `SA_001` | **Staples Advantage.com** |
27
+ | DealerId | Program | Status |
28
+ |---|---|---|
29
+ | `STS_001` | **Staples Technology Solutions** | live api2 feed; intake gap **RESOLVED 2026-07-29** (self-healed) |
30
+ | `SA_001` | **Staples Advantage.com** | **never reaches api2** — separate feed; scope decision is the one open item |
31
31
 
32
- `STS_001` arrives as a **live API feed** (and had a large gap from a now-fixed intake bug — see
33
- below); `SA_001` was never ingested at all (open scope question).
32
+ `STS_001` arrives as a **live API feed**; its 156-contract gap from the now-fixed intake bug
33
+ **self-healed** (the feed re-sent and all 156 succeeded — see below). `SA_001` never reaches our
34
+ API at all — a confirmed fact, not a missing-intake bug (see the `SA_001` section).
34
35
 
35
36
  ## Match keys (sales sheet → `Client_Aig`)
36
37
 
@@ -70,41 +71,62 @@ and dropping every contract that used an `ASI-*` code. Cause: the TRUE-79534 `AS
70
71
  query threw MySQL ERROR 1064 → 500. Full chain in
71
72
  [entitlement-intake → Root cause of the 500s](entitlement-intake.md#root-cause-of-the-intake-500s-true-79534-catalog-never-deployed).
72
73
 
73
- **Fix status (2026-07-28):** the catalog (~2,200 codes) is now loaded on prod, so **new** `STS_001`
74
- POSTs succeed. **Still open — do NOT mark resolved:**
75
- - **(a) Back-fill of already-dropped contracts.** Contracts that 500'd are **not** created
76
- retroactively — back-fill needs **AIG to re-send** them, or a **manual/`dbchanges2` load**.
77
- Quantify the exact set by re-running this reconciliation against a **fresh prod copy**.
78
- - **(b) Unguarded empty-`$itemId` in `prePost`** — a still-missing code will 500 again (ERROR 1064)
79
- instead of returning a clean "Missing AIG item ID." Latent until guarded.
80
- - **(c) Deploy process gap** — investigate *why* the migration SQL step was skipped in the deploy,
81
- so future `dbchanges2` changes aren't silently missed.
74
+ **RESOLVED 2026-07-29 — the 156 self-healed.** After the catalog (~2,200 codes) loaded on prod, AIG's
75
+ **next scheduled batch re-sent all 156 outstanding `STS_001` contracts and every one succeeded**
76
+ (`POST /v2/entitlements` = 201 × 156, zero failures; `Client_Aig.Entitlements` gained exactly 156
77
+ rows). **No manual back-fill was needed** — the feed re-sends OUTSTANDING contracts every run, so
78
+ the backlog cleared itself once intake worked (see
79
+ [entitlement-intake → scheduled re-send batch](entitlement-intake.md#feed-is-a-scheduled-re-send-batch-not-one-shot--self-healing)).
80
+ The `STS_001` intake failure mode is **RESOLVED in prod.**
81
+
82
+ **Still open (tickets, not doc work):**
83
+ - **(a) Unguarded empty-`$itemId` in `prePost`** — a still-missing sale-item code will 500 again
84
+ (ERROR 1064) instead of returning a clean "Missing AIG item ID." Latent until guarded.
85
+ - **(b) Deploy process gap** — investigate *why* the TRUE-79534 migration SQL step was skipped in
86
+ the deploy, so future `dbchanges2` changes aren't silently missed.
82
87
 
83
- ### 545 `SA_001` missing — still an open scope question
88
+ ### 545 `SA_001` missing — confirmed out-of-pipeline (separate feed)
84
89
 
85
90
  The entire `SA_001` (Staples Advantage.com) feed is **absent from `Client_Aig`** — confirmed
86
91
  absent from **all** local tenant DBs, not just this one — and is a **separate** matter from the
87
92
  `STS_001` catalog bug above.
88
93
 
94
+ **CONFIRMED (2026-07-29): `SA_001` never reaches our API at all.** The failed-POST logs and the
95
+ vendor split (on `$.vendor.number`) show **only `STS_001`** (100 unique failed contracts) and
96
+ **zero `SA_001`** across **all** response codes. So the 545 `SA_001` contracts (506 active) are a
97
+ **genuinely separate feed that does not route into `Client_Aig` via api2** — this is **not** a
98
+ missing-intake bug on our side. The long-open scope question is now a **decided fact**: `SA_001` is
99
+ out-of-pipeline. Do **not** re-investigate whether it is a missing-intake defect.
100
+
89
101
  **Real support failure that surfaced it:** a customer (client contract `7679463206-2`, AIG
90
102
  contract `1000046237903`, an **active 2YR Printer** plan) called in and could not be found,
91
103
  because their `SA_001` contract was never ingested.
92
104
 
93
- **Open scope question (still unresolved):** is `SA_001` *supposed* to route into `Client_Aig` at
94
- all, or is it a separate program with its own destination? This needs an answer from AIG / the
95
- team before back-filling — do not assume the gap is simply a missed load.
105
+ **The one remaining open item (a ticket/business decision, not a bug):** *should* `SA_001` flow
106
+ into `Client_Aig`, and if so via what feed/integration? That is a scope/integration decision for
107
+ AIG / the team — not a back-fill of a dropped load. Until decided, `SA_001` stays out-of-pipeline.
96
108
 
97
109
  ## How to load a missing program / sheet (back-fill)
98
110
 
99
- If `SA_001` (or a back-fill of dropped `STS_001` contracts) is confirmed in-scope for
100
- `Client_Aig` and AIG cannot re-send via the API, it can be loaded the same way the SaleItem
101
- catalog is — a `dbchanges2/Client_Aig/` migration with a session-scoped temp table + anti-join
111
+ If `SA_001` is ever confirmed in-scope for `Client_Aig` and AIG cannot route it through api2, it
112
+ can be loaded the same way the SaleItem catalog is — a `dbchanges2/Client_Aig/` migration with a
113
+ session-scoped temp table + anti-join
102
114
  (see [entitlement-intake.md → Uploading new codes](entitlement-intake.md#uploading-new-codes-the-recurring-task);
103
- **pin the temp column's collation** to avoid the prod-only ERROR 1267). Preferred, though, is
104
- letting AIG **re-send** the contracts through the now-working live feed.
115
+ **pin the temp column's collation** to avoid the prod-only ERROR 1267). Note the `STS_001` backlog
116
+ did **not** need this — a scheduled re-send self-healed it once intake worked, so prefer letting a
117
+ feed re-send over a manual load whenever the contracts do reach api2.
105
118
 
106
119
  ## Change history
107
120
 
121
+ - 2026-07-29 — TRUE-80179 / TRUE-79534: **156 active `STS_001` RESOLVED (self-healed).** The next
122
+ scheduled batch re-sent all 156 outstanding contracts and every one succeeded (201 × 156, zero
123
+ failures; `Client_Aig.Entitlements` +156) — no manual back-fill needed, because the feed re-sends
124
+ outstanding contracts every run. Also **confirmed `SA_001` never reaches api2**: the failed-POST
125
+ logs / vendor split show only `STS_001` (100 unique failed) and zero `SA_001` across all codes, so
126
+ the 545 `SA_001` (506 active) are a genuinely separate out-of-pipeline feed — converting the old
127
+ scope question into a decided fact (not a missing-intake bug). Remaining open items are tickets:
128
+ the `SA_001` scope/integration decision, the unguarded `prePost` lookup, and the skipped-migration
129
+ deploy gap. (mhammontree)
108
130
  - 2026-07-28 — TRUE-80179 / TRUE-79534: root-caused the **156 missing active `STS_001`**. These are
109
131
  a **live API feed** whose writes were 500'ing (ERROR 1064) and being dropped because the
110
132
  TRUE-79534 `ASI-*`/`SM-*` catalog never deployed to prod `Client_Aig.Items` — corrects the earlier
@@ -5,7 +5,7 @@ project: API
5
5
  client: aig
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-07-28
8
+ updated: 2026-07-29
9
9
  owners: ["mhammontree"]
10
10
  files:
11
11
  - _underscore/Model/Aig/Entitlement.php
@@ -25,13 +25,17 @@ The `_Model_Aig_Entitlement::prePost` interceptor below is the code path for AIG
25
25
  protection-plan entitlements arriving as `api2` V2 JSON POSTs — and that path **is a live
26
26
  production feed.** AIG posts real customer contracts to `POST /v2/entitlements` on
27
27
  `api-writer.togahub.com` (authenticated as client `Aig`, vendor `STS_001`) using the current
28
- `ASI-*` / `SM-*` sale-item codes. Until 2026-07-28 that feed was **silently failing with HTTP
29
- 500** because `Client_Aig.Items` was missing the entire `ASI-*`/`SM-*` catalog (the TRUE-79534
30
- load never ran on prod), so every posted contract was dropped at intake (see
31
- [Intake is a live API feed](#intake-is-a-live-api-feed-and-was-silently-failing-true-80179--true-79534)
32
- and [Root cause of the 500s](#root-cause-of-the-intake-500s-true-79534-catalog-never-deployed),
33
- TRUE-80179 / TRUE-79534). The historical **430** `Client_Aig` entitlements were a one-time
34
- Sept-2025 bulk load — **that part stands** — but AIG has since switched to the live API feed.
28
+ `ASI-*` / `SM-*` sale-item codes. From before 2026-07-28 until the catalog was loaded, that feed
29
+ was **silently failing with HTTP 500** because `Client_Aig.Items` was missing the entire
30
+ `ASI-*`/`SM-*` catalog (the TRUE-79534 load never ran on prod), so every posted contract was
31
+ dropped at intake. **RESOLVED 2026-07-29** — once the catalog loaded, AIG's next scheduled batch
32
+ delivered all **156** outstanding `STS_001` contracts and **every one succeeded** (`POST
33
+ /v2/entitlements` = **201 × 156**, zero failures; `Client_Aig.Entitlements` gained exactly 156
34
+ rows). **No manual back-fill was needed — the feed self-healed** (see
35
+ [Intake is a live API feed](#intake-is-a-live-api-feed-resolved-2026-07-29-true-80179--true-79534)
36
+ and [Feed is a scheduled re-send batch](#feed-is-a-scheduled-re-send-batch-not-one-shot--self-healing)).
37
+ The historical **430** `Client_Aig` entitlements were a one-time Sept-2025 bulk load — **that part
38
+ stands** — but AIG has since switched to the live API feed.
35
39
 
36
40
  On a V2 POST, before the entitlement is written, `prePost` resolves the **sold warranty SKU**
37
41
  (`saleItem.partNumber`) against `Client_Aig.Items` to set the `Entitlement.saleItemId` foreign
@@ -40,7 +44,7 @@ the lookup returns an **empty `$itemId`**, the downstream fulfillment query inte
40
44
  empty value, and it dies with MySQL **ERROR 1064** → **HTTP 500** (see the gotcha). Keeping
41
45
  `Client_Aig.Items` populated with AIG's current sale-item catalog is what keeps the feed working.
42
46
 
43
- ## Intake is a live API feed (and was silently failing) (TRUE-80179 / TRUE-79534)
47
+ ## Intake is a live API feed (RESOLVED 2026-07-29) (TRUE-80179 / TRUE-79534)
44
48
 
45
49
  The API-intake path in *How it works* is the **real, live feed.** An earlier capture wrongly
46
50
  concluded "no live feed — periodic bulk load only." That conclusion came from auditing
@@ -65,10 +69,34 @@ What the corrected audit found (TRUE-80179):
65
69
  contracts in [reconciliation](contract-reconciliation.md), and it kept losing contracts **daily**
66
70
  until the catalog was loaded on 2026-07-28.
67
71
 
72
+ **Resolution (2026-07-29):** once the catalog was loaded (TRUE-79534) and the temp-table collation
73
+ fixed, AIG's **next scheduled batch cleared the entire backlog in one pass** — `Logs_Aig` shows
74
+ `POST /v2/entitlements` = **201 × 156** on 2026-07-29 with **zero failures** in the core/writer
75
+ `Logs`, and `Client_Aig.Entitlements` gained exactly the **156** previously-missing active
76
+ `STS_001` rows. **No manual log-replay was needed.** The prior "intake 500s / Missing AIG item ID"
77
+ failure mode is **RESOLVED in prod for the STS_001 side**, which is now healthy and self-maintaining
78
+ (see next section for *why* it self-healed).
79
+
68
80
  **Practical takeaway:** AIG intake is a **live API feed whose health must be monitored** — not a
69
81
  manual bulk load, and not a phantom "no feed." When auditing it, check the **core/writer `Logs`**
70
82
  for non-2xx writes, not just `Logs_Aig`. For reconciling a sales sheet against the tenant (and the
71
- still-open `SA_001` program gap), see [AIG contract reconciliation](contract-reconciliation.md).
83
+ one remaining `SA_001` scope decision), see [AIG contract reconciliation](contract-reconciliation.md).
84
+
85
+ ## Feed is a scheduled re-send batch (not one-shot) → self-healing
86
+
87
+ **Correction of an interim read.** AIG's entitlement feed is a **scheduled batch that re-sends
88
+ OUTSTANDING / unacknowledged contracts every run** — it is **not** one-shot-per-contract, and it
89
+ does **not** "not retry." Supersedes any earlier note implying the feed does not retry.
90
+
91
+ Evidence: the same ~156 contracts were **re-POSTed on every run** and 500'd on the missing catalog —
92
+ that is what produced **~110,504 failed attempts for only ~100 distinct contract numbers** in the
93
+ logs. The moment the catalog resolved, the **very next scheduled run cleared all 156 in a single
94
+ pass** (156 × 201).
95
+
96
+ **Operational consequence:** a persistent intake bug is **silently retried, not dropped**, until it
97
+ is fixed — so a catalog/intake fix **auto-recovers the whole backlog on the next scheduled run**
98
+ with no manual back-fill. This is the key operational fact for the next incident: fix the intake
99
+ cause and let the feed re-send.
72
100
 
73
101
  ## Root cause of the intake 500s (TRUE-79534 catalog never deployed)
74
102
 
@@ -86,10 +114,12 @@ The full failure chain behind the HTTP 500s:
86
114
  ongoing daily, until the catalog was loaded (2026-07-28).
87
115
 
88
116
  The empty-`$itemId` is **unguarded** (see the gotcha): a missing code should fail cleanly with
89
- "Missing AIG item ID," not a raw SQL syntax error. This defect is now **confirmed firing in prod**,
90
- not theoretical. Loading the catalog fixes intake **going forward**; contracts already dropped are
91
- **not** retroactively created — back-fill is tracked in
92
- [contract reconciliation](contract-reconciliation.md).
117
+ "Missing AIG item ID," not a raw SQL syntax error. This defect was **confirmed firing in prod**,
118
+ not theoretical, and the **unguarded lookup remains open** (a future missing code will 500 again).
119
+ Loading the catalog fixed intake, and because the feed re-sends outstanding contracts (see
120
+ [scheduled re-send batch](#feed-is-a-scheduled-re-send-batch-not-one-shot--self-healing)), the
121
+ **backlog of dropped contracts self-healed on the next scheduled run** — no manual back-fill was
122
+ needed (see [contract reconciliation](contract-reconciliation.md)).
93
123
 
94
124
  ## Key files / entry points
95
125
 
@@ -245,6 +275,16 @@ this interceptor or use this dual-purpose Items pattern.
245
275
 
246
276
  ## Change history
247
277
 
278
+ - 2026-07-29 — TRUE-80179 / TRUE-79534 (**RESOLVED + correction**): the `STS_001` intake gap is
279
+ **closed and it self-healed.** After the catalog load + collation fix shipped, AIG's next
280
+ scheduled batch delivered all **156** outstanding `STS_001` contracts and every one succeeded
281
+ (`POST /v2/entitlements` = 201 × 156 on 2026-07-29, zero failures; `Client_Aig.Entitlements`
282
+ +156 rows). **No manual replay needed.** Also corrected the feed model: it is a **scheduled batch
283
+ that re-sends OUTSTANDING/unacknowledged contracts every run** (the ~156 re-POSTs on the missing
284
+ catalog produced ~110,504 failed attempts for only ~100 distinct contracts), so a persistent
285
+ intake bug is silently retried until fixed and then **auto-recovers on the next run** — supersedes
286
+ any earlier "no retry" implication. The unguarded empty-`$itemId` lookup and the skipped-migration
287
+ deploy gap remain open. (mhammontree)
248
288
  - 2026-07-28 — TRUE-80179 / TRUE-79534 (**CORRECTION — supersedes the entry below**): AIG intake
249
289
  **is** a live production API feed, and it was **silently failing with HTTP 500.** The earlier
250
290
  same-day "no live feed / bulk load only" conclusion was **wrong** — it audited `Logs_Aig` alone,
@@ -6,7 +6,7 @@ project: TOGa View Frontend
6
6
  client: rate
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-07-27
9
+ updated: 2026-07-28
10
10
  owners: ["bala", "tcox", "mhammontree"]
11
11
  files:
12
12
  - src/components/ServiceCard/ServiceCard.tsx
@@ -24,6 +24,7 @@ files:
24
24
  related:
25
25
  - clients/rate/profile.md
26
26
  - whole-home-warranty-purchase-guard.md
27
+ - 2.0/apps/toga2-view/features/get-support-cancel-subscription.md
27
28
  ---
28
29
 
29
30
  ## Summary
@@ -101,6 +102,11 @@ This feature is Rate-specific. The warranty card showing Address instead of Pric
101
102
  by Rate's product mix (Whole Home Warranty + Tech Support). Other clients with different
102
103
  product types would need `detectServiceType` extended or overridden.
103
104
 
105
+ **`detectServiceType` is no longer display-only.** As of 2026-07-28 it also gates the
106
+ **Cancel Subscription** UI on the Get Support page (`serviceType === 'tech'` — WH warranty and
107
+ unclassified services get no cancel path). See
108
+ [Get Support — Subscription Details & Cancel Subscription Gating](../../../2.0/apps/toga2-view/features/get-support-cancel-subscription.md).
109
+
104
110
  ## Gotchas / known issues
105
111
 
106
112
  - **`ojoin` is required for address** — using INNER JOIN (`join`) drops every entitlement
@@ -115,6 +121,10 @@ product types would need `detectServiceType` extended or overridden.
115
121
  `AddSubscriptionSheet`) replaces the old "Get Started" inactive bundle cards.
116
122
  - **`useBundleServices` is shared** — both `useHomePageViewModel` and `useServicePageViewModel`
117
123
  call it. Changes to the hook affect both pages.
124
+ - **Widening `detectServiceType`'s `'tech'` keywords now changes entitlement *permissions*, not just
125
+ a card row.** The Get Support page gates its Cancel Subscription button on
126
+ `serviceType === 'tech'`, so anything newly classified as tech becomes self-service cancellable.
127
+ Review both consumers (`ServiceCard` row 2 and the cancel gate) before editing the classifier.
118
128
  - **Show the entitlement's OWN pin only — never the contact's shared address.** The removed
119
129
  `contact.primaryContactAddress` fallback made every unpinned card display the same (latest)
120
130
  address because that field is one shared, purchase-overwritten value per contact. An unpinned
@@ -137,6 +147,9 @@ product types would need `detectServiceType` extended or overridden.
137
147
 
138
148
  ## Change history
139
149
 
150
+ - 2026-07-28 — `detectServiceType` gained a second, non-display consumer: the Get Support page gates
151
+ its Cancel Subscription UI on `serviceType === 'tech'`, so the classifier now decides which
152
+ entitlements are self-service cancellable. (tcox)
140
153
  - 2026-07-27 — TRUE-79533: recorded that `fetchAddressesByIds`
141
154
  (`GET /v2/addresses?fields=id,line1,...`) returned **403 `EZ-2`** because the **`Addresses.id`
142
155
  field** had no read ACL grant in `Client_Rate` — the numeric `id` is ACL-gated like any field. Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.468",
3
+ "version": "1.0.470",
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",