toga-ai 1.0.247 → 1.0.249
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/features/email-template-sending.md +33 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +55 -5
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +12 -1
- package/knowledge/2.0/apps/worker2/features/notification-email-template.md +105 -0
- package/knowledge/2.0/apps/worker2/features/notification-email.md +5 -1
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/rate/INDEX.md +1 -0
- package/knowledge/clients/rate/features/service-purchase-emails.md +137 -0
- package/knowledge/clients/rate/profile.md +2 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-30
|
|
10
10
|
owners: ["jcardinal", "bala", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/EmailTemplate.php
|
|
@@ -14,6 +14,7 @@ files:
|
|
|
14
14
|
- _underscore/Email.php
|
|
15
15
|
related:
|
|
16
16
|
- ../../worker2/features/notification-email.md
|
|
17
|
+
- ../../worker2/features/notification-email-template.md
|
|
17
18
|
---
|
|
18
19
|
|
|
19
20
|
## Summary
|
|
@@ -100,9 +101,40 @@ can keep using `sendEmail($api, ...)`.
|
|
|
100
101
|
Any fire-and-forget caller now propagates that exception, which is intended (so an email is
|
|
101
102
|
never silently marked as sent). Blast radius is every client, including the Compass/Quad
|
|
102
103
|
`SalesOrder`/`ApprovalDecision` order-placed emails.
|
|
104
|
+
- **`_Email::send()` does not set PHPMailer `CharSet=UTF-8`.** Non-ASCII subjects/body (em
|
|
105
|
+
dash, curly quotes, accented names) **mojibake** (e.g. `—` → `—`). Scoped workaround:
|
|
106
|
+
use ASCII-only content. Recommended **root-cause fix**: set `$mailer->CharSet = 'UTF-8'` in
|
|
107
|
+
`_Email::send()` — but that affects **every** client, so it should be a separate, tested
|
|
108
|
+
change rather than a drive-by edit.
|
|
109
|
+
- **`clientIdentifier` does NOT pick the template DB.** `_Email::send()` uses
|
|
110
|
+
`clientIdentifier` only for CloudWatch logging/validation. The template loads from whatever
|
|
111
|
+
DB the `DB_CLIENT` alias points at — so register the right `Client_<x>` schema before
|
|
112
|
+
calling `send()`, or you'll load (or fail to find) the wrong tenant's template.
|
|
113
|
+
|
|
114
|
+
## Local test harness (2.0 worker2 / _underscore)
|
|
115
|
+
|
|
116
|
+
To exercise `send()` locally without enqueuing onto SQS:
|
|
117
|
+
|
|
118
|
+
1. `putenv('ENVIRONMENT=dev-<machine>')` — the framework reads `getenv('ENVIRONMENT')` and
|
|
119
|
+
`_Environment::initialize` throws if it's unset.
|
|
120
|
+
2. Add `C:\WWW\_underscore` to `include_path`; `chdir` to the app root (`worker2`) so
|
|
121
|
+
`./Config` and `./_.php` resolve.
|
|
122
|
+
3. `require vendor/autoload.php` **then** `_underscore/_underscore.php`.
|
|
123
|
+
4. `_Database::register('Client_Rate','localhost','root','',null,null,_underscore::DB_CLIENT)`.
|
|
124
|
+
5. `_Model_Client_EmailTemplate::send('Rate', $uuid, $to, [], [], ...$vars)`.
|
|
125
|
+
|
|
126
|
+
Dev config has `debug_mode=1` + `send_debug_emails_to`, so `_Email::send()` routes **all**
|
|
127
|
+
mail to that address. **CAVEAT:** do **not** test via `_Worker::runTask` locally — the dev
|
|
128
|
+
`[cloud] aws_worker_queue_url` points at the **production** SQS queue. Call `send()` (or the
|
|
129
|
+
worker method) in-process instead.
|
|
103
130
|
|
|
104
131
|
## Change history
|
|
105
132
|
|
|
133
|
+
- 2026-06-30 — Documented the **`CharSet=UTF-8` mojibake gotcha** (`_Email::send()` doesn't
|
|
134
|
+
set it → non-ASCII content garbles; scoped workaround = ASCII-only, root-cause = set
|
|
135
|
+
`$mailer->CharSet` as a separate tested change), clarified that `clientIdentifier` does not
|
|
136
|
+
select the template DB (`DB_CLIENT` does), and added the **local in-process test harness**.
|
|
137
|
+
Surfaced building the Rate service-purchase emails (TRUE-79251). (mhammontree)
|
|
106
138
|
- 2026-06-24 — Added the **branded-wrapper** path: `WRAPPER_UUID` + `renderWrappedBody()` load a
|
|
107
139
|
reserved per-client `EmailTemplates` row and inject `{subject}`/`{body}`. Consumed by the new
|
|
108
140
|
worker2 `_Worker_Notification_Email::Send` for internal/notification mail; the wrapper row is
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
| [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
|
|
9
9
|
| [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
|
|
10
10
|
| [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/, toga25-supply/src/hooks/useTableCellInteractions.ts |
|
|
11
|
-
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
|
|
11
|
+
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
|
|
12
12
|
| [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
|
|
@@ -7,7 +7,7 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-06-30
|
|
10
|
-
owners: [jcardinal]
|
|
10
|
+
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/surface/useFetchSurfaceMeta.ts
|
|
13
13
|
- toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
|
|
@@ -15,6 +15,7 @@ files:
|
|
|
15
15
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts
|
|
16
16
|
- toga25-supply/src/surface/evaluateSurfaceRule.ts
|
|
17
17
|
- toga25-supply/src/surface/actionRegistry.ts
|
|
18
|
+
- toga25-supply/src/surface/componentRegistry.tsx
|
|
18
19
|
- toga25-supply/src/surface/SurfaceActionBar.tsx
|
|
19
20
|
- toga25-supply/src/surface/SurfaceSection.tsx
|
|
20
21
|
- toga25-supply/src/surface/resolve.ts
|
|
@@ -50,16 +51,35 @@ feature flag — see below). Backend: [surface-resolver](../../_underscore/featu
|
|
|
50
51
|
meta. Loading a different *record* into the same surface reuses cached meta. Builds the URL as
|
|
51
52
|
`/surfaces/meta?slug=...` (query string), **not** `/surfaces/<slug>/meta` (the path form makes the
|
|
52
53
|
engine parse the slug as a record uuid → 404 EV-6). Unwraps the bundle via **`extractBundle()`**,
|
|
53
|
-
which
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
which uses a recursive **`findBundle(raw, predicate, depth=4)`** to walk the envelope (replacing the
|
|
55
|
+
old hard-coded `raw`/`raw.data`/`raw.data.<key>` probing) and locates the bundle by its
|
|
56
|
+
**structural fingerprint**: `looksLikeBundle` now requires **both** an `elements` array **and** a
|
|
57
|
+
`surface` descriptor object (not the `elements` array alone — see the decoy gotcha below).
|
|
58
|
+
`extractBundle(raw, slug)` then **prefers** the bundle whose `surface.slug === slug`, falling back to
|
|
59
|
+
first-found, so a stray sibling bundle can't win. `looksLikeGroup` requires the documented `group`
|
|
60
|
+
discriminator key so a single-bundle envelope is not mistaken for a group. On a malformed payload it
|
|
61
|
+
falls back to a safe empty bundle (empty `elements` + empty `messages`/`theme`/`vocabularies` maps)
|
|
62
|
+
so the page never crashes.
|
|
57
63
|
- **`useFetchSurfaceMetaGroup(slugs[])`** — the grouped sibling of `useFetchSurfaceMeta`, for a screen
|
|
58
64
|
needing many section surfaces at once. **Order-insensitive** (the slug list is normalized so cache
|
|
59
65
|
hits don't depend on argument order), `staleTime: Infinity`, hits the grouped endpoint
|
|
60
66
|
`GET /v2/surfaces/meta-group?slugs=...`, and extracts the `{surfaces}` map via `extractGroup()` /
|
|
61
67
|
`looksLikeGroup()` (the same structural-fingerprint approach as `extractBundle`). Returns `{}` for
|
|
62
68
|
unmigrated screens (`enabled:false`), so a non-roster client does no fetch.
|
|
69
|
+
- **`componentRegistry`** (`src/surface/componentRegistry.tsx`) — the **presentation sibling** of
|
|
70
|
+
`actionRegistry`, both keyed by the surface meta's `action.key`. `actionRegistry` maps key→behavior
|
|
71
|
+
handler; `componentRegistry` maps key→custom React component. `SurfaceActionBar.renderElement` calls
|
|
72
|
+
`getSurfaceComponent(element.action?.key)` and renders the registered component (in a keyed Fragment)
|
|
73
|
+
**before** falling through to the default icon button — so `action.key` drives **both** which
|
|
74
|
+
component renders and its behavior, while `messages.label` supplies its text and clicks route through
|
|
75
|
+
the same `handle`→dispatch path. First entry: `"salesOrder.viewLog"` → `<ViewLogButton>`. Extending
|
|
76
|
+
is one surface element in meta + one registry entry (same model as `actionRegistry` and the Pattern-3
|
|
77
|
+
cell registry).
|
|
78
|
+
- **element `config` vs `action.payload`.** Per-element `config` (`Record<string,unknown>|null`) is the
|
|
79
|
+
opaque element-level seed bag (the SurfaceElements JSON long-tail). At runtime the **backend resolver
|
|
80
|
+
merges the Action's own config with the element `config` to produce `action.payload`**, which is what
|
|
81
|
+
the FE consumes — the FE renderer never reads `element.config` directly. A separate surface-level
|
|
82
|
+
`surface.config` lives on the descriptor (e.g. Inventory's `groupByLabels`/`entityTags`).
|
|
63
83
|
- **`evaluateSurfaceRule`** — the Tier-1 rule evaluator: a **frozen `all/any/none` + `{field,op,value}`
|
|
64
84
|
grammar** that **throws on an unknown op** (generalized from the SalesOrders
|
|
65
85
|
`buildPatchedTenantFields`/`evaluateEnableRule`/`resolveFlag` helpers). Evaluated client-side against
|
|
@@ -155,6 +175,25 @@ seeding NYCHH/Prudential/SPGlobal is deferred; the Client-DB prod cross-cluster
|
|
|
155
175
|
|
|
156
176
|
## Gotchas
|
|
157
177
|
|
|
178
|
+
- **The single-meta envelope can hide the real bundle behind a DECOY `elements: []`.** The V2 envelope
|
|
179
|
+
can arrive as `{ surfaces: { meta: <real-bundle> }, elements: [] }` — a decoy empty `elements: []`
|
|
180
|
+
sits at the top level with **no `surface` descriptor**. A `looksLikeBundle` check matching on
|
|
181
|
+
`Array.isArray(value.elements)` alone short-circuits on the decoy and never finds the real nested
|
|
182
|
+
bundle → the action bar renders nothing. Fix: `looksLikeBundle` must require **both** `elements` AND
|
|
183
|
+
a `surface` descriptor; use recursive `findBundle` + slug-preferring `extractBundle` (see How it
|
|
184
|
+
works). The durable backend follow-up is to confirm/stabilize the single-meta envelope contract.
|
|
185
|
+
- **The surface `record` shape must match the rule field namespace — wrap as `{ order }`.** Seeded
|
|
186
|
+
Tier-1 rules namespace their field as `order._status`, so `getByPath(record, "order._status")` only
|
|
187
|
+
resolves if the host passes `record={{ order }}`. Passing the bare order object resolves to
|
|
188
|
+
`order.order._status` = undefined → every `in`/`eq` op is false → buttons hidden. The fix is at the
|
|
189
|
+
**host** (`SalesOrderTopBar.tsx`: `record={order ? { order } : null}`), **not** in
|
|
190
|
+
`evaluateSurfaceRule.ts` (the evaluator was correct).
|
|
191
|
+
- **`tokens.icon` is a LITERAL FontAwesome icon name, not a theme-token slug.** Only `tokens.color` and
|
|
192
|
+
`tokens.bg` are theme slugs resolved via `resolveThemeToken(theme, ...)` (the `theme` map only holds
|
|
193
|
+
`color.status.*`/`bg.status.*` keys). `tokens.icon` is a direct name (e.g. `check`, `xmark`,
|
|
194
|
+
`clipboardListCheck`, `penToSquare`) and must be used as-is (`element.tokens?.icon ?? "check"`).
|
|
195
|
+
Running it through `resolveThemeToken` misses on every icon and silently falls back to `"check"` for
|
|
196
|
+
every button.
|
|
158
197
|
- **Scripted-API GET return values land under `data.<scriptRoute>`, not `data` directly.** V2 nests a
|
|
159
198
|
scripted-API return value inside the envelope under a route-keyed slot of `data` (e.g. `data.meta`,
|
|
160
199
|
`data.surfaces`), one level deeper than a normal data fetch. The bundle hook unwraps this with
|
|
@@ -179,6 +218,17 @@ seeding NYCHH/Prudential/SPGlobal is deferred; the Client-DB prod cross-cluster
|
|
|
179
218
|
don't inherit Compass's shared base. Deleted the migrated section blocks from COMPASS/COMPASSCANADA/
|
|
180
219
|
QUAD `orderViewFields.json` (data-wiring kept; DEFAULT intact). Action-bar/approval-modal still
|
|
181
220
|
JSON. (jcardinal)
|
|
221
|
+
- 2026-06-30 — Debugged + extended the client-side action-bar renderer (no client-specific work).
|
|
222
|
+
Fixed three rendering bugs: (1) the single-meta envelope's **decoy `elements: []`** masked the real
|
|
223
|
+
nested bundle — `looksLikeBundle` now requires `elements` AND a `surface` descriptor, with recursive
|
|
224
|
+
`findBundle(depth=4)` + slug-preferring `extractBundle` + `group`-discriminator `looksLikeGroup` +
|
|
225
|
+
safe empty `messages`/`theme`/`vocabularies` fallback (`useFetchSurfaceMeta.ts`); (2) Tier-1 rules
|
|
226
|
+
always false because the host passed the bare order object instead of the `{ order }` envelope the
|
|
227
|
+
`order.*` field namespace requires (`SalesOrderTopBar.tsx`); (3) every action icon rendered as
|
|
228
|
+
`check` because `tokens.icon` (a literal FA name) was wrongly run through `resolveThemeToken`
|
|
229
|
+
(`SurfaceActionBar.tsx`). Built `componentRegistry.tsx` — the presentation sibling of
|
|
230
|
+
`actionRegistry`, key→custom component (`salesOrder.viewLog` → `ViewLogButton`). Clarified element
|
|
231
|
+
`config` vs backend-merged `action.payload`. (apeterson)
|
|
182
232
|
- 2026-06-29 — Migrated VendorItems (screen 2, recipe-exact) and Inventory (screen 3,
|
|
183
233
|
**presentation-only by decision** — fetch topology stays JSON; only title/buttons/modal copy/entity
|
|
184
234
|
tags moved). Fixed the "s.elements is not iterable" crash: `extractBundle()` now finds the bundle by
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
| [NetSuite → TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, test/@dave/probe_clickup_desc_match.php, test/@dave/test_model_load_behavior.php, dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
17
17
|
| [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.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_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
18
18
|
| [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, worker/crons/toga2/forecast2/import_supporting_records.php |
|
|
19
|
+
| [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, _underscore/Model/Client/EmailTemplate.php |
|
|
19
20
|
| [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
|
|
20
21
|
| [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
|
|
21
22
|
| [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-30
|
|
10
10
|
owners: [jcardinal, dfranks, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/
|
|
@@ -159,10 +159,21 @@ be reattempted.
|
|
|
159
159
|
- Class must be **`abstract`** and methods **`public static`** or routing fails.
|
|
160
160
|
- Webhook headers are **lowercased** by API Gateway (`X-GitHub-Event` → `x-github-event`).
|
|
161
161
|
- If the action needs a client DB, register it in `initialize()` — it runs before the method.
|
|
162
|
+
- **Worker actions do NOT auto-register `DB_CLIENT`.** The dispatcher registers only
|
|
163
|
+
`DB_CLIENT_LOGS`. A worker that loads a `Client_*` model must register the concrete client
|
|
164
|
+
schema itself — `_Database::registerClientDatabases($clientId, $environment)`, where
|
|
165
|
+
`$environment = (substr(_Environment::$name, 0, 4) === 'dev-') ? 'dev' : _Environment::$name`.
|
|
166
|
+
Without it, every `_Model_Client_*` load fails. Proven pattern: `_Worker_Startech` (and
|
|
167
|
+
`_Worker_Notification_EmailTemplate`). Do this in `initialize()` or at the top of the method.
|
|
162
168
|
- See [architecture.md](../architecture.md) for the always-HTTP-200 rule and the
|
|
163
169
|
commit-before-SQS transaction pattern that the worker relies on.
|
|
164
170
|
|
|
165
171
|
## Change history
|
|
172
|
+
- 2026-06-30 — Added the **`DB_CLIENT` auto-registration gotcha**: the dispatcher registers only
|
|
173
|
+
`DB_CLIENT_LOGS`, so a worker loading a `Client_*` model must call
|
|
174
|
+
`_Database::registerClientDatabases($clientId, $environment)` itself (proven by
|
|
175
|
+
`_Worker_Startech` / `_Worker_Notification_EmailTemplate`). Surfaced building the Rate
|
|
176
|
+
service-purchase email worker (TRUE-79251). (mhammontree)
|
|
166
177
|
- 2026-06-24 — Clarified that the dispatcher spreads the string-keyed `parameters` as PHP **named
|
|
167
178
|
arguments** (`$class::$method(...$parameters)`), so parameter names are the queue contract — do
|
|
168
179
|
not collapse a multi-param action into a single `array` arg. (mhammontree)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Background Email-Template Worker (_Worker_Notification_EmailTemplate)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-30
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Notification/EmailTemplate.php
|
|
13
|
+
- _underscore/Model/Client/EmailTemplate.php
|
|
14
|
+
related:
|
|
15
|
+
- ./notification-email.md
|
|
16
|
+
- ./creating-worker-actions.md
|
|
17
|
+
- ../../_underscore/features/email-template-sending.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
`_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined
|
|
23
|
+
`EmailTemplates` row off-thread** as a background WorkerJob. It is the generic/reusable
|
|
24
|
+
companion to `_Worker_Notification_Email::Send` (see
|
|
25
|
+
[`notification-email.md`](./notification-email.md)): where that one wraps an ad-hoc
|
|
26
|
+
subject/body in the client's branded shell for *internal* notices, this one dispatches a
|
|
27
|
+
**complete pre-authored transactional template by UUID** (e.g. a client-facing purchase
|
|
28
|
+
confirmation). It is **not** specific to any client — any client's stored `EmailTemplates`
|
|
29
|
+
row can be sent through it.
|
|
30
|
+
|
|
31
|
+
Decoupling the send from whatever triggers it (e.g. an entitlement save) means an SMTP /
|
|
32
|
+
template failure becomes a **visible failed `WorkerJobs` row** rather than blocking — or
|
|
33
|
+
silently riding inside — the originating transaction.
|
|
34
|
+
|
|
35
|
+
## Key files / entry points
|
|
36
|
+
|
|
37
|
+
- `worker2/Worker/Notification/EmailTemplate.php` —
|
|
38
|
+
`abstract class _Worker_Notification_EmailTemplate`. One method:
|
|
39
|
+
`public static Send(string $clientIdentifier, string $uuid, $to, $cc, $bcc, ...$args): string`.
|
|
40
|
+
- Resolves the numeric `clientId` from `$clientIdentifier` via a Core query.
|
|
41
|
+
- Registers the client DB with
|
|
42
|
+
`_Database::registerClientDatabases($clientId, $environment)` (see the DB-registration
|
|
43
|
+
note below — worker actions do **not** get the client DB for free).
|
|
44
|
+
- Calls `_Model_Client_EmailTemplate::send($clientIdentifier, $uuid, $to, $cc, $bcc, ...$args)`.
|
|
45
|
+
- The variadic `...$args` forwards the template's `{placeholder}` values straight through
|
|
46
|
+
the model's named-argument dispatch contract — so the queued `parameters` keys *are* the
|
|
47
|
+
template variables.
|
|
48
|
+
- `_underscore/Model/Client/EmailTemplate.php` — `send()` does the actual load / placeholder
|
|
49
|
+
substitution / `_Email` send. Documented in
|
|
50
|
+
[`email-template-sending.md`](../../_underscore/features/email-template-sending.md).
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
1. **Resolve the client.** Query Core to turn `clientIdentifier` (e.g. `'Rate'`) into the
|
|
55
|
+
numeric `clientId`.
|
|
56
|
+
2. **Register the client DB.** `_Database::registerClientDatabases($clientId, $environment)`,
|
|
57
|
+
where
|
|
58
|
+
`$environment = (substr(_Environment::$name, 0, 4) === 'dev-') ? 'dev' : _Environment::$name`.
|
|
59
|
+
The worker dispatcher only registers `DB_CLIENT_LOGS` for you — the concrete `Client_<x>`
|
|
60
|
+
schema that `_Model_Client_EmailTemplate` reads from is **your** responsibility (proven
|
|
61
|
+
pattern: `_Worker_Startech`). See
|
|
62
|
+
[`creating-worker-actions.md`](./creating-worker-actions.md).
|
|
63
|
+
3. **Dispatch the template.** `_Model_Client_EmailTemplate::send()` loads the `EmailTemplates`
|
|
64
|
+
row by UUID, substitutes `{placeholder}` vars from `...$args`, and sends via `_Email`.
|
|
65
|
+
|
|
66
|
+
### Enqueuing it
|
|
67
|
+
|
|
68
|
+
`_Worker::runTask('Notification/EmailTemplate/Send', ['clientIdentifier'=>'Rate',
|
|
69
|
+
'uuid'=>'<template-uuid>', 'to'=>'…', 'cc'=>[], 'bcc'=>[], '<var1>'=>…, '<var2>'=>…])`
|
|
70
|
+
inserts a WorkerJob. The dispatcher spreads the string-keyed `parameters` as PHP **named
|
|
71
|
+
arguments**, so each `parameters` key binds to a method parameter — and the leftover keys
|
|
72
|
+
flow into `...$args` as the template variables.
|
|
73
|
+
|
|
74
|
+
## Gotchas / known issues
|
|
75
|
+
|
|
76
|
+
- **Worker actions do not auto-register `DB_CLIENT`.** The dispatcher registers only
|
|
77
|
+
`DB_CLIENT_LOGS`; a worker that loads a `Client_*` model must call
|
|
78
|
+
`_Database::registerClientDatabases($clientId, $environment)` itself or the
|
|
79
|
+
`_Model_Client_*` load fails. (Captured as a standalone gotcha in
|
|
80
|
+
[`creating-worker-actions.md`](./creating-worker-actions.md).)
|
|
81
|
+
- **Don't test this via `_Worker::runTask` locally.** The dev `[cloud]
|
|
82
|
+
aws_worker_queue_url` points at the **production** SQS queue, so a local `runTask` enqueues
|
|
83
|
+
onto prod. Call `Send()` (or `_Model_Client_EmailTemplate::send()`) in-process instead —
|
|
84
|
+
see the local harness in
|
|
85
|
+
[`email-template-sending.md`](../../_underscore/features/email-template-sending.md#local-test-harness-2-0-worker2--underscore).
|
|
86
|
+
|
|
87
|
+
## Change history
|
|
88
|
+
|
|
89
|
+
- 2026-06-30 — Built the generic background email-template worker
|
|
90
|
+
`_Worker_Notification_EmailTemplate::Send` (TRUE-79251): resolves the client, registers the
|
|
91
|
+
client DB via `registerClientDatabases`, and dispatches a stored `EmailTemplates` row by
|
|
92
|
+
UUID through `_Model_Client_EmailTemplate::send`, forwarding `{placeholder}` values via
|
|
93
|
+
`...$args`. Sits beside `_Worker_Notification_Email::Send` (internal/branded-wrapper path).
|
|
94
|
+
First consumer: Rate service-purchase confirmation emails — see
|
|
95
|
+
[`clients/rate/features/service-purchase-emails.md`](../../../clients/rate/features/service-purchase-emails.md).
|
|
96
|
+
(mhammontree)
|
|
97
|
+
|
|
98
|
+
## Related docs
|
|
99
|
+
|
|
100
|
+
- [`notification-email.md`](./notification-email.md) — the internal/branded-wrapper sibling
|
|
101
|
+
(`_Worker_Notification_Email::Send`).
|
|
102
|
+
- [`creating-worker-actions.md`](./creating-worker-actions.md) — worker dispatch + the
|
|
103
|
+
client-DB registration gotcha.
|
|
104
|
+
- [`2.0/apps/_underscore/features/email-template-sending.md`](../../_underscore/features/email-template-sending.md)
|
|
105
|
+
— the `_Model_Client_EmailTemplate` model this worker dispatches.
|
|
@@ -16,6 +16,7 @@ files:
|
|
|
16
16
|
related:
|
|
17
17
|
- ../../_underscore/features/email-template-sending.md
|
|
18
18
|
- ./creating-worker-actions.md
|
|
19
|
+
- ./notification-email-template.md
|
|
19
20
|
---
|
|
20
21
|
|
|
21
22
|
## Summary
|
|
@@ -23,7 +24,10 @@ related:
|
|
|
23
24
|
Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated,
|
|
24
25
|
not client-facing transactional mail) are sent through one worker action,
|
|
25
26
|
`_Worker_Notification_Email::Send(...)`, which wraps a plain subject/body in the client's
|
|
26
|
-
**DB-stored branded shell** before sending
|
|
27
|
+
**DB-stored branded shell** before sending. (For sending a **complete pre-authored stored
|
|
28
|
+
template by UUID** off-thread — e.g. client-facing transactional mail — use the sibling
|
|
29
|
+
worker [`_Worker_Notification_EmailTemplate::Send`](./notification-email-template.md)
|
|
30
|
+
instead.) The branded shell used to be a
|
|
27
31
|
hardcoded PHP class (`_Email_Template`, now **deleted**); it now lives in the client DB as a
|
|
28
32
|
reserved `EmailTemplates` row so the branding can change without a code deploy. This is a
|
|
29
33
|
**shared/core 2.0 mechanism** — every client inherits the wrapper row from the dbchanges2
|
package/knowledge/INDEX.md
CHANGED
|
@@ -16,8 +16,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
16
16
|
|
|
17
17
|
## 2.0 framework
|
|
18
18
|
|
|
19
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
20
|
-
- **worker2** (Worker) —
|
|
19
|
+
- **_underscore** (_Underscore) _(framework core)_ — 20 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
20
|
+
- **worker2** (Worker) — 24 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
21
21
|
- **api2** (API) — 7 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
22
22
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
23
23
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
| [Rate SalesOrder → NetSuite CashSale Export (postPost)](features/netsuite-cashsale-export.md) | 2.0 | Rate sells home-warranty / home-tech-support products. | _underscore/Model/Rate/SalesOrder.php, _underscore/Model/Rate/Item.php |
|
|
8
8
|
| [Rate SAML SSO](features/saml-sso.md) | 2.0 | Rate uses Azure AD as its IdP (`login.rate.com`). | _underscore/Model/Rate/ClientAuthentication.php, saml/Controller/Index.php, toga2-view/src/hooks/useAuthenticationFlow.ts |
|
|
9
9
|
| [Service Card Entitlement Display](features/service-card-entitlements.md) | 2.0 | Rate's home and services pages display one service card per purchased entitlement. | src/components/ServiceCard/ServiceCard.tsx, src/components/ServiceCard/index.ts, src/hooks/useBundleServices.ts, src/pages/Home/api/homeApi.ts, src/pages/Home/view/HomePage.tsx, src/pages/Home/viewModels/useHomePageViewModel.ts, src/pages/Services/view/ServicesPage.tsx, src/pages/Services/viewModels/useServicePageViewModel.ts |
|
|
10
|
+
| [Rate Service-Purchase Confirmation Emails (Tech / Warranty)](features/service-purchase-emails.md) | 2.0 | When a Rate customer purchases a service, a confirmation email is sent. | _underscore/Model/Rate/Entitlement.php, worker2/Worker/Notification/EmailTemplate.php, dbchanges2/Client_Rate/2026-06-30a - Rate purchase email templates.sql |
|
|
10
11
|
| [Rate](profile.md) | 2.0 | Rate is a mortgage/lending client. | |
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Rate Service-Purchase Confirmation Emails (Tech / Warranty)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: rate
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-30
|
|
10
|
+
owners: [mhammontree]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Rate/Entitlement.php
|
|
13
|
+
- worker2/Worker/Notification/EmailTemplate.php
|
|
14
|
+
- dbchanges2/Client_Rate/2026-06-30a - Rate purchase email templates.sql
|
|
15
|
+
related:
|
|
16
|
+
- clients/rate/profile.md
|
|
17
|
+
- clients/rate/features/aig-contract-creation.md
|
|
18
|
+
- ../../../2.0/apps/worker2/features/notification-email-template.md
|
|
19
|
+
- ../../../2.0/apps/_underscore/features/email-template-sending.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
When a Rate customer purchases a service, a confirmation email is sent. There are two
|
|
25
|
+
products, each with its own template:
|
|
26
|
+
|
|
27
|
+
- **Whole Home Tech Services** ("1 Year Unlimited Tech Support") — the *tech* email.
|
|
28
|
+
- **Whole Home Warranty** — the *warranty* email. (AIG is the warranty provider behind this
|
|
29
|
+
product; not the separate `aig` Staples client.)
|
|
30
|
+
|
|
31
|
+
The send is triggered from the existing Rate entitlement interceptor
|
|
32
|
+
(`_Model_Rate_Entitlement::postPost`) and dispatched **off-thread** through the generic
|
|
33
|
+
[`_Worker_Notification_EmailTemplate::Send`](../../../2.0/apps/worker2/features/notification-email-template.md)
|
|
34
|
+
worker. The email content lives in DB-stored `Client_Rate.EmailTemplates` rows so it can be
|
|
35
|
+
edited without a deploy.
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
1. **Trigger.** `postPost` (`_underscore/Model/Rate/Entitlement.php`) runs after an
|
|
40
|
+
entitlement save. It calls `sendPurchaseEmail($payload)` **after** the existing
|
|
41
|
+
AIG-contract block, via helpers:
|
|
42
|
+
- `resolvePurchaseProduct()` — warranty vs. tech detection.
|
|
43
|
+
- `buildPurchaseEmailParameters()` — assemble the template `{placeholder}` values.
|
|
44
|
+
- `sendPurchaseEmail()` — enqueue
|
|
45
|
+
`_Worker::runTask('Notification/EmailTemplate/Send', [...])`, wrapped in `try/catch` so a
|
|
46
|
+
send failure **never blocks the save**.
|
|
47
|
+
2. **Fires on purchase, independent of the AIG contract outcome.** Tech has no AIG contract
|
|
48
|
+
at all; the warranty email still sends even if the AIG contract call failed (that path is
|
|
49
|
+
monitored separately by `_Worker_Monitors_RateEntitlement` — see
|
|
50
|
+
[`aig-contract-creation.md`](./aig-contract-creation.md)).
|
|
51
|
+
3. **Dispatch + send.** The worker resolves the `Rate` client, registers `Client_Rate`, and
|
|
52
|
+
sends the stored template by UUID with the assembled variables.
|
|
53
|
+
|
|
54
|
+
### Product detection (the entitlement / product model)
|
|
55
|
+
|
|
56
|
+
In `Client_Rate`: `Entitlements.saleItemId → Items.id`. **There is no product-type column** —
|
|
57
|
+
the discriminator is **`Items.title`**. Detection mirrors toga2-view's `detectServiceType`:
|
|
58
|
+
|
|
59
|
+
- title contains `"warranty"` → **warranty**;
|
|
60
|
+
- else `"tech"` / `"support"` → **tech**.
|
|
61
|
+
|
|
62
|
+
Live titles: `"1 Year Unlimited Tech Support - Annual/Monthly"` and
|
|
63
|
+
`"Whole Home Warranty - Monthly"`. The entitlement's client identifier is `'Rate'`.
|
|
64
|
+
|
|
65
|
+
### Constants (in `_Model_Rate_Entitlement`)
|
|
66
|
+
|
|
67
|
+
- `EMAIL_TEMPLATE_TECH_PURCHASE` / `EMAIL_TEMPLATE_WARRANTY_PURCHASE` — the template UUIDs.
|
|
68
|
+
**Kept in sync with the dbchanges2 rows** (tech `2c4f8a1e-…`, warranty `5d9e3f72-…`).
|
|
69
|
+
- `CLIENT_IDENTIFIER_RATE = 'Rate'`, `PORTAL_URL_TECH` / `PORTAL_URL_WARRANTY`,
|
|
70
|
+
`SUPPORT_PHONE = '1-206-260-6187'`.
|
|
71
|
+
|
|
72
|
+
## Templates (dbchanges2)
|
|
73
|
+
|
|
74
|
+
`dbchanges2/Client_Rate/2026-06-30a - Rate purchase email templates.sql` (new `Client_Rate`
|
|
75
|
+
migration folder) inserts two transactional HTML emails via
|
|
76
|
+
`INSERT … ON DUPLICATE KEY UPDATE` (fixed UUIDs). `isActive` is **intentionally excluded**
|
|
77
|
+
from the `ON DUPLICATE UPDATE` list so a re-run never silently re-activates a row that was
|
|
78
|
+
turned off. Responsive 600px table-based HTML faithful to the Figma.
|
|
79
|
+
|
|
80
|
+
- **Warranty template** keeps the "Renewal Notice" / "Renewal Summary" verbiage per PM/design
|
|
81
|
+
but **drops the "Renewed" row** — a first purchase has no prior renewal. **Next Charge** is
|
|
82
|
+
the date shown.
|
|
83
|
+
|
|
84
|
+
## Brand assets (toga-public S3)
|
|
85
|
+
|
|
86
|
+
2.0 transactional-email images are hosted on the public bucket **`toga-public`** (us-west-2),
|
|
87
|
+
same convention as the Compass emails (`compass/Email/…`). Rate assets live under
|
|
88
|
+
`rate/Email/`: `rate-header-logo.png` (white "Rate" + red house composited onto header
|
|
89
|
+
`#2c2926`), `toga-technology-footer.png` (white TOGA wordmark composited onto footer
|
|
90
|
+
`#21292d`), `check-circle.png` (green `#226257` circle + white check), `ProximaNovaRegular.woff`.
|
|
91
|
+
|
|
92
|
+
**Recipe for a transparent/SVG brand asset on a dark email region:** rasterize/flatten the
|
|
93
|
+
asset onto the **exact region background color** so it renders identically across clients
|
|
94
|
+
(Figma SVG → rasterize via Node `sharp` with `flatten: <bgcolor>`).
|
|
95
|
+
|
|
96
|
+
### Figma fonts (TOGa View 2.0 email designs)
|
|
97
|
+
|
|
98
|
+
- **Figtree** — content area (headline/body/summary). Free Google Font.
|
|
99
|
+
- **Proxima Nova** — button + footer "Contact us" / copyright. toga2-view self-hosts
|
|
100
|
+
`ProximaNovaRegular.woff` at `toga2-view/src/fonts/`.
|
|
101
|
+
- **Axiforma** — "Powered By" + the "TOGA TECHNOLOGY" wordmark. **Not in repo, not free** — so
|
|
102
|
+
the footer wordmark is rendered as the **logo image** (Axiforma baked in) rather than live
|
|
103
|
+
text.
|
|
104
|
+
|
|
105
|
+
Email web-font reality: Gmail/Outlook strip web fonts and fall back; Apple Mail/iOS honor
|
|
106
|
+
`@font-face`/`<link>`. Design for the fallback.
|
|
107
|
+
|
|
108
|
+
## Gotchas / known issues
|
|
109
|
+
|
|
110
|
+
- **`border-radius` on a `<td>` renders SQUARE in many clients (Outlook).** Use an `<img>` for
|
|
111
|
+
circular badges/icons (e.g. the green circle-check) instead of a CSS-rounded table cell.
|
|
112
|
+
- **ASCII-only subjects (mojibake workaround).** `_Email::send()` does not set PHPMailer
|
|
113
|
+
`CharSet=UTF-8`, so non-ASCII subjects/body (em dash, curly quotes, accents) mojibake. This
|
|
114
|
+
workflow uses ASCII-only subjects; the proper root-cause fix is tracked on
|
|
115
|
+
[`email-template-sending.md`](../../../2.0/apps/_underscore/features/email-template-sending.md).
|
|
116
|
+
- **Worker DB registration** — see
|
|
117
|
+
[`notification-email-template.md`](../../../2.0/apps/worker2/features/notification-email-template.md):
|
|
118
|
+
worker actions do not auto-register `DB_CLIENT`.
|
|
119
|
+
|
|
120
|
+
## Go-live TODOs (TRUE-79251, flagged in-code as `TODO TRUE-79251`)
|
|
121
|
+
|
|
122
|
+
Not yet durable knowledge — open items before go-live:
|
|
123
|
+
|
|
124
|
+
- AIG Terms & Conditions URL — `{termsUrl}` currently `'#'`.
|
|
125
|
+
- From-address / name — currently `donotreply@togatech.com` / "Rate".
|
|
126
|
+
- Confirm the **Next Charge** source field — currently `dateCoverageEnd`.
|
|
127
|
+
|
|
128
|
+
(Support phone is resolved: `1-206-260-6187`.)
|
|
129
|
+
|
|
130
|
+
## Change history
|
|
131
|
+
|
|
132
|
+
- 2026-06-30 — Built the Rate service-purchase confirmation email workflow (TRUE-79251):
|
|
133
|
+
`postPost` trigger + helpers in `_Model_Rate_Entitlement`, two DB-stored `Client_Rate`
|
|
134
|
+
templates (tech / warranty) dispatched off-thread via
|
|
135
|
+
`_Worker_Notification_EmailTemplate::Send`, brand assets on `toga-public/rate/Email/`.
|
|
136
|
+
Fires on purchase independent of the AIG-contract outcome. Tested locally with a real send
|
|
137
|
+
to the dev team. (mhammontree)
|
|
@@ -12,7 +12,7 @@ project: SAML SSO Gateway
|
|
|
12
12
|
client: rate
|
|
13
13
|
type: profile
|
|
14
14
|
status: active
|
|
15
|
-
updated: 2026-06-
|
|
15
|
+
updated: 2026-06-30
|
|
16
16
|
owners: ["rgirish", "bala", "mhammontree"]
|
|
17
17
|
files: []
|
|
18
18
|
related:
|
|
@@ -21,6 +21,7 @@ related:
|
|
|
21
21
|
- clients/rate/features/netsuite-cashsale-export.md
|
|
22
22
|
- clients/rate/features/service-card-entitlements.md
|
|
23
23
|
- clients/rate/features/aig-contract-creation.md
|
|
24
|
+
- clients/rate/features/service-purchase-emails.md
|
|
24
25
|
---
|
|
25
26
|
|
|
26
27
|
## Summary
|
package/package.json
CHANGED