toga-ai 1.0.633 → 1.0.635

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.
@@ -5,7 +5,7 @@
5
5
  | [Worker (1.0 Framework) Architecture](architecture.md) | `worker` is the legacy (**1.0** `App_` framework) **background-job tier**. | worker/index.php, worker/_/app/framework.php, worker/crons/, worker/schedules/, worker/ebs/cron.worker.php, worker/.ebextensions/035_cron.worker.config, worker/crons/infrastructure/execute_dbchanges.php, worker/.ebextensions/030_dbchanges.config |
6
6
  | [Compass MA Sales Order Exception Report](features/compass-ma-sales-order-exception-report.md) | A worker cron that emails operations the "Compass Refresh Exception Report" — Compass `MA%` sales orders whose corresponding Office Depot (ODP) sales order has | worker/crons/toga2/compass/workflow/7_generate_ma_sales_order_exception_report.php |
7
7
  | [Compass Manager Approval Reminder Emails (1.0 worker crons)](features/compass-manager-approval-reminder-emails.md) | Two 1.0 worker crons nag approvers about sales orders still waiting on a decision — one per Compass tenant. | worker/crons/toga2/compass/compass_email_reminders.php, worker/crons/toga2/compasscanada/compass_email_reminders.php, worker/schedules/cron.worker.sync.json, worker1.5/crons/toga2/compass/compass_email_reminders.php, worker1.5/schedules/cron.worker.json |
