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.
- package/knowledge/2.0/apps/api2/features/request-logging.md +32 -1
- package/knowledge/2.0/apps/toga2-view/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga2-view/features/get-support-cancel-subscription.md +145 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/aig/INDEX.md +1 -1
- package/knowledge/clients/aig/features/contract-reconciliation.md +50 -28
- package/knowledge/clients/aig/features/entitlement-intake.md +54 -14
- package/knowledge/clients/rate/features/service-card-entitlements.md +14 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
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)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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) —
|
|
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
|
|
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-
|
|
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
|
|
24
|
-
`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
|
|
33
|
-
|
|
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
|
-
**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 —
|
|
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
|
-
**
|
|
94
|
-
|
|
95
|
-
|
|
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`
|
|
100
|
-
|
|
101
|
-
|
|
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).
|
|
104
|
-
|
|
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-
|
|
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.
|
|
29
|
-
500** because `Client_Aig.Items` was missing the entire
|
|
30
|
-
load never ran on prod), so every posted contract was
|
|
31
|
-
|
|
32
|
-
and
|
|
33
|
-
|
|
34
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
90
|
-
not theoretical
|
|
91
|
-
|
|
92
|
-
[
|
|
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-
|
|
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