toga-ai 1.0.616 → 1.0.618
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/1.0/apps/tools/features/errors-curation-console.md +15 -2
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/error-reporting-issue-event.md +48 -1
- package/knowledge/2.0/apps/toga2-commerce/features/expedited-shipping-gating.md +18 -4
- package/knowledge/2.0/apps/toga2-commerce/features/shipping-cost-waiver-gating.md +18 -3
- package/knowledge/clients/rate/INDEX.md +1 -1
- package/knowledge/clients/rate/features/aig-contract-creation.md +138 -6
- package/knowledge/clients/rate/features/subscription-cancellation.md +59 -1
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-08-19
|
|
10
|
+
owners: ["jcardinal", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- tools/mvc/errors/get.php
|
|
13
13
|
- tools/mvc/errors/post.php
|
|
@@ -266,6 +266,14 @@ They were renamed from plural on 2026-08-01, after the pipeline was already in p
|
|
|
266
266
|
|
|
267
267
|
## Gotchas / known issues
|
|
268
268
|
|
|
269
|
+
- **⚠ Recipients attach to an `issueId`, so they cannot be configured in advance.** A new business
|
|
270
|
+
exception's recipients can only be added **after** the condition first fires and creates the
|
|
271
|
+
`Logs.Issue` row — declaring a stable `issueKey` in code does **not** let you pre-wire the
|
|
272
|
+
email list. Therefore the **first occurrence always routes to a developer ClickUp task**, and
|
|
273
|
+
somebody has to come back here and add recipients (which is also what flips `isManaged = 1`).
|
|
274
|
+
Live example: issue **208** `COMPASS_OFFICE_DEPOT_PO_IMPORT_REJECTED_OVER_QUANTITY` is still
|
|
275
|
+
ClickUp-routed because nobody did. See
|
|
276
|
+
[error reporting — Issue/Event capture](../../../../2.0/apps/_underscore/features/error-reporting-issue-event.md).
|
|
269
277
|
- **⚠ OPEN BUG (found 2026-08-04, deliberately left unfixed pending the developer's go-ahead):
|
|
270
278
|
`tools/mvc/errors/post.php` ~line 181, the merge-fingerprint action, will merge into the WRONG
|
|
271
279
|
issue.** It takes a reference **or** a numeric id in **one** field and disambiguates with
|
|
@@ -312,6 +320,11 @@ They were renamed from plural on 2026-08-01, after the pipeline was already in p
|
|
|
312
320
|
|
|
313
321
|
## Change history
|
|
314
322
|
|
|
323
|
+
- 2026-08-19 — Documented (no code change) that **email recipients are keyed by `issueId`, so they
|
|
324
|
+
cannot be pre-configured** for a business exception that has never fired — the first occurrence
|
|
325
|
+
always routes to a developer ClickUp task, and adding a recipient here is what converts it to
|
|
326
|
+
business email (and sets `isManaged = 1`). Found while wiring a new Rate business exception.
|
|
327
|
+
(mhammontree)
|
|
315
328
|
- 2026-08-05 — **Built:** the `/errors` console surfaces the new Issue-level RESOLVED lifecycle —
|
|
316
329
|
an **Open/Resolved/All** status filter (default Open, allowlisted value), a **Resolved badge**
|
|
317
330
|
(listing + detail header), a **`dtAutoResolved`** fact row, and the ClickUp-episodes column
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
| [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
|
|
21
21
|
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, _underscore/String.php, worker2/Worker/Infrastructure/Email/Send.php, _underscore/Model/Client/Logs/Email.php, _underscore/Model/Client/Logs/EmailAttachment.php |
|
|
22
22
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
23
|
-
| [Error Reporting — Issue/Event Capture, Fingerprinting & Aggregation](features/error-reporting-issue-event.md) | Platform-wide error reporting for TOGA 2.0, built on an **Issue / Event** aggregation model in the **shared Core Logs DB**. | _underscore/Error.php, _underscore/Database.php, _underscore/Exception/Business.php, api2/Controller/Index.php, worker2/Controller/Index.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, _underscore/Model/Core/Logs/IssueFingerprint.php, _underscore/Model/Core/Logs/IssueClickupTask.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, _underscore/Model/Core/Logs/IssueAreaOwner.php, dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql, dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql, dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql |
|
|
23
|
+
| [Error Reporting — Issue/Event Capture, Fingerprinting & Aggregation](features/error-reporting-issue-event.md) | Platform-wide error reporting for TOGA 2.0, built on an **Issue / Event** aggregation model in the **shared Core Logs DB**. | _underscore/Error.php, _underscore/Database.php, _underscore/Exception/Business.php, api2/Controller/Index.php, worker2/Controller/Index.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, _underscore/Model/Core/Logs/IssueFingerprint.php, _underscore/Model/Core/Logs/IssueClickupTask.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, _underscore/Model/Core/Logs/IssueAreaOwner.php, worker2/Worker/Infrastructure/Errors.php, tools/mvc/errors/issue/get.php, tools/mvc/errors/post.php, dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql, dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql, dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql |
|
|
24
24
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
25
25
|
| [FIELD_STORAGE fields — per-row lazy hydration and the platform-wide missing-column 500](features/field-storage-row-hydration.md) | `FIELD_STORAGE` is the 2.0 field type for blob-backed columns (S3 or local folder). | _underscore/Model.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/Invoice.php, dbchanges2/Core/HISTORIC/2024/2024-11b - item-fulfillments.sql |
|
|
26
26
|
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/probe_je_shape.php, test/@dave/fixer.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-19
|
|
10
10
|
owners: ["dfranks", "jcardinal", "mhammontree", "ajean", "bala", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Error.php
|
|
@@ -20,6 +20,9 @@ files:
|
|
|
20
20
|
- _underscore/Model/Core/Logs/IssueClickupTask.php
|
|
21
21
|
- _underscore/Model/Core/Logs/IssueEmailAddress.php
|
|
22
22
|
- _underscore/Model/Core/Logs/IssueAreaOwner.php
|
|
23
|
+
- worker2/Worker/Infrastructure/Errors.php
|
|
24
|
+
- tools/mvc/errors/issue/get.php
|
|
25
|
+
- tools/mvc/errors/post.php
|
|
23
26
|
- dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql
|
|
24
27
|
- dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql
|
|
25
28
|
- dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql
|
|
@@ -659,8 +662,52 @@ clientUserId). **Neither was built.** As built instead:
|
|
|
659
662
|
clients) rather than client-user ids, so recipients who have **no login** can still be
|
|
660
663
|
addressed.
|
|
661
664
|
|
|
665
|
+
### ⚠ Recipients are keyed by `issueId`, NOT `issueKey` — so the FIRST occurrence always goes to a developer
|
|
666
|
+
|
|
667
|
+
**Corrects a natural (and wrong) assumption:** that declaring an `issueKey` in code lets you
|
|
668
|
+
pre-configure who gets emailed. It does not. Recipients attach to the **`Logs.Issue` row**, which
|
|
669
|
+
does not exist until the condition **first fires**.
|
|
670
|
+
|
|
671
|
+
- Path: **Tools → `/errors` → open the issue → "Email recipients" → "Add recipient"** (client,
|
|
672
|
+
email, TO/CC/BCC) — `tools/mvc/errors/issue/get.php` (~L959 "Email recipients"),
|
|
673
|
+
`tools/mvc/errors/post.php` (~L149).
|
|
674
|
+
- That write inserts `Logs.IssueEmailAddress` (`uuid`, `issueId`, `clientId`, `emailAddress`,
|
|
675
|
+
`toCcBcc`) and sets `Issue.isManaged = 1`. worker2 reads them via
|
|
676
|
+
`worker2/Worker/Infrastructure/Errors.php::loadRecipientsByIssueId()`.
|
|
677
|
+
- **Adding a recipient is what converts an issue from developer-ClickUp-task routing to business
|
|
678
|
+
email.** `clientId 0` = ALL CLIENTS (Rate is `Core.Clients` id 39).
|
|
679
|
+
|
|
680
|
+
**Consequence to plan around:** every brand-new business exception routes its **first** occurrence to
|
|
681
|
+
a **developer ClickUp task**; someone must then configure recipients. Production evidence — only two
|
|
682
|
+
keyed business issues exist: id **24** ref `1X` `COMPASS_SALES_ORDER_MITS_TRANSMIT_REJECTED`,
|
|
683
|
+
`isManaged = 1` (the working precedent), and id **208** ref `AG`
|
|
684
|
+
`COMPASS_OFFICE_DEPOT_PO_IMPORT_REJECTED_OVER_QUANTITY`, `isManaged = 0` — a live example of a
|
|
685
|
+
business issue still going to ClickUp because nobody ever configured recipients.
|
|
686
|
+
|
|
687
|
+
### `captureException` without throwing is a supported pattern
|
|
688
|
+
|
|
689
|
+
`_Error::captureException(new _Exception_Business(...))` **records and notifies without throwing**,
|
|
690
|
+
which is the correct shape when one *leg* of a job is unresolved but the job itself is valid — see
|
|
691
|
+
`worker2/Controller/Index.php:286` and the worked case in
|
|
692
|
+
[Rate subscription cancellation](../../../clients/rate/features/subscription-cancellation.md)
|
|
693
|
+
(carrier cancel abandoned while the deactivation is legitimate). Wrap the reporting call in
|
|
694
|
+
`try/catch (Throwable)` so reporting can never fail the job, and `is_object()`-guard any property
|
|
695
|
+
read you feed it — `_Error` promotes a *"read property on int"* warning into a job-killing exception.
|
|
696
|
+
|
|
662
697
|
## Change history
|
|
663
698
|
|
|
699
|
+
- 2026-08-19 — Recorded that **email recipients are configured per `issueId`, not per `issueKey`**,
|
|
700
|
+
so they cannot be pre-configured before a condition first fires — the `Logs.Issue` row must exist
|
|
701
|
+
first, which means the **first occurrence of any new business exception always routes to a
|
|
702
|
+
developer ClickUp task**. Documented the Tools path (`/errors` → issue → "Email recipients" →
|
|
703
|
+
"Add recipient"), the `Logs.IssueEmailAddress` write + `Issue.isManaged = 1` side effect,
|
|
704
|
+
`loadRecipientsByIssueId()` on the worker2 side, `clientId 0` = all clients, and the production
|
|
705
|
+
census (only issue 24 `COMPASS_SALES_ORDER_MITS_TRANSMIT_REJECTED` is managed; issue 208
|
|
706
|
+
`COMPASS_OFFICE_DEPOT_PO_IMPORT_REJECTED_OVER_QUANTITY` still routes to ClickUp for want of
|
|
707
|
+
recipients). Also recorded **non-throwing
|
|
708
|
+
`_Error::captureException(new _Exception_Business(...))`** as a supported pattern for an
|
|
709
|
+
unresolved leg of an otherwise-valid job, with the Rate carrier-cancel worked case and the
|
|
710
|
+
`is_object()` guard. (mhammontree)
|
|
664
711
|
- 2026-08-18 — Recorded the environment-provisioning gotcha: the `Logs` cluster tables
|
|
665
712
|
(`Issue`/`Event`/`IssueFingerprint`/…) come from the `dbchanges2/Logs/` migrations, and an
|
|
666
713
|
environment missing them fails inside `_Error::captureException()` (e.g. `"Table
|
|
@@ -27,8 +27,12 @@ selected expedited method auto-resets to **"Standard Ground"**. The behavior is
|
|
|
27
27
|
driven** — a tenant opts in per shipping-method field via an `optionGates` entry; there is no
|
|
28
28
|
`if (clientSlug === ...)` branching. Omitting `optionGates` means expedited is always shown.
|
|
29
29
|
|
|
30
|
-
Applies today to
|
|
31
|
-
**QUAD**.
|
|
30
|
+
Applies today to: **COMPASS** (USER/MANAGER/SUPERUSER — **not ADMIN**, see below),
|
|
31
|
+
**COMPASSCANADA** (English + French), and **QUAD**. Because configs are **per role**
|
|
32
|
+
(`.../FIELDS/<CLIENT>/<LANG>/<ROLE>/CARTPAGE.ts`), opting a single role out of the gate is done
|
|
33
|
+
by dropping `optionGates: [EXPEDITED_SHIPPING_GATE]` from that one role's shipping-method field —
|
|
34
|
+
no shared-logic or client-branching change. **Compass USA ADMIN** now omits the gate, so admins
|
|
35
|
+
always see every method (Standard Ground + both expedited) in both regular and edit-order mode.
|
|
32
36
|
|
|
33
37
|
## Key files / entry points
|
|
34
38
|
- `src/pages/Cart/helpers/shippingOptionGates.ts` (NEW) — pure helpers + types:
|
|
@@ -40,8 +44,10 @@ Applies today to all three tenants: **COMPASS**, **COMPASSCANADA** (English + Fr
|
|
|
40
44
|
`EXPEDITED_SHIPPING_GATE` config constant: `optionNames` `["2nd Day EOB","Next Day Air"]`,
|
|
41
45
|
`visibleWhen { item: "primaryItem", path: "item.itemCategory.name", operator: "equals",
|
|
42
46
|
value: "COMPUTERS" }`.
|
|
43
|
-
-
|
|
44
|
-
each shipping-method field opts in via
|
|
47
|
+
- `CARTPAGE.ts` files (COMPASS USER/MANAGER/SUPERUSER — **ADMIN opted out**, COMPASSCANADA
|
|
48
|
+
ENGLISH x4 + FRENCH x4, QUAD x3) — each shipping-method field opts in via
|
|
49
|
+
`optionGates: [EXPEDITED_SHIPPING_GATE]`. `COMPASS/ENGLISH/ADMIN/CARTPAGE.ts` intentionally
|
|
50
|
+
omits it (and its now-unused import).
|
|
45
51
|
- `src/pages/Cart/view/cartForm/CartForm.tsx` — filters `shippingMethodOptions` through the
|
|
46
52
|
gates (reactive on `cartData.cartBundles`) before the select renders; resolves the shipping
|
|
47
53
|
field via `findShippingMethodField`; uses `SHIPPING_OPTION_LABEL_SEPARATOR` for the
|
|
@@ -103,6 +109,14 @@ behavior is expressed in config, not in code branches.
|
|
|
103
109
|
on that doc's slice-2 work-list.
|
|
104
110
|
|
|
105
111
|
## Change history
|
|
112
|
+
- 2026-08-19 — **Compass USA ADMIN role opted out of the expedited gate.** Removed
|
|
113
|
+
`optionGates: [EXPEDITED_SHIPPING_GATE]` (and the now-unused import) from
|
|
114
|
+
`COMPASS/ENGLISH/ADMIN/CARTPAGE.ts`, so admins always see Standard Ground + Next Day Air +
|
|
115
|
+
2nd Day EOB in both regular commerce mode **and** edit-order mode. Because configs are per
|
|
116
|
+
role, dropping the gate on the ADMIN config alone is the scoped customization — no
|
|
117
|
+
shared-logic or client-branching change. Side effect: in edit-order mode an existing order's
|
|
118
|
+
expedited selection is no longer hidden/reset to Standard Ground for admins.
|
|
119
|
+
USER/MANAGER/SUPERUSER still gate expedited behind a computer kit. (apeterson)
|
|
106
120
|
- 2026-08-19 — No change to expedited visibility. Noted that the shared shipping-rule type
|
|
107
121
|
gained an optional `requireNoOtherItemsInKit` flag (+ `bundleHasOtherItems()` helper) used by
|
|
108
122
|
the separate Standard Ground cost waiver — see
|
|
@@ -51,9 +51,18 @@ four Compass USA role configs (USER/ADMIN/MANAGER/SUPERUSER).
|
|
|
51
51
|
4. When the selected method qualifies, the effect in `CartPage` waives its `c_cost` to 0 before
|
|
52
52
|
writing `salesOrder.shipping`.
|
|
53
53
|
|
|
54
|
-
Resulting Compass USA behavior
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
Resulting Compass USA behavior (full truth table, confirmed complete — no further code change):
|
|
55
|
+
- **Solo computer kit on Standard Ground** → **$0** (waived; the freight is covered by the
|
|
56
|
+
`LT-FREIGHT` fee). "Solo" is scoped to the **kit** (`bundleProgressContents` has no non-primary
|
|
57
|
+
items), not the whole order.
|
|
58
|
+
- **Computer kit WITH other items in the kit, or any non-computer kit** → charged Standard
|
|
59
|
+
Ground **$10** (not waived).
|
|
60
|
+
- **Expedited (Next Day Air / 2nd Day EOB) + computers** → charged the method's `c_cost`
|
|
61
|
+
(2nd Day **$50**, Next Day **$100**), **never waived**.
|
|
62
|
+
|
|
63
|
+
All dollar amounts live in **`ShippingMethods.c_cost` in the Compass USA client DB** (developer
|
|
64
|
+
confirmed correct), **not** in frontend code — the waiver logic only zeroes a `c_cost`, it never
|
|
65
|
+
sets a price.
|
|
57
66
|
|
|
58
67
|
## Gotchas
|
|
59
68
|
- **The waiver only means what the flag says.** Before this fix the gate had no
|
|
@@ -68,6 +77,12 @@ expedited methods unchanged (still priced).
|
|
|
68
77
|
a computer kit mis-categorized in the catalog will neither waive nor gate.
|
|
69
78
|
|
|
70
79
|
## Change history
|
|
80
|
+
- 2026-08-19 — Confirmed the Compass USA shipping-charge truth table is fully satisfied by the
|
|
81
|
+
existing logic (no code change): solo computer kit on Standard Ground → $0 (waived, covered by
|
|
82
|
+
`LT-FREIGHT`); kit with other items or any non-computer kit → $10; expedited + computers →
|
|
83
|
+
method `c_cost` (2nd Day $50 / Next Day $100), never waived. Documented that all dollar amounts
|
|
84
|
+
live in `ShippingMethods.c_cost` in the Compass USA client DB, not in frontend code, and that
|
|
85
|
+
"solo computer" is scoped to the kit not the order. (apeterson)
|
|
71
86
|
- 2026-08-19 — Fixed an over-broad Standard Ground waiver for Compass USA computer-kit carts:
|
|
72
87
|
added the optional `requireNoOtherItemsInKit` flag on `PrimaryItemShippingRule` and the
|
|
73
88
|
`bundleHasOtherItems()` helper, and set the flag on
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
-
| [Rate AIG Warranty Contract Creation (silent-failure interceptor)](features/aig-contract-creation.md) | 2.0 | When a Rate entitlement is created, `_Model_Rate_Entitlement::postPost` (`_underscore/Model/Rate/Entitlement.php`) creates an **AIG warranty contract** as a non | _underscore/Model/Rate/Entitlement.php, _underscore/ApiRequest.php, worker2/Worker/Monitors/RateEntitlement.php, worker2/Worker/Rate.php, api2/Component/Api/V2/V2.php, test/@Mark/Rate/verify_aig_contract_identifier_persistence.php, test/@Mark/Rate/audit_wh_missing_aig_contracts.php, test/@Mark/Rate/aig_contract_lookup.php |
|
|
5
|
+
| [Rate AIG Warranty Contract Creation (silent-failure interceptor)](features/aig-contract-creation.md) | 2.0 | When a Rate entitlement is created, `_Model_Rate_Entitlement::postPost` (`_underscore/Model/Rate/Entitlement.php`) creates an **AIG warranty contract** as a non | _underscore/Model/Rate/Entitlement.php, _underscore/ApiRequest.php, worker2/Worker/Monitors/RateEntitlement.php, worker2/Worker/Rate.php, api2/Component/Api/V2/V2.php, test/@Mark/Rate/verify_aig_contract_identifier_persistence.php, test/@Mark/Rate/audit_wh_missing_aig_contracts.php, test/@Mark/Rate/aig_contract_lookup.php, test/@Mark/Rate/cancel_aig_contract.php |
|
|
6
6
|
| [Rate Monthly Reconciliation Report](features/monthly-reconciliation-report.md) | 1.0 | A monthly cron that emails an Excel reconciliation report covering all Rate subscription sales orders and their linked PayPal payments for the prior calendar mo | worker/crons/notifications/reports/rate/send_monthly_rate_purchases_report.php, worker/schedules/cron.worker.notification.json |
|
|
7
7
|
| [Rate SalesOrder → NetSuite CashSale Export (postPost)](features/netsuite-cashsale-export.md) | 2.0 | Rate sells home-warranty / home-tech-support products. | _underscore/Model/Rate/SalesOrder.php, _underscore/Model/Rate/Item.php |
|
|
8
8
|
| [Rate PayPal Subscription Purchase & Webhook Pipeline](features/paypal-subscription-purchase-webhook.md) | 2.0 | > # ⚠ STATUS (2026-08-10, TRUE-80575) — A FIXED (uncommitted), B DECIDED (not built) > > **A. | worker2/Worker/Rate.php, worker2/Config/production.ini, api2/Config/production.ini, _underscore/Component/Api/Paypal/Paypal.php, toga2-view/src/hooks/usePayPalSubscription.ts, toga2-view/src/services/paypalService.ts, toga2-view/src/pages/CheckOut/viewModel/useCheckoutPageViewModel.ts, toga2-view/src/pages/CheckOut/api/checkoutApi.ts, toga2-view/src/pages/Activation/view/Activation.tsx |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: rate
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-19
|
|
10
10
|
owners: [mhammontree, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Rate/Entitlement.php
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
- test/@Mark/Rate/verify_aig_contract_identifier_persistence.php
|
|
18
18
|
- test/@Mark/Rate/audit_wh_missing_aig_contracts.php
|
|
19
19
|
- test/@Mark/Rate/aig_contract_lookup.php
|
|
20
|
+
- test/@Mark/Rate/cancel_aig_contract.php
|
|
20
21
|
related:
|
|
21
22
|
- clients/rate/profile.md
|
|
22
23
|
- ../../../2.0/apps/api2/features/api-payload-interceptors.md
|
|
@@ -147,6 +148,23 @@ it sent blanks and AIG rejected them. `ET100014` was never "unexplained" — it
|
|
|
147
148
|
All three now have both an address and a `ContactPhoneNumbers` row, so a replay should be accepted
|
|
148
149
|
— **but read the duplicate-contract rule below first.**
|
|
149
150
|
|
|
151
|
+
## Data repair — `ET100013` cancelled at the carrier (2026-08-19, production)
|
|
152
|
+
|
|
153
|
+
Doug Winter — entitlement **39**, contact **226**, subscription **20** (`I-KE367533YH1L`):
|
|
154
|
+
|
|
155
|
+
1. Backfilled `c_aigContractNumber = '1000045962407'` and `c_aigContractId = 45962407` (taken from
|
|
156
|
+
AIG's report; id derived by stripping `10000`). The developer ran the UPDATE directly — **no
|
|
157
|
+
`dbchanges2` migration retained**, matching the `ET100020` precedent.
|
|
158
|
+
2. Cancelled at the **live** carrier — HTTP **200** `{"errors":null,"successful":true}`.
|
|
159
|
+
|
|
160
|
+
The subscription was already `isActive 0`, `dateEnd = dateCancelled = 2026-08-18`, with
|
|
161
|
+
`dtRequestToCancel` **NULL** — i.e. cancelled by the **webhook handler**, not the portal `cancelApi`.
|
|
162
|
+
|
|
163
|
+
**STILL OPEN — `ET100014` (Kimberly Stearns) and `ET100015` (Daniel Moran)** are the same shape: WH,
|
|
164
|
+
**both** identifiers NULL, previously 400'd by depth starvation. They need their **contract numbers
|
|
165
|
+
from AIG's report** before they can be cancelled. Note this doc's replay guidance targets
|
|
166
|
+
`POST /contract` (which *creates* coverage) — **these two need `/contract/cancel` instead.**
|
|
167
|
+
|
|
150
168
|
## ⚠ A NULL identifier does NOT mean the contract is missing (pre-TRUE-81049 rows)
|
|
151
169
|
|
|
152
170
|
**The most important operational rule in this doc.** On any entitlement created **before**
|
|
@@ -157,12 +175,64 @@ the contract, our columns were NULL.
|
|
|
157
175
|
**Replaying such a row creates a DUPLICATE contract at the carrier.** Before any replay or repair:
|
|
158
176
|
|
|
159
177
|
1. Query `Logs_Rate.Api` for a **2xx `POST .../contract`** for that customer.
|
|
160
|
-
2. **Match on the customer EMAIL, not `contractNumber`** — the `contractNumber` we sent was
|
|
161
|
-
throwaway uuid that matches nothing on our side
|
|
178
|
+
2. **Match on the customer EMAIL, not `contractNumber`** — the `contractNumber` we sent was
|
|
179
|
+
*usually* a throwaway uuid that matches nothing on our side (but not always — see
|
|
180
|
+
"AIG holds contracts our API never created" below).
|
|
162
181
|
3. If a success exists → **backfill** the identifiers from AIG's logged response. Never replay.
|
|
163
182
|
4. If the log window contains **zero** rows at all → the answer is **INCONCLUSIVE**, not "never
|
|
164
183
|
attempted."
|
|
165
184
|
|
|
185
|
+
## ⚠⚠ `POST /contract/cancel` REQUIRES `contractNumber` — the shipped cancel had NEVER worked
|
|
186
|
+
|
|
187
|
+
**Verified against the LIVE carrier on 2026-08-19** (`aigContractId 45962407`):
|
|
188
|
+
|
|
189
|
+
| Payload | AIG response |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `{aigContractId, cancelRequestDate}` — **what we shipped** | HTTP **400** `{"errors":[{"errorCode":"76","errorDescription":"Required fields not provided"}],"successful":false}` |
|
|
192
|
+
| `{aigContractId, cancelRequestDate, contractNumber}` | HTTP **200** `{"errors":null,"successful":true}` |
|
|
193
|
+
|
|
194
|
+
`worker2/Worker/Rate.php::cancelAigContract()` (~L566) sent only the first shape, so **every AIG
|
|
195
|
+
cancellation we have ever attempted failed** — and because that method `catch`es, `error_log()`s and
|
|
196
|
+
returns `false`, it failed **silently**. This doc previously recorded only that `/contract/cancel`
|
|
197
|
+
exists; it did not record its required fields. That gap is the bug.
|
|
198
|
+
|
|
199
|
+
**This is independent of the NULL-identifier problem.** Even a row with a populated
|
|
200
|
+
`c_aigContractId` would have 400'd. Fixing the identifiers alone would not have cancelled anything.
|
|
201
|
+
Working payload is `{aigContractId, cancelRequestDate, contractNumber}` — send the stored
|
|
202
|
+
`c_aigContractNumber`.
|
|
203
|
+
|
|
204
|
+
## `c_aigContractId` is derivable from `c_aigContractNumber` — strip the leading `10000`
|
|
205
|
+
|
|
206
|
+
Verified across **all 7** `Client_Rate` entitlements that hold both values:
|
|
207
|
+
|
|
208
|
+
`1000045859462`→`45859462` (ET100011) · `1000045886119`→`45886119` (ET100012) ·
|
|
209
|
+
`1000046067341`→`46067341` (ET100016) · `1000046232878`→`46232878` (ET100017) ·
|
|
210
|
+
`1000046411721`→`46411721` (ET100018) · `1000046482036`→`46482036` (ET100019) ·
|
|
211
|
+
`1000046483092`→`46483092` (ET100020)
|
|
212
|
+
|
|
213
|
+
AIG ids also run **sequentially by purchase date**, which independently corroborates a derived
|
|
214
|
+
value. **Why it matters:** AIG exposes no lookup endpoint, so when all you have is a contract number
|
|
215
|
+
off AIG's report, you can still compute the `aigContractId` the cancel call needs.
|
|
216
|
+
|
|
217
|
+
## ⚠ AIG holds contracts our API never created — a NULL identifier does not prove *absence* either
|
|
218
|
+
|
|
219
|
+
Doug Winter `ET100013` (entitlement 39) is the proof, and it **corrects** this doc's earlier claim
|
|
220
|
+
that the `contractNumber` AIG holds is always a throwaway uuid matching nothing on our side:
|
|
221
|
+
|
|
222
|
+
- Our **only** outbound `/contract` call for him was `Logs_Rate.Api` id `4180950`, 2026-04-01,
|
|
223
|
+
HTTP **400** (depth starvation — `contractNumber ""`, `saleItemId ""`, blank customer
|
|
224
|
+
id/name/email; AIG answered *"SaleItemId field is required / ContractNumber field is required"*).
|
|
225
|
+
- **Zero** outbound calls on 2026-05-01 — so the recurring PayPal charge does **not** re-trigger
|
|
226
|
+
contract creation. Only *entitlement creation* does.
|
|
227
|
+
- **Yet AIG's own report showed an ACTIVE contract**, CCN
|
|
228
|
+
`f8685561-2177-6e62-16d0-4d70865db51b` — **our real stored entitlement uuid** — under AIG CN
|
|
229
|
+
`1000045962407`.
|
|
230
|
+
|
|
231
|
+
**Practical rule: our logs cannot prove a customer is uncovered. AIG's report is the authority.**
|
|
232
|
+
A NULL identifier plus no logged success means *inconclusive*, in **both** directions — the earlier
|
|
233
|
+
rule covered "coverage may exist despite NULL columns"; this adds "coverage may exist even when our
|
|
234
|
+
only logged attempt failed."
|
|
235
|
+
|
|
166
236
|
## AIG exposes NO contract lookup endpoint (verified 2026-08-18)
|
|
167
237
|
|
|
168
238
|
Only three AIG routes exist for us: `POST /authentication/login`, `POST /contract`, and
|
|
@@ -197,6 +267,22 @@ and every other `/authentication/login` row. Readable by anyone with production
|
|
|
197
267
|
> The credential **value** is deliberately not recorded anywhere in this knowledge base — only
|
|
198
268
|
> where the exposure lives.
|
|
199
269
|
|
|
270
|
+
**Third instance of the same exposure class (found 2026-08-19):** an uncaught DB connect failure
|
|
271
|
+
prints the production DB **username and password** in the stack trace — observed live this session
|
|
272
|
+
with `ENVIRONMENT=production`.
|
|
273
|
+
|
|
274
|
+
The mechanism is **not** positional arguments: `_Database_Driver_Mysql::connect()`
|
|
275
|
+
(`_underscore/Database/Driver/Mysql.php:10`) already calls `mysqli_connect()` with **named**
|
|
276
|
+
arguments. PHP renders the arguments of **every** stack frame, and PHP 8.2+ marks
|
|
277
|
+
`mysqli_connect`'s password with `#[\SensitiveParameter]` — so *that* frame safely shows
|
|
278
|
+
`Object(SensitiveParameterValue)`. **Our wrapper's own `string $password` parameter carries no such
|
|
279
|
+
attribute, so the wrapper's frame prints it in plaintext.** There are **zero** uses of
|
|
280
|
+
`#[\SensitiveParameter]` anywhere in the non-vendor codebase, so this applies to every credential
|
|
281
|
+
our own code accepts as a parameter.
|
|
282
|
+
|
|
283
|
+
Not Rate-specific; noted here only because it was hit while working this flow. (The other two: this
|
|
284
|
+
AIG auth body, and the plaintext live PayPal secret in `api2/Config/production.ini`.)
|
|
285
|
+
|
|
200
286
|
## Diagnostic & repair tooling (`test/@Mark/Rate/`)
|
|
201
287
|
|
|
202
288
|
Reusable team diagnostics, not throwaways. The CLI scripts bootstrap `worker2`/`_underscore`.
|
|
@@ -219,6 +305,25 @@ Reusable team diagnostics, not throwaways. The CLI scripts bootstrap `worker2`/`
|
|
|
219
305
|
*If AIG ever provides a lookup endpoint, promote this into the `tools` repo behind the Active
|
|
220
306
|
Directory login (TRUE-78064 SSO pattern) so support can self-serve.*
|
|
221
307
|
|
|
308
|
+
- **`cancel_aig_contract.php`** — the reusable AIG cancel/repair one-off (TRUE-81049, 2026-08-19).
|
|
309
|
+
**Dry-run by default**; `APPLY=1` to act; `CONFIRM_PRODUCTION_AIG=1` required for the live
|
|
310
|
+
carrier; `NO_DB=1` for the prod-cluster-unreachable case (skips the DB read/backfill and prints
|
|
311
|
+
AIG's response as the only record); `CANCEL_EXTRA` takes JSON merged into the payload for field
|
|
312
|
+
iteration; `FORCE=1` overrides the refusal to cancel a subscription that is not already cancelled
|
|
313
|
+
on our side. It **backfills the identifiers BEFORE calling the carrier**, because the carrier call
|
|
314
|
+
is irreversible and the write is not. Prints AIG's verbatim response body.
|
|
315
|
+
|
|
316
|
+
**⚠ Test-harness gotcha — a worker2-bootstrapped local script CANNOT reach `Client_<Tenant>` in
|
|
317
|
+
production.** `worker2/Config/production.ini` has a **single** `[database] hostname` pointing at the
|
|
318
|
+
**Core** cluster, and in production `Core` / `Client_*` / `Logs*` are **separate clusters** — so
|
|
319
|
+
`_Database::register()` for `Client_Rate` fails with *"Unknown database"*. Locally everything sits
|
|
320
|
+
behind one endpoint, so this **only bites against production**;
|
|
321
|
+
`audit_wh_missing_aig_contracts.php` has the same limitation and was only ever run against
|
|
322
|
+
sandbox-dev. That is what `NO_DB=1` above exists for. **Corollary:** setting
|
|
323
|
+
`_ApiRequest::isLoggingEnabled = true` while `DB_CLIENT_LOGS` is unregistered makes the log write
|
|
324
|
+
throw *after* the HTTP call, and `_underscore`'s error handler then **exits** — silently killing a
|
|
325
|
+
script between two API calls with no output. **Logging must follow database availability.**
|
|
326
|
+
|
|
222
327
|
**Test-harness gotcha (bit us live):** in a verify script, **reads go to the READ host and cannot
|
|
223
328
|
see the WRITE host's uncommitted rows** — a successful write reads back as a silent no-op and a
|
|
224
329
|
passing fix looks broken. Call `_Database::transactionCommit()` before **every** read-back, and
|
|
@@ -264,9 +369,11 @@ creation vs. cancellation. This is an accepted, documented limitation — not an
|
|
|
264
369
|
**outbound response** (`api2/Component/Api/V2/V2.php:5934` passes `$outData`), which the engine
|
|
265
370
|
returns and never saves. This is why the AIG identifiers silently never persisted. See the
|
|
266
371
|
[API payload interceptors](../../../2.0/apps/api2/features/api-payload-interceptors.md) doc.
|
|
267
|
-
-
|
|
268
|
-
|
|
269
|
-
|
|
372
|
+
- **Cancelling the carrier contract needs BOTH `c_aigContractId` AND `c_aigContractNumber`** —
|
|
373
|
+
`worker2` `_Worker_Rate:521` gates on the id, but `/contract/cancel` rejects a payload without the
|
|
374
|
+
number (see the required-fields section above). A NULL means the AIG contract **can never be
|
|
375
|
+
cancelled**: the customer stops paying us and coverage stays active at AIG. That is the real cost
|
|
376
|
+
of the persistence bug.
|
|
270
377
|
- **Ordering: TRUE-81049 must land before/with TRUE-80575.** TRUE-80575's worker2 handler
|
|
271
378
|
re-reads the entitlement and alerts when `c_aigContractId` is empty — until identifiers
|
|
272
379
|
persist, that check false-alarms on **every** purchase. TRUE-80575 (1 `_underscore` commit + 3
|
|
@@ -295,6 +402,31 @@ creation vs. cancellation. This is an accepted, documented limitation — not an
|
|
|
295
402
|
|
|
296
403
|
## Change history
|
|
297
404
|
|
|
405
|
+
- 2026-08-19 — TRUE-81049 (fix parked in that already-scheduled deployment; no new ticket):
|
|
406
|
+
**`POST /contract/cancel` requires `contractNumber`, so the shipped AIG cancel had NEVER
|
|
407
|
+
worked.** Verified against the live carrier on `aigContractId 45962407` — the shipped
|
|
408
|
+
`{aigContractId, cancelRequestDate}` returns HTTP 400 errorCode 76 *"Required fields not
|
|
409
|
+
provided"*; adding `contractNumber` returns HTTP 200 `successful: true`. Silent because
|
|
410
|
+
`cancelAigContract()` catches/logs/returns false; **independent of the NULL-identifier bug** (a
|
|
411
|
+
populated id would still have 400'd). Fixed in `worker2/Worker/Rate.php` (~L566), commit
|
|
412
|
+
`a399145` on branch TRUE-81049 — **committed, not pushed.** Also recorded: `c_aigContractId` is
|
|
413
|
+
**derivable** from `c_aigContractNumber` by stripping the leading `10000` (verified on all 7 rows
|
|
414
|
+
holding both; AIG ids are sequential by purchase date), which is the workaround for the missing
|
|
415
|
+
lookup endpoint; **AIG holds contracts our API never created** — `ET100013`'s only `/contract`
|
|
416
|
+
call was a 400 and the recurring PayPal charge does *not* retry creation, yet AIG's report shows
|
|
417
|
+
an ACTIVE contract carrying **our real entitlement uuid** as its CCN, correcting the
|
|
418
|
+
"throwaway uuid matches nothing" claim and establishing that **AIG's report, not our logs, is the
|
|
419
|
+
authority** on coverage; the **`ET100013` production repair** (backfill + live cancel, direct
|
|
420
|
+
UPDATE, no migration kept) with `ET100014`/`ET100015` **still open** pending their contract
|
|
421
|
+
numbers from AIG; the new `test/@Mark/Rate/cancel_aig_contract.php` repair tool; the
|
|
422
|
+
**prod-cluster test-harness limit** (worker2's single `production.ini` hostname cannot reach
|
|
423
|
+
`Client_Rate`; and enabling `_ApiRequest` logging without `DB_CLIENT_LOGS` kills the script
|
|
424
|
+
silently between API calls); and a **third instance of the credential-exposure class** —
|
|
425
|
+
an uncaught DB connect failure prints user+password in the stack trace, because our
|
|
426
|
+
`_Database_Driver_Mysql::connect()` wrapper's `string $password` parameter lacks
|
|
427
|
+
`#[\SensitiveParameter]` (PHP 8.2+ protects `mysqli_connect`'s own frame but not ours; zero uses
|
|
428
|
+
of the attribute exist in the non-vendor codebase). Location only, never the value.
|
|
429
|
+
(mhammontree)
|
|
298
430
|
- 2026-08-18 — TRUE-81049: **corrected two claims in this doc that were wrong.** (1) The success
|
|
299
431
|
path did **not** persist the generated uuid or the AIG identifiers — `postPost` receives the
|
|
300
432
|
**outbound response** (`V2.php:5934` → `$outData`), so `$payload->c_aigContract*` and the
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: rate
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-19
|
|
10
10
|
owners: [mhammontree, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Rate/Subscription.php
|
|
@@ -39,6 +39,7 @@ related:
|
|
|
39
39
|
- ../../../2.0/apps/_underscore/features/config-group-access.md
|
|
40
40
|
- ../../../2.0/apps/api2/features/environment-variable-drives-underscore-branch.md
|
|
41
41
|
- ../../../2.0/standards/backend-testing.md
|
|
42
|
+
- ../../../2.0/apps/_underscore/features/error-reporting-issue-event.md
|
|
42
43
|
---
|
|
43
44
|
|
|
44
45
|
## Summary
|
|
@@ -220,6 +221,46 @@ RecordFields id literals were verified correct per environment.
|
|
|
220
221
|
read the cancellation columns.** This is why mirroring `dateEnd`'s grants was the right call rather
|
|
221
222
|
than inventing a worker-specific role.
|
|
222
223
|
|
|
224
|
+
## The carrier leg — a missing AIG identifier is now REPORTED, not silently skipped (2026-08-19)
|
|
225
|
+
|
|
226
|
+
`handleSubscriptionDeactivation` (`worker2/Worker/Rate.php` ~L519) guarded the AIG cancel with
|
|
227
|
+
`if ($entitlement && !empty($entitlement->c_aigContractId))` — and had **no `else`**. So a missing
|
|
228
|
+
identifier abandoned the **carrier leg with no record anywhere**: the customer stopped paying while
|
|
229
|
+
coverage stayed **active at AIG**. This defect was found by a **human noticing**, not by any system.
|
|
230
|
+
|
|
231
|
+
Now the else branch calls `reportAigContractNotCancellable()`, which captures
|
|
232
|
+
`_Exception_Business(issueKey: 'RATE_AIG_CONTRACT_CANCEL_MISSING_IDENTIFIER', URGENCY_HIGH)` naming
|
|
233
|
+
**which column** is missing.
|
|
234
|
+
|
|
235
|
+
**The pattern worth copying (general to 2.0):**
|
|
236
|
+
`_Error::captureException(new _Exception_Business(...))` **records the issue and notifies WITHOUT
|
|
237
|
+
throwing.** That is the right tool when *part* of a job is unresolved but the job itself is valid —
|
|
238
|
+
throwing here would abort a subscription deactivation that is entirely correct; only the carrier leg
|
|
239
|
+
is open. Precedent for the non-throwing call: `worker2/Controller/Index.php:286`. The whole
|
|
240
|
+
reporting call sits inside `try/catch (Throwable)` so **a reporting path can never fail the job**.
|
|
241
|
+
|
|
242
|
+
Splitting the failure modes also **sharpened the thrown exception**: a blank identifier no longer
|
|
243
|
+
reaches the `throw`, so a thrown exception now means exactly one thing — **the carrier rejected a
|
|
244
|
+
well-formed request.** Previously a blank `contractNumber` threw a *technical* exception and aborted
|
|
245
|
+
a valid deactivation.
|
|
246
|
+
|
|
247
|
+
### ⚠ The report MUST be gated on Whole Home Warranty, or it fires HIGH on every routine cancel
|
|
248
|
+
|
|
249
|
+
Rate's data model makes most cancellation events legitimately carrier-free:
|
|
250
|
+
|
|
251
|
+
- `Items.id 3` = **"Whole Home Warranty - Monthly"** — the **only** AIG-backed product.
|
|
252
|
+
- `Items.id 1` and `2` are **tech-support** products with **no carrier contract by design**.
|
|
253
|
+
- TRUE-80282's portal self-service cancellation **refuses warranties**, so **tech** cancellations
|
|
254
|
+
are the **dominant** `BILLING.SUBSCRIPTION.CANCELLED` case.
|
|
255
|
+
|
|
256
|
+
Ungated, the missing-identifier report would raise a HIGH-urgency **business** alert on every
|
|
257
|
+
routine tech cancellation. `isWholeHomeWarrantyEntitlement()` therefore uses the **same authority as
|
|
258
|
+
the purchase guard and the DB dedup** — sale-item **title keyword `warranty`** OR **`saleItemId 3`**
|
|
259
|
+
— and checks **both** the nested `saleItem` object **and** the flat `saleItemId` FK, so a **shallow
|
|
260
|
+
payload cannot disable the gate**. It is `is_object()`-guarded because `_Error` promotes a
|
|
261
|
+
*"read property on int"* warning into an **exception that would kill the job**. Verified
|
|
262
|
+
`ET100013`/`14`/`15` all carry `saleItemId 3` / title "Whole Home Warranty - Monthly".
|
|
263
|
+
|
|
223
264
|
## ⚠ SECURITY — pre-existing unscoped ACL grant undermines this feature's core guarantee
|
|
224
265
|
|
|
225
266
|
**Needs its own ticket. Found by reading the permission tables only — NOT exploited, NOT tested.**
|
|
@@ -327,6 +368,23 @@ feature rather than failing the page.
|
|
|
327
368
|
|
|
328
369
|
## Change history
|
|
329
370
|
|
|
371
|
+
- 2026-08-19 — TRUE-81049 (fix parked in that scheduled deployment; commit `a399145`, branch
|
|
372
|
+
TRUE-81049 in worker2 — **committed, not pushed**): closed the **silent skip of the AIG carrier
|
|
373
|
+
leg** in `handleSubscriptionDeactivation`. The old `if (… !empty($entitlement->c_aigContractId))`
|
|
374
|
+
had no `else`, so a missing identifier left coverage **active at the carrier** with no record
|
|
375
|
+
anywhere — found by a human, not a system. New `reportAigContractNotCancellable()` captures a
|
|
376
|
+
**non-throwing** `_Exception_Business('RATE_AIG_CONTRACT_CANCEL_MISSING_IDENTIFIER',
|
|
377
|
+
URGENCY_HIGH)` naming the missing column, inside `try/catch (Throwable)` so reporting can never
|
|
378
|
+
fail the job; precedent `worker2/Controller/Index.php:286`. Splitting the failure modes means the
|
|
379
|
+
**thrown** exception now means exactly one thing (carrier rejected a well-formed request) instead
|
|
380
|
+
of also firing on a blank identifier and aborting a valid deactivation. Gated by new
|
|
381
|
+
`isWholeHomeWarrantyEntitlement()` on the purchase guard's authority (title keyword `warranty` OR
|
|
382
|
+
`saleItemId 3`, checked on **both** the nested object and the flat FK, `is_object()`-guarded)
|
|
383
|
+
because `Items.id 1`/`2` are tech products with no carrier contract and the portal refuses
|
|
384
|
+
warranties — so tech is the dominant cancellation case and an ungated report would alert HIGH on
|
|
385
|
+
every routine cancel. See also the AIG doc: `/contract/cancel` also **requires
|
|
386
|
+
`contractNumber`**, so the carrier cancel had never succeeded regardless of the identifiers.
|
|
387
|
+
(mhammontree)
|
|
330
388
|
- 2026-08-06 — TRUE-80282 **testing & deploy**: **verified end-to-end on beta** (PayPal sandbox) —
|
|
331
389
|
`POST /v2/subscriptions/cancel` returned 200 with `dateCancel: 2026-08-27`, a real sandbox
|
|
332
390
|
agreement was cancelled, and `Subscriptions` id 84 confirmed `isActive = 1`, `dateEnd` untouched,
|
package/package.json
CHANGED