8
- | [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php, worker/crons/toga2/compasscanada/send_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, library/app/client/compasscanada.php |
8
+ | [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compass/send_delivered_email.php, worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php, worker/crons/toga2/compasscanada/send_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, library/app/client/compasscanada.php |
9
9
  | [Elite TOGA 2.0 → TOGaDeskSupport Standalone Attachment Sync](features/elite-togadesk-attachment-sync.md) | `sync_togadesk_elite_attachments.php` is a standalone cron (every 5 minutes) that syncs file attachments from TOGA 2.0 into TOGaDeskSupport for Elite. | worker/crons/toga2/elite/sync_togadesk_elite_attachments.php, worker/crons/toga2/elite/test_sync_togadesk_elite_attachments.php |
10
10
  | [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, worker2/Component/Forecast/SaleImport/SaleImport.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/reconcile_drift_2023plus.php, test/@dave/probe_invoice_gap_2026.php, test/@dave/probe_creditmemo_gap_detail.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
11
11
  | [NetSuite Sales Order Sales Rep Sourcing (Staples & ODP EDI orders)](features/netsuite-sales-order-sales-rep-sourcing.md) | How the **sales rep** on a NetSuite Sales Order is determined for the two 1.0 `worker` EDI order-creation integrations (Staples cXML and Compass/ODP EDI). | worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/crons/sync/staples/sync_staples_cxml.php, test/@Mark/NetSuite/TRUE_80451_customer_salesrep_diag.php |
@@ -6,10 +6,11 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-03
9
+ updated: 2026-08-24
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/update_salesorder_status_from_odp.php
13
+ - worker/crons/toga2/compass/send_delivered_email.php
13
14
  - worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php
14
15
  - worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php
15
16
  - worker/crons/toga2/compasscanada/send_delivered_email.php
@@ -133,9 +134,26 @@ the tracking number as emailed (it retries next run).
133
134
  - **Pick the template from the language of the person actually being emailed.** The manager-approval
134
135
  reminder cron (`compass_email_reminders.php`) selected the template from the **requester's**
135
136
  language even though the mail goes to the **manager**. Fixed 2026-08-03.
137
+ - **⚠ Conflict with the new 2.0 centralized tracking-status refresh.** The delivered-email cron
138
+ (`worker/crons/toga2/compass/send_delivered_email.php`) polls UPS itself, sends the delivered
139
+ template (uuid `fe961c7c-277b-4a89-be1d-3d318d8c7718`) via api2 `GET /email-templates/sendEmail`,
140
+ and writes `TrackingNumbers.status` with a **direct DB UPDATE** guarded by
141
+ `WHERE status <> 'DELIVERED'`. The new platform-wide
142
+ [tracking-status refresh (worker2)](../../../2.0/apps/worker2/features/tracking-status-refresh.md)
143
+ writes `status` via api2 PUT for **all** clients. If the refresh marks a Compass row `DELIVERED`
144
+ **first**, this cron's guard **skips** it and the delivered email **never sends** — a real conflict
145
+ to resolve before the refresh is enabled for Compass. There is **no `postPut` interceptor** on
146
+ tracking-numbers today; the plan (not built) is to move this email logic into a Compass `postPut`
147
+ interceptor and retire this 1.0 cron.
136
148
 
137
149
  ## Change history
138
150
 
151
+ - 2026-08-24 — Recorded the **conflict with the new 2.0 platform-wide tracking-status refresh**: that
152
+ refresh writes `TrackingNumbers.status` via api2 PUT for all clients, and this cron's delivered
153
+ UPDATE is guarded by `WHERE status <> 'DELIVERED'` — so if the refresh marks a Compass row
154
+ `DELIVERED` first, the delivered email never sends. Noted there is no `postPut` interceptor on
155
+ tracking-numbers and the plan to move this email logic into a Compass `postPut` interceptor and
156
+ retire this cron. Added `send_delivered_email.php` to the file list. (bala)
139
157
  - 2026-08-03 — Compass Canada **item rows are now localized**: titles resolved from the
140
158
  `ItemTranslations` sidecar (two-column select + PHP fallback, never a mixed-collation SQL
141
159
  `COALESCE`) and translated `N/P` / `Qté` labels; language resolution moved onto the shared 1.0
@@ -11,7 +11,7 @@
11
11
  | [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
12
12
  | [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
13
13
  | [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent | _underscore/Model.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/ServiceRequest.php, _underscore/Model/Elite/SalesOrder.php, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql |
14
- | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | test/@Mark/true-80824-fedex-inflate-test.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
14
+ | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | test/@Mark/true-80824-fedex-inflate-test.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
15
15
  | [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | A throwaway CLI script (a data check, a backfill dry-run, a render harness) that wants the real 2.0 framework — `_Model`, `_Query`, `_Database` — is **not** the | _underscore/Database.php, _underscore/Environment.php, api2/Initialize.php |
16
16
  | [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
17
17
  | [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP | _underscore/Loader.php, worker2/_.php, api2/_.php, worker2/Component/Forecast/Db/Db.php, worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Api/Oneuptime/Oneuptime.php, api2/Component/Api/Netsuite/Netsuite.php, _underscore/Component/Api/Paypal/Paypal.php |
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-11
10
- owners: [mhammontree, tcox]
9
+ updated: 2026-08-24
10
+ owners: [mhammontree, tcox, bala]
11
11
  files:
12
12
  - test/@Mark/true-80824-fedex-inflate-test.php
13
13
  - dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql
@@ -18,6 +18,7 @@ files:
18
18
  - _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php
19
19
  - _underscore/Component/Library/Carriers/Ups/Ups.php
20
20
  - _underscore/Component/Library/Carriers/Fedex/Fedex.php
21
+ - _underscore/Component/Library/Carriers/Usps/Usps.php
21
22
  - _underscore/Trait/Netsuite/ItemFulfillment.php
22
23
  - _underscore/Trait/Netsuite/SalesOrder.php
23
24
  - _underscore/Component/Library/NetSuite/NetSuite.php
@@ -275,6 +276,26 @@ including **4 fuzz-discovered `fp:` cases whose only job is to fail if the UTF-8
275
276
  class for **every** integration, but that is the framework's shared HTTP client and needs its own
276
277
  review. **The UPS client has the same latent gap.**
277
278
 
279
+ ## Batch tracking-status polling — `getStatuses()` (implemented 2026-08-24)
280
+
281
+ Alongside the single-shipment `getStatus()`, the carrier libs gained **batch** status lookups to
282
+ support the platform-wide
283
+ [tracking-status refresh](../../worker2/features/tracking-status-refresh.md):
284
+
285
+ - `_Component_Library_Carriers_Fedex::getStatuses(array)` (~**30 numbers/call**) and
286
+ `_Component_Library_Carriers_Usps::getStatuses(array)` (~**10 numbers/call**) reuse the **same
287
+ parsers** as `getStatus()` via extracted private helpers `statusFromFedexTrackResult()` /
288
+ `statusFromUspsDetail()`. Results are keyed by the tracking number the carrier echoes back.
289
+ - **UPS has no batch** — it stays single-call (`getStatus()`); the refresh loops it per number.
290
+
291
+ ### ⚠ UPS `getStatus()` gained `$forceNoLogging` (default `false`) — do not flip the shipment callers
292
+ Bulk tracking polls set **`$forceNoLogging = true`** so UPS request logging is suppressed. With prod
293
+ `[database]` set, an unsuppressed poll logs **every** call → log bloat **and** an
294
+ `"Exactly 1 row expected"` throw. `$forceNoLogging` is also **propagated into `getOAuthToken()`**, so
295
+ an OAuth cache-miss during a no-log poll does not leave the token request logged. **Shipment and
296
+ address-validation callers keep the default (logging ON)** — only the bulk tracking poll passes
297
+ `true`.
298
+
278
299
  ## `FIELD_STORAGE` mechanics (reference)
279
300
 
280
301
  > **⚠ `TrackingNumbers.labelPdfFile` has NO physical column in any `Client_*` schema in any
@@ -423,6 +444,14 @@ still discards the unsaved in-memory value.
423
444
  bill) — expect a hard error rather than a silently-wrong bill in those cases.
424
445
 
425
446
  ## Change history
447
+ - 2026-08-24 — Added **batch tracking-status polling** to the carrier libs for the platform-wide
448
+ [tracking-status refresh](../../worker2/features/tracking-status-refresh.md):
449
+ `Fedex::getStatuses()` (~30/call) and `Usps::getStatuses()` (~10/call), reusing the single-shipment
450
+ parsers via extracted `statusFromFedexTrackResult()` / `statusFromUspsDetail()` helpers (UPS stays
451
+ single-call, no batch). Gave UPS `getStatus()` a `$forceNoLogging` param (default false, also
452
+ propagated into `getOAuthToken()`) so bulk polls suppress request logging — an unsuppressed poll on
453
+ prod `[database]` bloats logs and throws `"Exactly 1 row expected"`; shipment + address-validation
454
+ callers keep logging on. (bala)
426
455
  - 2026-08-11 — Flagged that `TrackingNumbers.labelPdfFile` has **no physical column in any
427
456
  `Client_*` schema in any environment** (only the Core `RecordFields` metadata was ever inserted),
428
457
  so `GET /tracking-numbers` 500s once a query returns rows — storage fields hydrate lazily per row,
@@ -9,4 +9,5 @@
9
9
  | [/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 |
10
10
  | [Reference-data dropdowns are sorted in the viewModel, not in genericApi](features/reference-dropdown-sorting.md) | Reference lists (states, and any other `getData("<record>", [...])` lookup) come back from api2 in **arbitrary API order** — there is no implicit alphabetical g | toga2-view/src/pages/ZipValidation/viewModels/useZipValidationViewModel.ts, toga2-view/src/api/genericApi.ts |
11
11
  | [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 |
12
+ | [Terms & Conditions page — verbatim vendor text, regeneration + verification](features/terms-and-conditions-page.md) | The Terms & Conditions pages render long **vendor-supplied legal documents** held as single TypeScript string constants: `TERMS_HW.ts` (home warranty) and `TERM | toga2-view/src/pages/TermsAndConditions/viewModel/DUMMYFIELDS/TERMS_HW.ts, toga2-view/src/pages/TermsAndConditions/viewModel/DUMMYFIELDS/TERMS.ts, toga2-view/src/pages/TermsAndConditions/view/components/HomeWarrantyTerms.tsx, toga2-view/src/pages/TermsAndConditions/view/components/TechSupportTerms.tsx, toga2-view/src/pages/TermsAndConditions/view/HomeWarrantyTermsPage.tsx |
12
13
  | [ZipValidation — contact phone is a prefill default, never a lock](features/zip-validation-contact-phone.md) | The `ZipValidation` page collects the covered-property address and a **contact phone number** as part of the purchase / address-validation entry flow (the front | toga2-view/src/pages/ZipValidation/view/ZipValidation.tsx |
@@ -0,0 +1,92 @@
1
+ ---
2
+ title: "Terms & Conditions page — verbatim vendor text, regeneration + verification"
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-08-21
10
+ owners: [mhammontree]
11
+ files:
12
+ - toga2-view/src/pages/TermsAndConditions/viewModel/DUMMYFIELDS/TERMS_HW.ts
13
+ - toga2-view/src/pages/TermsAndConditions/viewModel/DUMMYFIELDS/TERMS.ts
14
+ - toga2-view/src/pages/TermsAndConditions/view/components/HomeWarrantyTerms.tsx
15
+ - toga2-view/src/pages/TermsAndConditions/view/components/TechSupportTerms.tsx
16
+ - toga2-view/src/pages/TermsAndConditions/view/HomeWarrantyTermsPage.tsx
17
+ related:
18
+ - ../architecture.md
19
+ - ../../../../clients/rate/features/home-warranty-terms-content.md
20
+ - ../../../../clients/rate/features/checkout-terms-delivery-preference.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ The Terms & Conditions pages render long **vendor-supplied legal documents** held as single
26
+ TypeScript string constants: `TERMS_HW.ts` (home warranty) and `TERMS.ts` (tech support). The
27
+ same `TermsAndConditions` component serves **two render paths** — the public
28
+ `/terms/home-warranty` route (registered **outside** `PrivateRoute` so emailed links work
29
+ without a login) and the in-checkout terms modal — via
30
+ `TermsAndConditions isTechSupport={false}`.
31
+
32
+ **These strings are transcripts, not content we author.** They must reproduce the vendor's
33
+ published form byte-for-byte, including its errors. Never hand-edit them; regenerate them from
34
+ the source document with the pipeline below. Client-specific provenance and the policy rationale
35
+ live in [Rate home-warranty terms content](../../../../clients/rate/features/home-warranty-terms-content.md).
36
+
37
+ ## How it works — regenerating a terms file from a vendor document
38
+
39
+ Do **not** hand-type or hand-patch a long legal document. On TRUE-80826, ~21 substantive changes
40
+ were scattered through 10 pages of highly repetitive clause language (six near-identical state
41
+ cancellation paragraphs) across 11,520 words. Targeted edits mis-anchor in text like that, and a
42
+ mis-anchored edit inside a contract is exactly what human review does not catch. Replace the file
43
+ wholesale, by script:
44
+
45
+ 1. **Extract:** `pdftotext -layout -enc UTF-8 <pdf> out.txt`.
46
+ 2. **Normalize to paragraphs:** strip the running page footer; rejoin lines wrapped across page
47
+ breaks; and **rejoin end-of-line hyphenation with NO separator** — a line ending in `-` is a
48
+ wrapped word, and joining with a space invents text the vendor never wrote (this actually
49
+ happened: `all-` + `inclusive` became `all- inclusive`).
50
+ 3. **Generate the `.ts` by script.** Escape with `json.dumps(ensure_ascii=False)` so quotes and
51
+ backslashes are safe while the document's curly quotes stay literal. Emit wrapped source lines
52
+ each keeping a **trailing space except the last**, so concatenation reproduces the text exactly.
53
+ 4. **Round-trip verify.** Re-evaluate the generated module as CommonJS, render the string, and diff
54
+ it against the normalized source. On TRUE-80826 this was an **exact match** — machine proof of
55
+ faithful transcription, replacing "someone read all 10 pages." Shim for step 4:
56
+
57
+ ```
58
+ sed -e '1s/^\xef\xbb\xbf//' -e 's/^const <NAME> =/module.exports =/' -e '/^export default/d'
59
+ ```
60
+
61
+ 5. **Cross-validate** against a second independent rendering of the same form (e.g. the vendor's
62
+ `.docx`) — see the client doc for what agreement/disagreement means.
63
+
64
+ ## Gotchas / known issues
65
+
66
+ - **The BOM strip in the sed shim is load-bearing.** The pre-TRUE-80826 `TERMS_HW.ts` began with a
67
+ UTF-8 BOM, which defeats a `^const` anchor and silently yields a no-op shim that "verifies" nothing.
68
+ The regenerated file no longer carries a BOM (harmless for Vite/TS).
69
+ - **`pdftotext` on a TOGA dev box does not default to UTF-8.** Output is cp1252/latin-1 and blows up
70
+ as `UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb7`. Always pass `-enc UTF-8`; without it
71
+ curly quotes and bullets are silently mangled. Post-fix extraction carried 61 proper quote pairs
72
+ where the old hand-built file had only 15, mixed straight and curly.
73
+ - Also on this box: `grep -P` is unavailable ("supports only unibyte and UTF-8 locales"), and node
74
+ cannot `require()` an MSYS `/c/...` path — use the `C:/...` form.
75
+ - **OPEN DEFECT — the terms render as one unbroken wall of text.** The strings separate paragraphs
76
+ with `\n\n`, but nothing in the render chain sets `whitespace-pre-line` / `pre-wrap`, so every
77
+ newline collapses in HTML. Affects **both** render paths (public route and checkout modal), since
78
+ they share the same component. Pre-existing (the old file had the same `\n\n` and the same
79
+ components), so it was deliberately left out of TRUE-80826's scope. Likely a one-line fix — add
80
+ `whitespace-pre-line` to the wrapper — and **needs its own ticket**. Tech Support terms
81
+ (`TERMS.ts`, a separate 537-line document) presumably shares the defect via `TechSupportTerms.tsx`;
82
+ unverified.
83
+ - Typecheck these changes with `npx tsc -p tsconfig.app.json --noEmit` and compare against the
84
+ pre-change baseline (72 pre-existing errors as of 2026-08-21). The root `tsconfig.json` is
85
+ references-only and gives a silent false green — see the repo architecture doc.
86
+
87
+ ## Change history
88
+
89
+ - 2026-08-21 — TRUE-80826: established the scripted-transcription + round-trip-verification pipeline
90
+ while replacing `TERMS_HW.ts` wholesale (634 insertions / 567 deletions) with Rate's new published
91
+ form. Recorded the `pdftotext -enc UTF-8`, hyphenation-rejoin, and BOM/sed gotchas, and logged the
92
+ pre-existing `whitespace-pre-line` render defect for a separate ticket. (mhammontree)
@@ -44,6 +44,7 @@
44
44
  | [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | > **DB-DRIVEN AI-MODEL ROUTING (2026-07-29).** Which knowledge base a transcript is cleaned > into is now decided by the **meeting organizer's "home" AI model** | worker2/Worker/Team/Transcripts.php, worker2/bin/sync-knowledge-bases.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Client_True/2026-07-27a - TranscriptAiModelRoutingColumns.sql, dbchanges2/Client_True/2026-07-27b - TranscriptAiModelRoutingData.sql, dbchanges2/Team/2026-07-27a - TranscriptVocabularyAiModelScope.sql, dbchanges2/Team/2026-07-29a - TranscriptVocabularyBackfillAllModels.sql, dbchanges2/Team/2026-06-30a, dbchanges2/Team/2026-06-30b, dbchanges2/Team/2026-06-30c, dbchanges2/Team/2026-06-30d, dbchanges2/Team/2026-06-30e, dbchanges2/Core/2026-06-30a, dbchanges2/Core/2026-07-02a, dbchanges2/Team/2026-07-02a, dbchanges2/Team/2026-07-08a, dbchanges2/Team/2026-07-09a, dbchanges2/Team/2026-07-10a, dbchanges2/Team/2026-07-28a - TranscriptProcessingRetryAttempts.sql, dbchanges2/Team/2026-07-28b - TranscriptPromptTemplateConverseModel.sql, dbchanges2/Core/2026-07-28a - TeamsTranscriptRetryCron.sql |
45
45
  | [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` (file `Worker/Team/Sprint.php`) is the engine behind TOGA's internal **development-sprint process and reporting**. | worker2/Worker/Team/Sprint.php, _underscore/Model/Team/Sprint.php, dbchanges2/Core/CronJobs (SprintLockScheduled seed) |
46
46
  | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | > **SUPERSEDED (2026-07-09) — the S3-staging model below is history.** `Export` is now a thin > **GRAPH-DIRECT** cron poller: it no longer archives raw VTT to ` | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
47
+ | [Centralized Tracking-Status Refresh (worker2 Sync cron, all clients)](features/tracking-status-refresh.md) | A single platform-wide cron keeps `TrackingNumbers.status` current for **every provisioned client** until each shipment reaches a terminal state, by polling Fed | worker2/Worker/Sync/TrackingNumbers.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-07-30 - TrackingNumbers_RefreshColumns.sql, dbchanges2/Client/2026-07-30 - Acl_TrackingStatus_Grant.sql, dbchanges2/Client_Tdsynnex/2026-07-30 - Apis_TdsynnexKey.sql, dbchanges2/Core/2026-07-30 - TrackingRefresh_Cron.sql |
47
48
  | [VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)](features/vapi-webhook-handler.md) | `_Worker_Vapi` ([worker2/Worker/Vapi.php](worker2/Worker/Vapi.php)) is the **PHP side of the AI-BDR call loop** — the webhook that receives VAPI's end-of-call r | worker2/Worker/Vapi.php, worker2/Worker/Ai/Bdr/Vapi.php, worker2/Controller/Index.php |
48
49
  | [WJE Freshservice Sync (worker2)](features/wje-freshservice-sync.md) | WJE ("WJE IT", helpdesk `wje.freshservice.com`) is a **Freshservice**-based help-desk client whose tickets, contacts, assets, groups, categories, and canned res | worker2/Worker/Wje.php, _underscore/Component/Api/Wje/Wje.php, _underscore/Model/Wje/Ticket.php, _underscore/Model/Wje/TicketNote.php, _underscore/Model/Wje/Contact.php, _underscore/Model/Wje/Unit.php, _underscore/Model/Wje/TicketTeam.php, _underscore/Model/Wje/TicketCategory.php, _underscore/Model/Wje/AssetType.php, _underscore/Model/Wje/PredefinedReply.php, library/app/api/wje.php, worker/crons/toga2/wje/import_supporting_records.php, worker/crons/toga2/wje/sync_togasupply_wje.php, worker/crons/notifications/reports/wje/wje_common.php, library/app/systemmonitor/wje.php, dbchanges2/Client_Wje/2024-10-04 - WjeOnboarding.sql |
49
50
  | [PHP Runtime Upgrade on Elastic Beanstalk (worker2 8.3 → 8.5 + PhpSpreadsheet 1.x → 3.x)](workflows/php-runtime-upgrade-dependency-audit.md) | The procedure used to move worker2 from **PHP 8.3 to PHP 8.5** on Elastic Beanstalk, and the dependency work that had to land first. | worker2/composer.json, worker2/composer.lock, worker2/Worker/Team/Sprint.php, worker2/Worker/Client/TowFoundation/ProcessReceipts.php, worker2/Worker/Forecast/Import.php |
@@ -0,0 +1,208 @@
1
+ ---
2
+ title: Centralized Tracking-Status Refresh (worker2 Sync cron, all clients)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-24
10
+ owners: ["bala"]
11
+ files:
12
+ - worker2/Worker/Sync/TrackingNumbers.php
13
+ - _underscore/Component/Library/Carriers/Fedex/Fedex.php
14
+ - _underscore/Component/Library/Carriers/Usps/Usps.php
15
+ - _underscore/Component/Library/Carriers/Ups/Ups.php
16
+ - _underscore/Model/Client/TrackingNumber.php
17
+ - dbchanges2/Client/2026-07-30 - TrackingNumbers_RefreshColumns.sql
18
+ - dbchanges2/Client/2026-07-30 - Acl_TrackingStatus_Grant.sql
19
+ - dbchanges2/Client_Tdsynnex/2026-07-30 - Apis_TdsynnexKey.sql
20
+ - dbchanges2/Core/2026-07-30 - TrackingRefresh_Cron.sql
21
+ related:
22
+ - ../architecture.md
23
+ - ../workflows/php-runtime-upgrade-dependency-audit.md
24
+ - ../workflows/running-worker2-locally.md
25
+ - ../../_underscore/features/carrier-shipping-labels.md
26
+ - ../../_underscore/features/tracking-number-bridges.md
27
+ - ../../api2/features/api-payload-interceptors.md
28
+ - ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
29
+ ---
30
+
31
+ ## Summary
32
+
33
+ A single platform-wide cron keeps `TrackingNumbers.status` current for **every provisioned
34
+ client** until each shipment reaches a terminal state, by polling FedEx/USPS/UPS and writing the
35
+ mapped status back through **api2 PUT**. There is **no hardcoded client list** and **no new table**:
36
+ the working set is bounded, carrier load is decoupled from cron frequency by a per-row re-check
37
+ cursor, and concurrency/observability ride the existing per-client `Parameters` key/value table.
38
+
39
+ The abstract worker class is `_Worker_Sync_TrackingNumbers` in
40
+ `worker2/Worker/Sync/TrackingNumbers.php`. The Core cron
41
+ (`Sync/TrackingNumbers/Refresh`, `*/10 * * * *`, `maxExecutionTime 120`) fires a **dispatcher**;
42
+ the dispatcher fans out one child task per client.
43
+
44
+ > **⚠ NOT LIVE — go-live is gated on a platform bug.** The write path is api2 PUT, and
45
+ > **worker2 → api2 calls hard-crash on PHP 8.5** (see the go-live gate below). Until that is
46
+ > resolved on the prod worker box, this feature must not run for real (writes will crash).
47
+
48
+ ## How it works
49
+
50
+ ### Dispatcher → per-client child (no hardcoded client list)
51
+ - `Refresh(?clientIdentifier = null, isDryRun = true)` is the dispatcher. With **no**
52
+ `clientIdentifier` it **enumerates provisioned clients** (`Clients` JOIN `Databases` JOIN
53
+ `DatabaseHosts` JOIN `Environments` on slug) and `_Worker::runTask`s one
54
+ `Sync/TrackingNumbers/RefreshClient` child per client. With a `clientIdentifier` it runs that one
55
+ client inline.
56
+ - Each `RefreshClient` child: registers the client DB, claims a per-client run (lease, below),
57
+ resolves a write key (capability-based, below), fetches **one bounded page** of due rows, polls
58
+ carriers, writes changed status via api2 PUT, stamps each row's next-check cursor, and
59
+ **self-continues** to the next page if more remain.
60
+
61
+ ### Re-check cadence is a per-row cursor (carrier load ≠ cron frequency)
62
+ - `TrackingNumbers.c_dtTrackingCheckNext` is the cursor. A row is selected **only when it is NULL
63
+ or `<= NOW()`**.
64
+ - On a good check the cursor is stamped forward: **+1h** when delivery is imminent
65
+ (`OUT_FOR_DELIVERY` / `AWAITING_PICKUP` / `PICKUP`), else **+6h**, each with a few minutes of
66
+ `RAND()` **jitter** to avoid a carrier stampede.
67
+ - **Terminal statuses (`DELIVERED` / `RETURNED_TO_SENDER`) set the cursor NULL** → never
68
+ re-checked.
69
+ - A **45-day `dtCreated` age cutoff** bounds the working set. Steady-state carrier load is therefore
70
+ `working set / cadence`, **independent of how often the cron fires**.
71
+
72
+ ### Batches bounded to finish under the worker request timeout
73
+ - `PAGE_CAP = 50`, `SOFT_TIME_BUDGET_SECONDS = 40`. The budget is checked **between carrier calls
74
+ and between the two batch prefetches**.
75
+ - **⚠ This was a real bug:** an earlier **200s** budget let jobs get **hard-killed at ~75s** (the
76
+ worker's hard request timeout). Keep the soft budget comfortably under ~75s.
77
+
78
+ ### Carrier polling
79
+ - **FedEx** and **USPS** are polled with **one batch call each** (`getStatuses`), keyed by the
80
+ tracking number the carrier echoes back. **UPS is single-call only.** Anything a batch call misses
81
+ falls back to a per-number single lookup. (Carrier-library batch support lives in `_underscore` —
82
+ see [carrier-shipping-labels](../../_underscore/features/carrier-shipping-labels.md).)
83
+ - Carrier is resolved from the stored `ShippingCarrier` first (normalized), else from the
84
+ tracking-number pattern. **Unsupported carrier** (DHL / freight) is **parked**; **undetectable**
85
+ carrier is **deferred 3 days**; **malformed** number is **parked**.
86
+
87
+ ### Status mapping (coarse carrier status → our 12-status vocabulary)
88
+ - The coarse carrier status is refined by the latest scan-event text via a `match(true)` keyword
89
+ scan.
90
+ - **⚠ Two guards that must not be removed:**
91
+ - Scan text can **never** mark `DELIVERED` unless the carrier's coarse status already agrees —
92
+ stops a misleading scan note from producing a false terminal.
93
+ - `RETURNED_TO_SENDER` only comes from the specific phrase **`return to sender`**.
94
+
95
+ ### Concurrency + observability WITHOUT a new table (Parameters convention)
96
+ State lives in each client's existing `Parameters` (key/value) table, mirroring the established
97
+ `NETSUITE_EXECUTION_MODE` / `NETSUITE_LAST_SYNC_DATETIME_*` convention:
98
+ - `TRACKING_REFRESH_EXECUTION_MODE` — a **lease-based claim** holding `RUNNING:<unixStartTime>:<runId>`.
99
+ A claim wins only if idle **or** the lease (`RUN_LEASE_SECONDS = 120`) has expired. **Release is
100
+ owner-scoped** — only the `runId` that owns the lease can free it, so a stale prior run cannot
101
+ clear a newer run's claim. Single row + unique key → deadlock-free.
102
+ - `TRACKING_REFRESH_LAST_SYNC_DATETIME` — the "caught up through" watermark, stamped **only when the
103
+ client's due-set fully drains**.
104
+ - The run body is wrapped in **`try/finally`** so the lease is always released even on a throw.
105
+
106
+ ### Write key resolution is CAPABILITY-BASED, not by name
107
+ `findApiKeyAllowedToWriteStatus()` selects the active `Apis` key **whose role has
108
+ `AclRecordPermissions.allowUpdate` on record 62 (tracking-numbers) AND
109
+ `AclFieldPermissions.isWritable` on field 357 (status)**. Keying off the ACL grant itself means the
110
+ key selection can never drift from the permission, and no per-client key renaming is needed. If **no
111
+ capable key exists**, the client is **skipped** and a **fail-loud alert email** is sent **at most
112
+ once per day** (throttled via a `Parameters` key).
113
+
114
+ ## Data model — the internal `c_` bookkeeping columns
115
+
116
+ `dbchanges2/Client/2026-07-30 - TrackingNumbers_RefreshColumns.sql` adds five columns to
117
+ `TrackingNumbers` plus a composite sweep index: `c_dtTrackingCheckNext`, `c_dtTrackingChecked`,
118
+ `c_trackingCheckFailures`, `c_dtTrackingParked`, `c_trackingParkReason`. They are declared as model
119
+ fields on `_Model_Client_TrackingNumber` (`_underscore/Model/Client/TrackingNumber.php`).
120
+
121
+ **The full custom-field convention (learned here).** A first-class `c_` custom field needs **four**
122
+ things, not one: (1) the `ALTER TABLE`, (2) a `CustomRecordFields` registration on the record
123
+ (here record 62), (3) an `AclCustomFieldPermissions` grant to the roles that use it (Base + API
124
+ here), and (4) a **model property declaration**. **Internal-only variant used here:** these five are
125
+ registered with **no `Apis_CustomRecordFields` link and no `*CustomRecordFieldSettings`**, so they
126
+ are usable internally but **never surface in the API payload or the UI** — the right pattern for
127
+ private bookkeeping columns.
128
+
129
+ ## Migrations
130
+
131
+ - `dbchanges2/Client/2026-07-30 - TrackingNumbers_RefreshColumns.sql` — the five `c_` columns +
132
+ sweep index + `CustomRecordFields` (record 62) + `AclCustomFieldPermissions` (Base + API roles),
133
+ internal-only.
134
+ - `dbchanges2/Client/2026-07-30 - Acl_TrackingStatus_Grant.sql` — gap-fill: grants the API role write
135
+ on tracking-numbers.status (record 62 / field 357) wherever missing, **plus two UPDATEs that HEAL
136
+ clients whose grant rows exist but with the flags off** (`allowUpdate = 0` / `isWritable = 0`) — the
137
+ `NOT EXISTS` inserts skip those rows, so the heal is required (this was the **GroWrk** case).
138
+ - `dbchanges2/Client_Tdsynnex/2026-07-30 - Apis_TdsynnexKey.sql` — creates the integration key for
139
+ Tdsynnex, the only provisioned client with no key. It lives in the **per-client** folder (not a
140
+ `Client/` fan-out) because a `Client/` file cannot reference a specific tenant DB — the dbchanges2
141
+ cluster-isolation rule.
142
+ - `dbchanges2/Core/2026-07-30 - TrackingRefresh_Cron.sql` — registers the dispatcher cron.
143
+
144
+ ## Decisions
145
+
146
+ - **Write path = api2 PUT, not direct-DB (deliberate).** Direct-DB was **rejected** so that a future
147
+ **`postPut` interceptor** on tracking-numbers can fire the in-transit / delivered emails on a
148
+ status change. There is no such interceptor today (see the interceptor doc). This is a departure
149
+ from other worker2 crons (e.g. NYCHH asset-tag backfill) that write direct-DB to dodge the PHP-8.5
150
+ crash — here the interceptor requirement wins, which is exactly why the crash below is a hard gate.
151
+
152
+ ## Gotchas / known issues
153
+
154
+ ### ⚠ GO-LIVE GATE — worker2 → api2 hard-crashes on PHP 8.5
155
+ Writes go through api2 PUT, and a worker2 → api2 call **hard-crashes the request on PHP 8.5
156
+ (OpenSSL 3.5.5): a process-level 500 with no catchable trace, and the shutdown handler never
157
+ fires.** The prod worker box was bumped to PHP 8.5 on **2026-07-29** and this path was **never
158
+ re-verified there**. **api2 itself is fine** — on local PHP 8.1, auth + `PUT /tracking-numbers`
159
+ succeeded and api2 understands the new `c_` columns (GET + PUT returned `isSuccess` with no
160
+ unknown-column error). So the fix is **platform-level (worker TLS/curl on 8.5)**, not code. Until
161
+ resolved, this feature cannot run for real. See the causal
162
+ [PHP 8.3 → 8.5 runtime-upgrade workflow](../workflows/php-runtime-upgrade-dependency-audit.md) and
163
+ the same symptom in [NYCHH asset-tag backfill](nychh-asset-tag-backfill.md).
164
+
165
+ ### ⚠ Confirm the worker box's `ENVIRONMENT` before trusting enumeration on beta
166
+ `resolveEnvironment()` — `(substr(_Environment::$name, 0, 4) === 'dev-') ? 'dev' : name` — is
167
+ **byte-identical** to api2's `V2.php` / `Index.php`. On beta the worker box's `ENVIRONMENT` resolves
168
+ to slug **`beta`** or **`sandbox-dev`**, **not `dev`**, and those enumerate a **different/empty**
169
+ client set than `dev`. Confirm the box's actual `ENVIRONMENT` value before trusting the dispatcher's
170
+ client enumeration on beta.
171
+
172
+ ### ⚠ Conflict with the 1.0 Compass delivered-email cron
173
+ The in-transit / delivered emails **today** come from a separate **1.0** Compass worker cron
174
+ (`worker/crons/toga2/compass/send_delivered_email.php`) that polls UPS itself, sends via api2
175
+ `GET /email-templates/sendEmail`, and writes `TrackingNumbers.status` via a **direct DB UPDATE**
176
+ guarded by `WHERE status <> 'DELIVERED'`. If this refresh marks a Compass row `DELIVERED` **first**,
177
+ that cron's guard skips it and the delivered email **never sends**. This is a real conflict to
178
+ resolve before enabling the refresh for Compass — see the
179
+ [Compass in-transit/delivered emails doc](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
180
+ Plan (not built): move the email logic into a Compass `postPut` interceptor and retire the 1.0 cron.
181
+
182
+ ## Verification status
183
+
184
+ - **Beta (dev-sandbox):** all **33** provisioned clients resolve a capable write key via the
185
+ capability query (GroWrk healed, Tdsynnex provisioned). Dry-run (`isDryRun = true`) is fully
186
+ read-only (no writes, no api2 PUT, no emails) and is the safe smoke test.
187
+ - **Local:** worker2's worker endpoint is the **ROOT** (`POST http://worker2/`), **not** `/worker`
188
+ — see [running worker2 locally](../workflows/running-worker2-locally.md).
189
+ - **Prod (before go-live):** apply the three client/core migrations, re-run the 33/33 capable-key
190
+ check, and **resolve the PHP-8.5 worker → api2 crash** (the gate).
191
+
192
+ ## Change history
193
+ - 2026-08-24 — Built the centralized tracking-status refresh: dispatcher + per-client child
194
+ (`_Worker_Sync_TrackingNumbers`), per-row `c_dtTrackingCheckNext` re-check cursor (+1h imminent /
195
+ +6h else, jitter, terminal → NULL, 45-day cutoff), `PAGE_CAP=50`/`SOFT_TIME_BUDGET_SECONDS=40`
196
+ bounding (fixing a 200s budget that got jobs hard-killed at ~75s), FedEx/USPS batch + UPS
197
+ single-call polling, coarse→12-status mapping with the DELIVERED-agreement and literal
198
+ `return to sender` guards, and a `Parameters`-table lease (owner-scoped release) + watermark
199
+ reusing the NetSuite convention (no new table). Write key is capability-based
200
+ (`AclRecordPermissions.allowUpdate` rec 62 + `AclFieldPermissions.isWritable` field 357), skip +
201
+ once-daily alert if none. Added the five internal-only `c_` bookkeeping columns (4-part custom-field
202
+ registration, no API/UI surface) and the cron/ACL/Tdsynnex-key migrations. **Decided write path =
203
+ api2 PUT (not direct-DB)** to enable a future `postPut` email interceptor. Recorded the **PHP-8.5
204
+ worker → api2 hard-crash go-live gate**, the beta `ENVIRONMENT`-slug enumeration caveat, and the
205
+ conflict with the 1.0 Compass delivered-email cron's `WHERE status <> 'DELIVERED'` guard.
206
+ Verified 33/33 capable keys on beta; NOT yet live. (bala)
207
+ </content>
208
+ </invoke>
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-07-29
10
- owners: [jcardinal]
9
+ updated: 2026-08-24
10
+ owners: [jcardinal, bala]
11
11
  files:
12
12
  - worker2/composer.json
13
13
  - worker2/composer.lock
@@ -16,6 +16,8 @@ files:
16
16
  - worker2/Worker/Forecast/Import.php
17
17
  related:
18
18
  - ../architecture.md
19
+ - ../features/tracking-status-refresh.md
20
+ - ../features/nychh-asset-tag-backfill.md
19
21
  - ../features/team-sprint-management.md
20
22
  - ../features/platform-cache-cleanup.md
21
23
  - ../../../1.0/apps/tools/workflows/deploy-to-elastic-beanstalk-al2023.md
@@ -160,6 +162,24 @@ that production will hit next.
160
162
  Post-upgrade verification: `Platform/Cache/Truncate` confirmed working on PHP 8.5 (see
161
163
  [Platform Cache Cleanup](../features/platform-cache-cleanup.md)).
162
164
 
165
+ ## ⚠ PHP 8.5 regression discovered after the bump — worker2 → api2 hard-crash
166
+
167
+ **Not caught during this upgrade; surfaced later (2026-08).** After the prod worker box moved to
168
+ PHP 8.5 (OpenSSL 3.5.5), an outbound **worker2 → api2 HTTPS call hard-crashes the request: a
169
+ process-level 500 with no catchable trace, and the shutdown handler never fires.** It is a
170
+ **platform-level** fault (worker TLS/curl on 8.5), **not** application code — **api2 itself is fine**
171
+ (verified on local PHP 8.1: auth + `PUT /tracking-numbers` succeeded). Because the prod worker was
172
+ bumped **2026-07-29** and worker → api2 was **never re-verified there**, any worker2 feature that
173
+ writes through api2 is currently blocked on this.
174
+
175
+ Practical consequences:
176
+ - Existing worker2 crons dodge it by writing **direct-DB instead of api2** (e.g.
177
+ [NYCHH asset-tag backfill](../features/nychh-asset-tag-backfill.md)).
178
+ - The [tracking-status refresh](../features/tracking-status-refresh.md) **cannot** — it deliberately
179
+ writes via api2 PUT for a future email interceptor, so this crash is its **go-live gate**.
180
+ - **After any worker2 runtime bump, re-verify the worker → api2 round-trip on the actual target box**
181
+ — a green local run on an older PHP proves nothing about the deployed tier.
182
+
163
183
  ## Open risk — pre-existing composer advisories (own ticket)
164
184
 
165
185
  **Unrelated to this work and not introduced by it.** `composer audit` reports **16
@@ -187,6 +207,13 @@ of the 8.5 branch to keep it a single-concern change — **needs its own ticket.
187
207
  - **Code first (dual-runtime), runtime second (isolated).**
188
208
 
189
209
  ## Change history
210
+ - 2026-08-24 — Recorded a **PHP 8.5 regression that surfaced after this bump**: worker2 → api2
211
+ outbound HTTPS **hard-crashes** (process-level 500, no trace, shutdown handler never fires) on
212
+ 8.5 / OpenSSL 3.5.5. Platform-level (worker TLS/curl), **not** code — api2 verified fine on local
213
+ 8.1. Prod worker went 8.5 on 2026-07-29 and this path was never re-verified there, so it gates
214
+ every worker2 feature that writes through api2 (the reason the tracking-status refresh writes via
215
+ api2 PUT is blocked; other crons use direct-DB to avoid it). Added the rule to re-verify the
216
+ worker → api2 round-trip on the target box after any runtime bump. (bala)
190
217
  - 2026-07-29 — Initial capture. worker2 upgraded phpspreadsheet 1.30.2 → 3.10.7 (`^3.10`) and
191
218
  root require `>=8.2` → `>=8.2 <8.6`; 856 `*ByColumnAndRow` call sites migrated to the array
192
219
  coordinate form across `Team/Sprint.php`, `Client/TowFoundation/ProcessReceipts.php`, and
@@ -19,12 +19,12 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
19
19
  ## 2.0 framework
20
20
 
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 61 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
- - **worker2** (Worker) — 54 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
+ - **worker2** (Worker) — 55 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
24
  - **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 7 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
26
26
  - **saml** (SAML SSO Gateway) — 4 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
27
- - **toga2-view** (TOGa View Frontend) — 10 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
27
+ - **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
28
28
  - **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
29
29
  - **talos** (TOGa IQ) — 7 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
30
30
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
@@ -4,6 +4,7 @@
4
4
  |-----|-----------|---------|-------|
5
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 checkout — T&C acceptance + delivery-preference payment gate](features/checkout-terms-delivery-preference.md) | 2.0 | On the Rate warranty checkout, the customer must accept the Terms & Conditions **and** choose how to receive them (email and/or mail) before the PayPal **Subscr | toga2-view/src/pages/CheckOut/view/components/CheckoutForm/CheckoutForm.tsx, toga2-view/src/pages/CheckOut/viewModel/DUMMYFIELDS/CHECKOUTFORMDUMMYFIELDS.json, toga2-view/src/pages/CheckOut/viewModel/useCheckoutPageViewModel.ts, _underscore/Model/Rate/Entitlement.php |
7
+ | [Rate home-warranty Terms & Conditions content — verbatim transcript policy](features/home-warranty-terms-content.md) | 2.0 | `TERMS_HW.ts` is a **verbatim transcript of Rate's published form 158359 (1/26)**. | toga2-view/src/pages/TermsAndConditions/viewModel/DUMMYFIELDS/TERMS_HW.ts |
7
8
  | [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 |
8
9
  | [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 |
9
10
  | [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 |
@@ -17,6 +17,7 @@ related:
17
17
  - clients/rate/profile.md
18
18
  - clients/rate/features/aig-contract-creation.md
19
19
  - clients/rate/features/whole-home-warranty-purchase-guard.md
20
+ - clients/rate/features/home-warranty-terms-content.md
20
21
  - ../../../2.0/apps/toga2-view/features/property-questions-eligibility.md
21
22
  ---
22
23
 
@@ -0,0 +1,92 @@
1
+ ---
2
+ title: "Rate home-warranty Terms & Conditions content — verbatim transcript policy"
3
+ framework: "2.0"
4
+ repo: toga2-view
5
+ project: TOGa View Frontend
6
+ client: rate
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-08-21
10
+ owners: [mhammontree]
11
+ files:
12
+ - toga2-view/src/pages/TermsAndConditions/viewModel/DUMMYFIELDS/TERMS_HW.ts
13
+ related:
14
+ - clients/rate/profile.md
15
+ - clients/rate/features/checkout-terms-delivery-preference.md
16
+ - ../../../2.0/apps/toga2-view/features/terms-and-conditions-page.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ `TERMS_HW.ts` is a **verbatim transcript of Rate's published form 158359 (1/26)**. It is Rate's
22
+ legal document, not ours, and Rate distributes the **same form to multiple partners**. Our copy
23
+ must match theirs exactly.
24
+
25
+ **Byte-identical is an interoperability requirement, not a style preference.** If a customer or
26
+ Rate compares our rendering against another partner's, any difference must be traceable to
27
+ whoever deviated — and that must not be us.
28
+
29
+ ## The governing rule — never hand-edit this file
30
+
31
+ - **Do not proofread it. Do not patch it.** The source document contains known errors and we
32
+ reproduce them **deliberately**.
33
+ - A developer who "fixes" an apparent typo here breaks parity with Rate's other partners and
34
+ silently alters a legal document.
35
+ - Suspected source errors go **to Rate through the PM**. Our file stays as-is until Rate publishes
36
+ a corrected form.
37
+ - **We deliberately keep no list of Rate's content errors.** Maintaining that document is Rate's
38
+ job, not TOGA's; the team does not spend resources tracking another company's content defects.
39
+ - Changes land only by **regenerating** the file from a newly published form — see the
40
+ [regeneration + verification pipeline](../../../2.0/apps/toga2-view/features/terms-and-conditions-page.md).
41
+
42
+ ## Provenance lives in the file itself
43
+
44
+ The file header comment now carries the **form number**, **both source filenames**, and a
45
+ **"regenerate, do not edit"** instruction, so the provenance survives without anyone consulting
46
+ this KB. Before TRUE-80826 the file carried **no version marker at all** — there was no way to
47
+ tell which revision was deployed or to diff it against a new one. Recording the form number is
48
+ what turns the next revision into a minutes-long diff.
49
+
50
+ ## Source selection — the PDF is authoritative, the .docx is the cross-check
51
+
52
+ When Rate supplies **both** a PDF and a `.docx` of the same form, transcribe the **PDF**. This is
53
+ counter-intuitive (the `.docx` looks like the better source: real paragraphs, no page furniture, no
54
+ hyphenation) and cost a detour to establish:
55
+
56
+ - **Section numbering is not in the .docx text.** Word auto-generates it from `numbering.xml`, so
57
+ `word/document.xml` yields `DEFINITIONS:` with no `1.`. Using the docx would mean reimplementing
58
+ Word's multilevel list engine (`1.` / `A.` / `i.` / `ix.` with restart rules) — and the contract
59
+ **cross-references its own numbering** ("pursuant to Section 9(A)", "see Section 4 below"), so
60
+ wrong numbering corrupts internal references. The PDF is rendered output, so numbering is already
61
+ resolved.
62
+ - **The .docx is the full form**, including a **Declaration of Coverage** template with unfilled
63
+ merge fields (`{Customer Name}`, `{Company Name}`, `{insert}`,
64
+ `{Monthly Subscription / Annual Subscription / N/A}`). That is the per-customer cover page, not
65
+ T&C body text, and accounts for a ~259-word gap (docx 11,781 vs PDF 11,522 words).
66
+
67
+ **But do extract the .docx as an independent second rendering** and word-diff it (strip list
68
+ markers first). On TRUE-80826 agreement was **98.8% with zero content disagreements** — every
69
+ difference was (a) editorial `[square-bracket]` markup around phone numbers/URLs/addresses present
70
+ only in the docx working master, (b) ALL-CAPS conspicuous-disclosure passages that the docx has in
71
+ sentence case, or (c) a list token. That separates *"the vendor wrote this"* from *"my extractor
72
+ did this"* — it is what caught the `all- inclusive` hyphenation artifact.
73
+
74
+ **docx extraction notes:** headers/footers are separate parts (`word/header1.xml`,
75
+ `word/footer1.xml`), so `word/document.xml` is pure body text; detect list paragraphs via
76
+ `pPr/numPr`; treat `w:tab` and `w:br` as spaces.
77
+
78
+ ## Where this text is shown
79
+
80
+ Both the public `/terms/home-warranty` route and the in-checkout terms modal render this same
81
+ string through the shared component — see
82
+ [checkout T&C acceptance + delivery preference](checkout-terms-delivery-preference.md) for the
83
+ acceptance/delivery gate, which is a separate concern from the content.
84
+
85
+ ## Change history
86
+
87
+ - 2026-08-21 — TRUE-80826: replaced `TERMS_HW.ts` wholesale with a scripted verbatim transcript of
88
+ Rate's published form 158359 (1/26) (634 insertions / 567 deletions); round-trip diff against the
89
+ normalized source was an exact match (11,520 words) and PDF-vs-docx cross-validation agreed 98.8%
90
+ with zero content disagreements. Established the never-hand-edit / never-proofread policy and
91
+ added form number + source filenames to the file header. Browser rendering of either terms path
92
+ not yet verified. (mhammontree)
@@ -14,7 +14,7 @@ project: SAML SSO Gateway
14
14
  client: rate
15
15
  type: profile
16
16
  status: active
17
- updated: 2026-08-04
17
+ updated: 2026-08-21
18
18
  owners: ["rgirish", "bala", "mhammontree", "tcox"]
19
19
  files: []
20
20
  related:
@@ -26,6 +26,7 @@ related:
26
26
  - clients/rate/features/service-purchase-emails.md
27
27
  - clients/rate/features/paypal-subscription-purchase-webhook.md
28
28
  - clients/rate/features/subscription-cancellation.md
29
+ - clients/rate/features/home-warranty-terms-content.md
29
30
  ---
30
31
 
31
32
  ## Summary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.633",
3
+ "version": "1.0.635",
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",