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