toga-ai 1.0.538 → 1.0.539

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.
@@ -0,0 +1,135 @@
1
+ ---
2
+ type: session
3
+ slug: TRUE-80575-rate-paypal-webhook
4
+ title: Rate PayPal webhook — Defect A fixed, Kwadwo backfilled + insured, handler still to build
5
+ author: mhammontree
6
+ repos: [worker2, _underscore, api2, toga2-view]
7
+ framework: "2.0"
8
+ client: rate
9
+ status: active
10
+ created: 2026-08-10
11
+ updated: 2026-08-10
12
+ ---
13
+
14
+ # Session: TRUE-80575-rate-paypal-webhook
15
+ **Date:** 2026-08-10
16
+ **Project/Repo:** worker2 (+ _underscore, api2, toga2-view) — 2.0
17
+ **Task:** Fix the Rate PayPal webhook (Defect A), decide and scope the missing server-side
18
+ entitlement creation (Defect B), and remediate the two customers who paid but had nothing recorded.
19
+
20
+ > Continues `2026-08-04-TRUE-80575-rate-paypal-webhook-mhammontree.md`. Read this one first —
21
+ > it corrects a conclusion in that file.
22
+
23
+ ---
24
+
25
+ ## What WORKED
26
+
27
+ - **Defect A fixed** (`worker2/Worker/Rate.php`, **written, NOT committed**, branch `TRUE-80575`):
28
+ deleted `isEventProcessed()` outright plus its call site; added `findPaymentByTransactionId()`
29
+ (GET /payments on `transactionIdentifier`) and a guard at the top of `handlePaymentCompleted()`
30
+ that returns success without writing if the sale is already recorded. `php -l` clean, no
31
+ dangling references. `DB_LOGS_RATE` deliberately kept — the outbound
32
+ `_ApiRequest::setLogging(true)` calls still need it.
33
+ - **Proved the idempotency check never worked** (this corrects the 2026-08-04 session):
34
+ `Logs_Rate.Api.source` is NULL on every row (95,204 on 2026-08-01; 696,272 over 7 days) and
35
+ `'PAYPAL'` appears in exactly one place in the non-vendor codebase — the query that *reads* it.
36
+ Nothing writes it, and inbound webhook payloads never reach that table. Deletion was correct;
37
+ optimising it would have produced a fast but still useless query.
38
+ - **Kwadwo Moore fully remediated and genuinely insured.** Entitlement 44 / `ET100018`,
39
+ Subscription 25, SalesOrder `SA100043`, Payment `0DY909616V1516622`, Address 10
40
+ (`isValidated=1`, zip auto-expanded to 20737-3049), contact address pinned, confirmation email
41
+ sent 16:48:27, and **AIG contract 1000046411721 / 46411721, status Active**.
42
+ - **Measured real webhook latency: 5–12 seconds** across 7 events on two days — not the 3–5
43
+ minutes assumed. Also proved event order is *not* guaranteed (`PAYMENT.SALE.COMPLETED` arrived
44
+ before `ACTIVATED` for I-7NF3HT1JNP96).
45
+ - **Found the recovery source for any future lost purchase:** `Core.WorkerJobs.parameters` holds
46
+ the complete raw PayPal payload, and `resource.custom_id` **is** the `Client_Rate.Users.uuid`.
47
+ - **Rosetta White closed** — both her charges were refunded, so no backfill was needed.
48
+
49
+ ## What did NOT work — DO NOT RETRY THESE
50
+
51
+ - **Raw SQL backfill.** Written and then abandoned before execution. Raw INSERTs never invoke
52
+ PHP, so `postPost` never runs: no AIG contract, no email. It produces a record that *looks*
53
+ like coverage while AIG has nothing on file. The SQL file still exists at
54
+ `test/@Mark/TRUE-80575-backfill-kwadwo-moore.sql` — **do not use it.**
55
+ - **First API attempt — 500 `EO-1` id `8K-6`.** Cause: the Postman request was in the AIG folder,
56
+ so an **AIG token** was used. It ran `_Model_Aig_Entitlement::prePost()` and died on
57
+ `Undefined property: stdClass::$partNumber`. Nothing was written.
58
+ - **Second attempt — same error, id `8K-18`.** Switching Postman *folders* does not change the
59
+ token; it lives in an environment variable and must be re-minted by re-running the auth
60
+ request with Rate credentials.
61
+ - **Third attempt — 201 Created, but AIG rejected the contract with HTTP 400:**
62
+ `errorCode 20 "Required field Phone Number is missing"` + `errorCode 76`. My payload omitted
63
+ `contactPhoneNumbers`; I had assumed the existing contact carried a phone. It did not —
64
+ `Users.phoneNumber` had it, but `Contacts`→`ContactPhoneNumbers` was empty, and
65
+ `Entitlement.php:564` reads only the latter. **The failure was silent**: the entitlement saved
66
+ and the customer was emailed a confirmation anyway.
67
+ - **Using `Core.WorkerJobs` as an idempotency ledger** — rejected with evidence. A
68
+ watchdog-killed job can still land its writes minutes later (job 560657 killed 07:58:18, its
69
+ `POST /v2/sales-orders` landed 08:00:10), so "no successful job" ≠ "not processed".
70
+ - **First `ContactPhoneNumbers` POST** created the row with the *contact's* uuid because the
71
+ payload carried a root-level `uuid`. api2 treats a root `uuid` as the new record's own
72
+ identifier. Fixed manually to `51fe286d-7c5d-4554-8021-59fe3a243375`.
73
+ - **Unbounded queries on `Logs_Rate.Api`** (8.7M rows) time out at 300s. Always bound by
74
+ `dtStamp` *and* filter `method`/`route`, or a bulk contact-sync floods the results.
75
+
76
+ ## Not tried yet (candidates for next session)
77
+
78
+ - Commit + PR the `worker2/Worker/Rate.php` fix.
79
+ - **toga2-view (rgirish97)** — persist the covered property before the PayPal handoff. This
80
+ **blocks** the ACTIVATED handler for warranty purchases. Also add the missing
81
+ `yearly_warranty` key to `PAYPAL_PLAN_IDS`.
82
+ - **The `BILLING.SUBSCRIPTION.ACTIVATED` handler** (worker2 + _underscore). Must restructure the
83
+ dispatch first — `findSubscriptionByToken()` currently runs before the event branch and throws
84
+ when the subscription is missing, which is the *normal* case for a new purchase. **Must include
85
+ `contactPhoneNumbers`** or every warranty hits the AIG rejection above.
86
+ - Make the webhook return HTTP 200 on failure paths (PayPal currently retries indefinitely).
87
+ - Audit the 5-of-9 Whole Home Warranty entitlements with no AIG contract — Mark explicitly
88
+ deferred this. Kimberly Stearns (ET100014) has both an address and a phone and still has none.
89
+ - Rotate the PayPal `client_secret` (plaintext in worker2/ and api2/ `Config/production.ini`).
90
+ - Clean up the 14 duplicate `Payments` rows from the 2026-06-15 double-run backfill.
91
+ - Re-enable webhook signature verification (endpoint is currently forgeable).
92
+
93
+ ## Current file state
94
+
95
+ | File | Status | Notes |
96
+ |------|--------|-------|
97
+ | `worker2/Worker/Rate.php` | **Modified, uncommitted** | Defect A fix. Branch `TRUE-80575`, +49/−27, `php -l` clean. |
98
+ | `test/@Mark/TRUE-80575-backfill-kwadwo-moore.sql` | Created, **not executed** | Raw-SQL fallback. Superseded — do not run. |
99
+ | `test/@Mark/TRUE-80575-backfill-kwadwo-moore-VIA-API.md` | Created, executed | The API route that worked. |
100
+ | `test/@Mark/TRUE-80575-kwadwo-AIG-contract-repair.md` | Created, executed | AIG repair runbook. |
101
+ | `test/@Mark/TRUE-80575-kwadwo-aig-repair.postman_collection.json` | Created | 4-request collection, tokens auto-captured. **Do not commit with credentials filled in.** |
102
+ | `_underscore`, `api2`, `toga2-view` | Untouched | Clean trees on `TRUE-80575`. |
103
+
104
+ ## Decisions made
105
+
106
+ - **Defect B → add a `BILLING.SUBSCRIPTION.ACTIVATED` handler.** The browser `onApprove` POST
107
+ stays the primary synchronous path; the webhook is a fallback net, not a replacement.
108
+ Rejected: hardening the frontend alone (leaves the hole open when the browser is lost).
109
+ - **No new tables (Rohan).** Idempotency uses natural keys — `Subscriptions.token` is *already*
110
+ `UNIQUE`, and `Payments` is 36 rows so an unindexed `transactionIdentifier` lookup is free.
111
+ The `PaypalWebhookEvents` table is deferred to the leads. Rejected: `Core.WorkerJobs` as a
112
+ ledger (see failures above).
113
+ - **Interim-only Defect A fix**, then superseded: verification proved the method was dead, so it
114
+ was deleted rather than `dtStamp`-bounded. Better outcome than planned.
115
+ - **API backfill over raw SQL** — only the model layer creates the AIG contract.
116
+ - **`coverageStartDate` = 2026-08-01** (purchase date) rather than the code's today-fallback.
117
+ - **No backfill for Rosetta White** — both charges refunded.
118
+
119
+ ## Blockers
120
+
121
+ - **toga2-view blocks the ACTIVATED handler** for warranty purchases: the covered-property
122
+ address is in no PayPal payload and is not persisted before payment, so the WH guard will
123
+ reject every server-side create until that lands.
124
+ - Non-prod worker2 is **not SQS-driven**, so the webhook path cannot be tested end-to-end on
125
+ beta. Invoke `Rate/Webhook` directly with a captured payload
126
+ (`Core.WorkerJobs` 563520 / 563544 / 332144 / 332157).
127
+
128
+ ## Exact next step
129
+
130
+ > Commit `worker2/Worker/Rate.php` on branch `TRUE-80575` (`TRUE-80575: <desc>` title, detail in
131
+ > body) and open the PR — the Defect A fix is finished and verified but has been sitting
132
+ > uncommitted in the working tree.
133
+
134
+ ---
135
+ _Saved by /session-save on 2026-08-10_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.538",
3
+ "version": "1.0.539",
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",