toga-ai 1.0.538 → 1.0.540

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_
@@ -0,0 +1,82 @@
1
+ ---
2
+ type: session
3
+ slug: elite-supply-fe
4
+ title: Elite Service Requests (Orders) + Inventory FE in toga2-supply — code complete, blocked on backend Page.php context-setting fatal
5
+ author: tcox
6
+ repos: [toga2-supply]
7
+ framework: "2.0"
8
+ client: elite
9
+ status: active
10
+ created: 2026-08-10
11
+ updated: 2026-08-10
12
+ ---
13
+
14
+ # Session: elite-supply-fe
15
+ **Date:** 2026-08-10
16
+ **Project/Repo:** toga2-supply (2.0)
17
+ **Task:** Elite's full supply2 FE surface — Service Requests page (retitled Orders page on the new `service-requests` backend view) + details modal + the previously built Inventory page — code complete and iteratively debugged against beta; currently blocked by a backend Page.php fatal that the new service-requests context-setting triggers on the inventory page.
18
+
19
+ > Supersedes session `elite-inventory-fe` (2026-08-06). All Inventory findings there still stand.
20
+
21
+ ---
22
+
23
+ ## What WORKED
24
+ - **"Service Requests" is NOT a new page.** The mockup source (`TOGa Supply mockup Elite/elite/elite-app.jsx`: `VIEW_META.Orders.title = "Service Requests"`, nav `{label:"Orders", text:"Service Requests"}`) defines it as the Elite Orders view retitled; the "details modal" is a details-only order modal (`elite-modal.jsx`, titles itself "Order INC…"). Implemented as config on the existing OrdersPage — no new route.
25
+ - **Sales Orders / Service Requests FE (all tsc-clean, uncommitted on local `_production`):**
26
+ - `src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json` (new): `pageTitle: "Service Requests"`, `tableSlug: "service-requests"`, `listingSlug: "sales-orders-listing"`, `hasQuoteFilter: false`, `approvalsButton.isEnabled: false`, `exportButton.isEnabled: true`, NO `approvalActionOptions`.
27
+ - `useOrdersPageViewModel.ts`: explicit `ELITE: {base: BASE_ELITE}` + `if (hostName === "ELITE") return hostFields.base;` (PRUDENTIAL precedent). L63 `|| fieldMapping.compass` fallback untouched.
28
+ - `OrdersPage.tsx`: all 8 hardcoded `sales-orders` references now `fields?.tableSlug || "sales-orders"` (listing meta, tableDisplayData, setSlug, ResetFilters, both providers); ACL check optional-chained + slug-keyed with a TS cast; `pageTitle` initialized from `fields?.pageTitle || "Orders"`; `isQuote: false` in additionalData is now conditional — omitted when `fields?.hasQuoteFilter === false` (backend confirmed FE-side fix: ServiceRequests has no isQuote column).
29
+ - `SlideMenu.tsx` ELITE branch: "Service Requests" (link `/`) + Inventory deep-link.
30
+ - `renderModalContent.tsx` untouched — DEFAULT routes ELITE row-clicks to OrderDetailsModal only (verified: no ELITE key, no ApprovalsModal).
31
+ - **Review fixes (CodeRabbit) on Inventory work:** `InventoryPage.tsx` re-seeds `selectedGrouping` when the stored uuid isn't in the resolved host's array (corrupt JSON handled); `InventoryRouter.tsx` redirects `/inventory_items` → `/inventory` for hosts whose default view lacks `subTableParent` (data-driven, no host literal). Both no-ops for NYCHH/Compass/Quad.
32
+ - **`convertConstructorColumnTitles.ts`** null-guarded — a page slug without backend meta degrades to "" labels instead of crashing the table (was the white-screen when `service-requests-listing` didn't exist).
33
+ - **Post-merge diagnostics (2026-08-07 prod→beta refresh rebuilt Client_Elite at 11:55 + beta API redeploy) — root causes found via DB reads:**
34
+ 1. Login failure → re-seeded devteam user had an EMPTY password → fixed by hash copy from `Client_Compass.Users` id 127046 (login verified working after).
35
+ 2. `service-requests` table view wiped by merge → backend recreated it (ServiceRequests-record view joined to Tickets/Customers/ServiceRequestTypes/SalesOrders with Elite-tailored status CASE).
36
+ 3. Service Requests table 500 → OUR `isQuote: false` filter against ServiceRequests → fixed FE-side (above), backend confirmed.
37
+ 4. Inventory `/pages/meta?slug=inventory` 500 "Undefined array key 190" → fully diagnosed (see Blockers).
38
+
39
+ ## What did NOT work — DO NOT RETRY THESE
40
+ - **`listingSlug: "service-requests-listing"`** — no such page exists: `Core.Pages` has only `sales-orders-listing` (id 2) etc. The null pageMeta then crashed `convertConstructorColumnTitles` (`Cannot read properties of null (reading 'settings')`). Reverted to `sales-orders-listing`; if backend ever creates a service-requests page, it's a one-line JSON change.
41
+ - **Assuming the service-requests view is a renamed SalesOrders view** — it's a ServiceRequests-record view (`SalesOrders.serviceRequestId = ServiceRequests.id` 1:1). Consequence: table rows carry ServiceRequests uuids; the details modal fetches by uuid via sales-orders record routes → probable uuid mismatch on row click (untested — see Not tried).
42
+ - **Theory: empty `ClientPageSettings` caused the inventory meta 500** — WRONG: NYCHH's is empty too; that table (and `Pages_DefaultTableViewGroups`/`TableViewGroups`) is unused everywhere. The real cause is the context-setting fatal (Blockers).
43
+ - **Sending `isQuote` unconditionally in OrdersPage additionalData** — 1054 "Unknown column 'ServiceRequests.isQuote'". It's a SalesOrders-only column.
44
+ - (Carried from previous session) `/login` redirect ≠ Domains row exists; devteam lives only in dev-sandbox `Client_Compass` (127046); MCP env `client-beta` exposes no schemas — beta API client DBs are on the **dev-sandbox** cluster.
45
+
46
+ ## Not tried yet (candidates for next session)
47
+ - **Row-click details modal on Service Requests** — expect uuid mismatch (ServiceRequests uuid vs sales-orders fetch routes). Fix is either backend (expose linked `SalesOrders.uuid` as the drill identifier) or FE (ELITE detail `apiFields` pointing at service-requests routes in `useOrderDetailsViewModel` — structure already supports per-host apiFields).
48
+ - Inventory nested-expand re-validation (blocked again post-merge: prod's stale TableViewJoins were re-imported; the 3-statement fix from the drift doc applies but join ROW IDS may have shifted — match by tableViewId + joinRecordId + joinOnRecordFieldId, not row id).
49
+ - Four-client regression pass (NYCHH/ENDEAVORHEALTH/COMPASS/COMPASSCANADA, same browser) before PR.
50
+ - Branch + PR (tcox branches personally; bare ticket-ID name; ask first).
51
+ - gitignore check: `TOGa Supply mockup Elite/` folder sits untracked in the repo root — must not ride into the PR.
52
+
53
+ ## Current file state
54
+ | File | Status | Notes |
55
+ |------|--------|-------|
56
+ | src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json | new, uncommitted | full ELITE page config (see What WORKED) |
57
+ | src/pages/Orders/viewModel/index.ts | modified, uncommitted | BASE_ELITE export |
58
+ | src/pages/Orders/viewModel/useOrdersPageViewModel.ts | modified, uncommitted | ELITE fieldMapping entry + base branch |
59
+ | src/pages/Orders/OrdersPage.tsx | modified, uncommitted | slug threading, pageTitle from fields, conditional isQuote, null-safe ACL |
60
+ | src/components/layout/SlideMenu/SlideMenu.tsx | modified, uncommitted | ELITE: Service Requests + Inventory |
61
+ | src/utils/convertConstructorColumnTitles.ts | modified, uncommitted | null-guarded pageMeta/settings |
62
+ | src/pages/Inventory/** (7 files, from elite-inventory-fe session) | modified/new, uncommitted | host-scoped group options etc. + CodeRabbit fixes (selectedGrouping re-seed, InventoryRouter guard) |
63
+ | src/utils/resolveClientHostName.ts | new, uncommitted | store-first + subdomain fallback |
64
+ | package.json | modified, uncommitted | `npm run elite` script |
65
+ | dev-sandbox Client_Elite.Users | hand-fixed, KEPT | devteam password restored via Compass hash copy |
66
+
67
+ ## Decisions made
68
+ - Service Requests = config on OrdersPage (pageTitle/tableSlug/listingSlug/hasQuoteFilter in FIELDS), NOT a new route — mockup defines it as the Orders view retitled; supply2.0 components render it (2.5 mockup = spec for WHAT, 2.0 = HOW).
69
+ - Slug threading via FIELDS with `|| "sales-orders"` fallbacks — other hosts have no tableSlug key, provably unchanged. Rejected: fixing the broken lowercase-`compass` fallback (cross-client risk).
70
+ - Reuse `sales-orders-listing` page meta for Elite until a service-requests page exists (headers come from the table-view meta, so only ACL/search-labels are affected).
71
+ - Keep devteam seeding + document every hand-fix — the 08-07 merge proved hand-inserted beta rows get eaten; everything must eventually be dbchanges2 migrations.
72
+
73
+ ## Blockers
74
+ - **Backend: `/pages/meta?slug=inventory` fatal "Undefined array key 190"** — `Client_Elite.ClientRecordFieldSettings` row id 1 (field 58 → label "Total" in context of field 190 = ServiceRequests.id, created for the new service-requests view) is fetched for ANY page whose field set includes field 58; `_underscore/Model/Core/Page.php:824` resolves `contextRecordFieldId` against a lookup built only from the current page's fields → key 190 missing on inventory → warning made fatal by the newly deployed strict error handler. Fix: isset()-guard Page.php ~L822-829 (skip context settings whose context field isn't on the page). Deleting the row would un-break inventory but lose the service-requests "Total" label. Reported to backend 2026-08-07.
75
+ - Inventory nested-expand still blocked by the re-imported TableViewJoins drift (see drift doc `clients/elite/features/supply2-tableview-config-drift.md`).
76
+ - Details-modal uuid mismatch (suspected, untested) — see Not tried.
77
+
78
+ ## Exact next step
79
+ > Confirm with backend that the Page.php:824 isset() guard (or equivalent) is deployed, then `npm run elite`, log in as devteam, and verify BOTH pages render: Inventory (columns + chevron) and Service Requests (table loads without isQuote error). Then click a Service Request row and capture the modal's network call — the expected failure is the ServiceRequests-uuid-vs-sales-orders-route mismatch.
80
+
81
+ ---
82
+ _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.540",
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",