toga-ai 1.0.192 → 1.0.194
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 +20 -3
- package/knowledge/2.0/apps/toga2-view/INDEX.md +2 -0
- package/knowledge/2.0/apps/toga2-view/features/mobile-nav-header.md +53 -0
- package/knowledge/2.0/apps/toga2-view/features/service-card.md +66 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +8 -2
- package/knowledge/2.0/apps/worker2/features/notification-email.md +144 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/rate/profile.md +2 -2
- package/package.json +1 -1
|
@@ -6,13 +6,14 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
10
|
-
owners: ["jcardinal", "bala"]
|
|
9
|
+
updated: 2026-06-24
|
|
10
|
+
owners: ["jcardinal", "bala", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/EmailTemplate.php
|
|
13
13
|
- _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php
|
|
14
14
|
- _underscore/Email.php
|
|
15
|
-
related:
|
|
15
|
+
related:
|
|
16
|
+
- ../../worker2/features/notification-email.md
|
|
16
17
|
---
|
|
17
18
|
|
|
18
19
|
## Summary
|
|
@@ -33,6 +34,17 @@ sends via `_Email`. The only client-specific input the send actually needs is th
|
|
|
33
34
|
non-API entry point. Caller passes the client identifier directly; no `$api` object.
|
|
34
35
|
- **`dispatch(string $clientIdentifier, ...): bool`** (private) — the shared body both
|
|
35
36
|
entry points call. Holds all the real logic.
|
|
37
|
+
- **`renderWrappedBody(string $subject, string $body): ?string`** — the **branded-wrapper**
|
|
38
|
+
path (distinct from the send-by-uuid path above). Loads the reserved wrapper row
|
|
39
|
+
(`uuid = WRAPPER_UUID`) and injects the subject/body into its `{subject}`/`{body}`
|
|
40
|
+
placeholders via `strtr` (simultaneous, so a `{body}` literal in the subject can't be
|
|
41
|
+
re-expanded). Returns `null` if there is no active wrapper row. Does **not** send — the caller
|
|
42
|
+
feeds the result to `_Email`.
|
|
43
|
+
- **`const WRAPPER_UUID = '11111111-1111-4111-8111-111111111111'`** — the reserved uuid of the
|
|
44
|
+
per-client branded "wrapper" template row. The model lookup and the dbchanges2 seed migration
|
|
45
|
+
must use this same literal. The row is seeded by dbchanges2 (`Client/` blank-client baseline so
|
|
46
|
+
every new client inherits it, plus per-client backfills). Its body is the branded HTML shell
|
|
47
|
+
holding `{subject}`/`{body}` placeholders.
|
|
36
48
|
- `_Model_Client_EmailTemplateOutgoingEmailAddress` — per-template stored TO/CC/BCC
|
|
37
49
|
addresses (`toCcBcc` enum), merged into the caller-supplied recipients.
|
|
38
50
|
- `_underscore/Email.php` — `_Email` requires a non-empty `clientIdentifier` (throws
|
|
@@ -91,6 +103,11 @@ can keep using `sendEmail($api, ...)`.
|
|
|
91
103
|
|
|
92
104
|
## Change history
|
|
93
105
|
|
|
106
|
+
- 2026-06-24 — Added the **branded-wrapper** path: `WRAPPER_UUID` + `renderWrappedBody()` load a
|
|
107
|
+
reserved per-client `EmailTemplates` row and inject `{subject}`/`{body}`. Consumed by the new
|
|
108
|
+
worker2 `_Worker_Notification_Email::Send` for internal/notification mail; the wrapper row is
|
|
109
|
+
seeded across clients by dbchanges2. See
|
|
110
|
+
[`worker2/notification-email.md`](../../worker2/features/notification-email.md). (mhammontree)
|
|
94
111
|
- 2026-06-18 — Hardened against silent failure: `dispatch()` throws on an inactive template
|
|
95
112
|
(was `return false`) and `_Email::send()` throws + logs `isSuccess`/`error` when
|
|
96
113
|
`PHPMailer::Send()` fails (was swallowed with no return/throw/log). Affects all clients;
|
|
@@ -3,3 +3,5 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [TOGa View Frontend (toga2-view) Architecture](architecture.md) | `toga2-view` is the **React/TypeScript single-page frontend** for the TOGa 2.0 platform — the customer-facing web app (home warranty / tech-support portals). | toga2-view/src/main.tsx, toga2-view/src/App.tsx, toga2-view/src/routes.tsx, toga2-view/src/api/axiosInstance.ts, toga2-view/src/api/apiFunctions.ts, toga2-view/src/utils/queryHelpers.ts, toga2-view/src/contexts/AuthContext.tsx, toga2-view/src/contexts/useUserStore.ts, toga2-view/src/hooks/useAuthenticationFlow.ts, toga2-view/vite.config.ts, toga2-view/package.json |
|
|
6
|
+
| [Mobile Nav Header](features/mobile-nav-header.md) | The `MobileNavToggle` component renders the fixed 72 px header (hamburger + Rate logo) and the slide-in drawer nav. | toga2-view/src/components/MobileNav/MobileNav.tsx, toga2-view/public/assets/Rate_Logo.svg |
|
|
7
|
+
| [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 |
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Mobile Nav Header
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-view
|
|
5
|
+
project: TOGa View Frontend
|
|
6
|
+
client: rate
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-24
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- toga2-view/src/components/MobileNav/MobileNav.tsx
|
|
13
|
+
- toga2-view/public/assets/Rate_Logo.svg
|
|
14
|
+
related:
|
|
15
|
+
- clients/rate/profile.md
|
|
16
|
+
- 2.0/apps/toga2-view/features/service-card.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
The `MobileNavToggle` component renders the fixed 72 px header (hamburger + Rate logo) and the slide-in drawer nav. Header dimensions and spacing match Figma file `lQxDR8fCBXPnStWzI9BCv1`, node `9203-10323`.
|
|
22
|
+
|
|
23
|
+
## Key files / entry points
|
|
24
|
+
|
|
25
|
+
- `MobileNav.tsx` — exports both `MobileNav` (drawer) and `MobileNavToggle` (page shell + fixed header)
|
|
26
|
+
- `public/assets/Rate_Logo.svg` — exported from Figma "Logos → Rate → Small" component (58.5 × 24 px); use SVG, not PNG, for correct aspect ratio
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
**Header layout (Figma spec):**
|
|
31
|
+
- Container: `fixed top-0 left-0 w-full z-50 bg-white h-[72px]`
|
|
32
|
+
- Inner flex row: `flex items-center h-[72px] px-4 gap-6`
|
|
33
|
+
- `px-4` = 16 px left/right (Figma X:16)
|
|
34
|
+
- `gap-6` = 24 px between hamburger and logo (derived from Figma: logo X:64 − hamburger X:16 − hamburger W:24 = 24 px)
|
|
35
|
+
- Hamburger button: `w-[24px] h-[24px]` at X:16, Y:24
|
|
36
|
+
- Rate logo: `<img src="./assets/Rate_Logo.svg" className="h-[24px] w-auto" />`
|
|
37
|
+
- Height 24 px → equal 24 px top/bottom padding in 72 px header (`items-center` on h-[72px])
|
|
38
|
+
- SVG scales to natural 58.5 px wide — matches Figma Logos component W:58.5 Hug H:24 Hug
|
|
39
|
+
- No `border-b`, no `shadow` on header (Figma has none)
|
|
40
|
+
|
|
41
|
+
**Drawer (MobileNav):**
|
|
42
|
+
- Logo in drawer: `w-[117px] h-[72px]` (3× the header logo, same `rateDark1.png`)
|
|
43
|
+
- Slide-in from left via framer-motion spring animation
|
|
44
|
+
|
|
45
|
+
## Gotchas / known issues
|
|
46
|
+
|
|
47
|
+
- **Use `Rate_Logo.svg`, not `rateDark1.png` in the header.** The PNG has a 1.625:1 aspect ratio; at `h-[24px]` it renders only 39 px wide. The Figma Logos component is 58.5 × 24 px — only the SVG export matches this.
|
|
48
|
+
- **`EnvironmentBadge` is intentionally kept** in `App.tsx` — it is a dev/QA tool and must not be removed.
|
|
49
|
+
- **Page content top offset**: pages behind the fixed header need `mt-[72px]` to avoid overlap. The `allowedPaths` list in `MobileNavToggle` controls which routes get the nav shell.
|
|
50
|
+
|
|
51
|
+
## Change history
|
|
52
|
+
|
|
53
|
+
- 2026-06-24 — Switched header logo to Rate_Logo.svg; fixed hamburger/logo spacing to match Figma (px-4, gap-6, h-[72px] items-center); removed border-b/shadow from header (bala)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Service Card Component
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-view
|
|
5
|
+
project: TOGa View Frontend
|
|
6
|
+
client: rate
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-24
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- toga2-view/src/components/ServiceCard/ServiceCard.tsx
|
|
13
|
+
- toga2-view/src/pages/Services/view/ServicesPage.tsx
|
|
14
|
+
- toga2-view/src/pages/Home/view/HomePage.tsx
|
|
15
|
+
- toga2-view/src/constants/bundleConstants.ts
|
|
16
|
+
- toga2-view/src/pages/Services/viewModels/DUMMYFIELDS/SERVICESDUMMYFIELDS.json
|
|
17
|
+
related:
|
|
18
|
+
- clients/rate/profile.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
The `ServiceCard` component renders a single service subscription (tech support or home warranty) on both the Home and Services pages. It shows a hero image, title, Active badge, contract data rows, and a Manage button. Display names are normalised from raw API titles via a data-driven mapping.
|
|
24
|
+
|
|
25
|
+
## Key files / entry points
|
|
26
|
+
|
|
27
|
+
- `ServiceCard.tsx` — presentational component; all sizing/spacing must match Figma node `9203-10283`
|
|
28
|
+
- `ServicesPage.tsx` — renders cards in a `flex flex-col gap-4 p-4` section (16 px uniform padding)
|
|
29
|
+
- `HomePage.tsx` — renders cards inside `DynamicSection`; content wrapper must have **no** extra `px-4` (outer `dynamicSectionContainer` already supplies 16 px)
|
|
30
|
+
- `bundleConstants.ts` — exports `mapServiceTitle()` which reads from `SERVICESDUMMYFIELDS.json`
|
|
31
|
+
- `SERVICESDUMMYFIELDS.json` — `serviceTitleMap` array; add/rename display names here without touching code
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
1. View model calls `mapServiceTitle(bs.title)` before setting state — raw API title becomes the branded display name.
|
|
36
|
+
2. `mapServiceTitle` iterates `serviceTitleMap` in the JSON, checking each entry's `keywords` array (lowercase includes-match). First match wins; unmatched titles pass through unchanged.
|
|
37
|
+
3. `ServiceCard` renders with `bg-[#e9eff2] rounded-[8px] overflow-clip` (no border, no hover shadow).
|
|
38
|
+
4. Hero image uses `aspect-[1482/604]` (40.75 % height ratio) — not the old padding-bottom hack.
|
|
39
|
+
5. Card body: `flex flex-col gap-5 p-4` (gap = 20 px, padding = 16 px all sides per Figma).
|
|
40
|
+
6. Data section (divider + rows): `flex flex-col gap-3` (12 px between divider and rows).
|
|
41
|
+
7. Plan/Renews column: explicit `w-[91px]` — not `w-24` (96 px).
|
|
42
|
+
8. Manage button: `p-3 rounded-full` (12 px uniform padding, `rounded-[99px]` equivalent).
|
|
43
|
+
9. Divider: `border-t border-gray-300` (Figma token `Gray/gray-300`).
|
|
44
|
+
|
|
45
|
+
## Data model
|
|
46
|
+
|
|
47
|
+
- `Client_Rate.Items.title` — raw service title (e.g. `1 Year Unlimited Tech Support - Annual`)
|
|
48
|
+
- Display names live in `SERVICESDUMMYFIELDS.json → serviceTitleMap`
|
|
49
|
+
- Current mappings:
|
|
50
|
+
- `"1 year unlimited tech support - annual"` → `Rate Whole Home Tech Services`
|
|
51
|
+
- `"whole home warranty - monthly"` → `Rate Whole Home Warranty`
|
|
52
|
+
|
|
53
|
+
## Client variations
|
|
54
|
+
|
|
55
|
+
Rate-specific — title mapping and image paths are Rate-only. The `ServiceCard` component itself is generic.
|
|
56
|
+
|
|
57
|
+
## Gotchas / known issues
|
|
58
|
+
|
|
59
|
+
- **Double padding on Home page**: `dynamicSectionContainer` has `px-4`; the services content wrapper inside `DynamicSection` must NOT add another `px-4` or cards appear too narrow.
|
|
60
|
+
- **`rounded-lg` ≠ exact Figma match**: use `rounded-[8px]` explicitly — the Tailwind config extends `borderRadius` with custom values that could shift defaults.
|
|
61
|
+
- **`overflow-hidden` vs `overflow-clip`**: Figma uses CSS `overflow: clip`; use Tailwind `overflow-clip` to match exactly.
|
|
62
|
+
- **`mapServiceTitle` uses the original API title for image matching** — pass `bs.title` (not the mapped title) to `determineServiceImage` so keyword matching still works.
|
|
63
|
+
|
|
64
|
+
## Change history
|
|
65
|
+
|
|
66
|
+
- 2026-06-24 — Aligned all spacing/radius/overflow to Figma node 9203-10283; added data-driven title mapping from SERVICESDUMMYFIELDS.json; fixed double-padding on Home page (bala)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Notification/Email.php, dbchanges2/Core/2026-05-21 - Monitors.sql |
|
|
12
12
|
| [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, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
13
13
|
| [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 |
|
|
14
|
+
| [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 |
|
|
14
15
|
| [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 |
|
|
15
16
|
| [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 |
|
|
16
17
|
| [Teams Meeting Transcript Export](features/teams-transcript-export.md) | `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for Teams meeting transcripts produced by a set of organizers, classifies ea | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
|
|
@@ -7,7 +7,7 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-06-18
|
|
10
|
-
owners: [jcardinal, dfranks]
|
|
10
|
+
owners: [jcardinal, dfranks, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/
|
|
13
13
|
- worker2/Controller/Index.php
|
|
@@ -34,7 +34,10 @@ Lambda code changes are needed** — you just create the PHP file.
|
|
|
34
34
|
- Method: `MethodName`
|
|
35
35
|
- Example: `Client/Acme/ImportData` → `Worker/Client/Acme.php`, `_Worker_Client_Acme::ImportData`.
|
|
36
36
|
2. **Parameters** — name + PHP type (`string`/`int`/`bool`/`array`/`float`); optional ones
|
|
37
|
-
get PHP defaults. The framework spreads the `parameters`
|
|
37
|
+
get PHP defaults. The framework spreads the **string-keyed** `parameters` array into the method
|
|
38
|
+
(`$class::$method(...$parameters)`), which PHP binds as **named arguments** — so each method
|
|
39
|
+
parameter name *is* the queue's parameter contract. Do **not** collapse a multi-param action
|
|
40
|
+
into a single `array` argument: named-argument dispatch then has no key to bind to and breaks.
|
|
38
41
|
3. **Return** — return `string`; `json_encode()` structured data.
|
|
39
42
|
4. **`initialize()`** — optional `public static function initialize()` the framework calls
|
|
40
43
|
automatically before any method in the class (register DB connections / shared setup).
|
|
@@ -160,6 +163,9 @@ be reattempted.
|
|
|
160
163
|
commit-before-SQS transaction pattern that the worker relies on.
|
|
161
164
|
|
|
162
165
|
## Change history
|
|
166
|
+
- 2026-06-24 — Clarified that the dispatcher spreads the string-keyed `parameters` as PHP **named
|
|
167
|
+
arguments** (`$class::$method(...$parameters)`), so parameter names are the queue contract — do
|
|
168
|
+
not collapse a multi-param action into a single `array` arg. (mhammontree)
|
|
163
169
|
- 2026-06-18 — Documented worker-action **exception/retry semantics**: dispatcher catches `Throwable` →
|
|
164
170
|
`isSuccess=0`+`failureReason`, **always HTTP 200, no DLQ, no auto-retry**; throwing surfaces a failure
|
|
165
171
|
but does not re-run — retry via `_Worker::runTask` re-enqueue (attempt-counter), in-process loop, or
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: DB-Driven Notification (Internal) Email
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-24
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Notification/Email.php
|
|
13
|
+
- _underscore/Model/Client/EmailTemplate.php
|
|
14
|
+
- dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql
|
|
15
|
+
- dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql
|
|
16
|
+
related:
|
|
17
|
+
- ../../_underscore/features/email-template-sending.md
|
|
18
|
+
- ./creating-worker-actions.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated,
|
|
24
|
+
not client-facing transactional mail) are sent through one worker action,
|
|
25
|
+
`_Worker_Notification_Email::Send(...)`, which wraps a plain subject/body in the client's
|
|
26
|
+
**DB-stored branded shell** before sending via `_Email`. The branded shell used to be a
|
|
27
|
+
hardcoded PHP class (`_Email_Template`, now **deleted**); it now lives in the client DB as a
|
|
28
|
+
reserved `EmailTemplates` row so the branding can change without a code deploy. This is a
|
|
29
|
+
**shared/core 2.0 mechanism** — every client inherits the wrapper row from the dbchanges2
|
|
30
|
+
`Client/` baseline; it is not specific to any one tenant (`True` is only the pilot tenant the
|
|
31
|
+
migration backfilled first).
|
|
32
|
+
|
|
33
|
+
## Key files / entry points
|
|
34
|
+
|
|
35
|
+
- `worker2/Worker/Notification/Email.php` — `abstract _Worker_Notification_Email`. One method:
|
|
36
|
+
`Send(string $clientIdentifier, string $subject, string $body, string|array $to=[],
|
|
37
|
+
string|array $cc=[], string|array $bcc=[], string $fromEmail='donotreply@togatech.com',
|
|
38
|
+
string $fromName='TOGA Technology')`. The entry point. Self-registers the client DB (no
|
|
39
|
+
`initialize()` — see below), wraps the body, and sends.
|
|
40
|
+
- `_underscore/Model/Client/EmailTemplate.php` — supplies `WRAPPER_UUID` and
|
|
41
|
+
`renderWrappedBody($subject, $body)`. Documented in
|
|
42
|
+
[`email-template-sending.md`](../../_underscore/features/email-template-sending.md).
|
|
43
|
+
- `dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql` — blank-client baseline; seeds the
|
|
44
|
+
wrapper row so **every new client** inherits it.
|
|
45
|
+
- `dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql` — backfills the pilot tenant.
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
1. **Validate the identifier.** `$clientIdentifier` is interpolated into a schema name
|
|
50
|
+
(`Client_<id>`), so `Send()` first rejects anything that isn't `^[A-Za-z0-9]+$` (matches
|
|
51
|
+
`Core.Clients.clientIdentifier`, e.g. `True`, `CompassCanada`). This is the injection guard —
|
|
52
|
+
keep it.
|
|
53
|
+
2. **Register the client DB under the `DB_CLIENT` alias** via
|
|
54
|
+
`_Database::register('Client_'.$id, _Config::databaseClient(...), …, _underscore::DB_CLIENT)`.
|
|
55
|
+
This points the process-global `Client` alias at the concrete `Client_<id>` schema so the
|
|
56
|
+
`DATABASE = DB_CLIENT` model resolves to the right tenant. Only the `Client` alias is
|
|
57
|
+
registered — **not** `DB_CLIENT_LOGS` (see gotchas).
|
|
58
|
+
3. **Wrap the body.** `_Model_Client_EmailTemplate::renderWrappedBody($subject, $body)` loads the
|
|
59
|
+
active wrapper row (uuid = `WRAPPER_UUID`) and `strtr()`-substitutes `{subject}`/`{body}`.
|
|
60
|
+
Returns `null` if there is no active wrapper row.
|
|
61
|
+
4. **Graceful degrade.** If `renderWrappedBody` returns `null`, `Send()` `error_log()`s the
|
|
62
|
+
missing wrapper (with the uuid + client) and sends the **raw, unwrapped** body. These are
|
|
63
|
+
internal alerts — delivering unbranded mail beats dropping the alert. (Revisit if this path is
|
|
64
|
+
ever reused for client-facing email, where branding should be guaranteed.)
|
|
65
|
+
5. **Send** via a plain `_Email`: `setClientIdentifier`, `addTo/Cc/Bcc`, `setFrom`,
|
|
66
|
+
`setSubject`, `setBody($wrapped ?? $raw)`, `send()`.
|
|
67
|
+
|
|
68
|
+
### Enqueuing it
|
|
69
|
+
|
|
70
|
+
`_Worker::runTask('Notification/Email/Send', ['clientIdentifier'=>…, 'subject'=>…, 'body'=>…, …])`
|
|
71
|
+
inserts a WorkerJob. The dispatcher (`worker2/Controller/Index.php`) resolves
|
|
72
|
+
`'Notification/Email/Send'` → `_Worker_Notification_Email::Send`, then spreads the **string-keyed**
|
|
73
|
+
parameters array as PHP **named arguments**. So the method's named parameters *are* the queue's
|
|
74
|
+
parameter contract — see [`creating-worker-actions.md`](./creating-worker-actions.md).
|
|
75
|
+
|
|
76
|
+
**Proven caller:** `_Worker_Team_GitHub::Merge()` (`worker2/Worker/Team/Github.php`) enqueues
|
|
77
|
+
`Notification/Email/Send` on a git merge conflict, so merge-conflict alerts now go out branded.
|
|
78
|
+
|
|
79
|
+
## The wrapper row (data model)
|
|
80
|
+
|
|
81
|
+
A single reserved `EmailTemplates` row per client DB:
|
|
82
|
+
|
|
83
|
+
- **uuid** = `_Model_Client_EmailTemplate::WRAPPER_UUID` (`11111111-1111-4111-8111-111111111111`).
|
|
84
|
+
This literal is the contract between the model lookup and the seed SQL — both must use it.
|
|
85
|
+
- **body** holds the full branded HTML with `{subject}` and `{body}` placeholders.
|
|
86
|
+
- Seeded once in `dbchanges2/Client/…` (baseline, so new clients inherit it) and backfilled per
|
|
87
|
+
existing client (`dbchanges2/Client_True/…` did the pilot). In the seed SQL the CSS font names
|
|
88
|
+
are left **unquoted** so the `INSERT` has no single quotes to escape.
|
|
89
|
+
|
|
90
|
+
## Branded header — Outlook bulletproofing (load-bearing recipe)
|
|
91
|
+
|
|
92
|
+
The wrapper's header must render the TOGA line-art logo
|
|
93
|
+
(`TOGA-TECHNOLOGY-Header-Logo-Vector.png`, sized **exactly 600×69** = the header box) in
|
|
94
|
+
**both** classic Outlook and new Outlook/Gmail/Apple, **without** the logo rendering twice. The
|
|
95
|
+
working recipe — verified in classic + new Outlook:
|
|
96
|
+
|
|
97
|
+
- **Classic Outlook:** VML — `<v:rect>` with `<v:fill type="frame" src=…>` and a `<v:textbox>`
|
|
98
|
+
holding the title.
|
|
99
|
+
- **New Outlook / Gmail / Apple Mail:** the HTML `background=` **attribute** on the cell.
|
|
100
|
+
- **No CSS `background-image`** on the header — only `background-color`. New Outlook would
|
|
101
|
+
otherwise render the CSS image *and* the `background=` attribute = the double-render.
|
|
102
|
+
- **Horizontal inset belongs on the title `<div>`** (`padding: 22px 90px`), **not** on the `<td>`.
|
|
103
|
+
Putting padding on the `<td>` squeezes the VML/background image inward in classic Outlook so it
|
|
104
|
+
no longer fills the full header width.
|
|
105
|
+
- The **footer** logo is a normal `<img>` (no VML needed).
|
|
106
|
+
|
|
107
|
+
## Gotchas / known issues
|
|
108
|
+
|
|
109
|
+
- **No `initialize()` on this action.** `_Worker_Notification_Email` does not define
|
|
110
|
+
`initialize()`, so the dispatcher never calls one — `Send()` registers its own client DB inline.
|
|
111
|
+
(The dispatcher only calls `initialize()` when the class defines it.)
|
|
112
|
+
- **A `DATABASE = DB_CLIENT` model resolves only after the `Client` alias is registered** to the
|
|
113
|
+
concrete `Client_<x>` schema. `renderWrappedBody()` will load the wrong (or no) tenant if you
|
|
114
|
+
call it before `_Database::register(..., _underscore::DB_CLIENT)`. Authoritative pattern:
|
|
115
|
+
`_Worker_Ai_Bdr_Netsuite::initialize()`.
|
|
116
|
+
- **Do not register `DB_CLIENT_LOGS` for this path.** `_Email` logs to CloudWatch, not a Logs DB,
|
|
117
|
+
so the notification path never needs it — and `[databaseLogs]` only exists in `production.ini`,
|
|
118
|
+
so registering it in dev/beta throws.
|
|
119
|
+
- **Debug mode redirects, doesn't suppress.** When `_Email::isDebugMode()` is on, `_Email::send()`
|
|
120
|
+
redirects the recipient to `[_underscore] send_debug_emails_to` and prepends a
|
|
121
|
+
"DEVELOPMENT MODE - Intended Recipients" banner — the email still sends.
|
|
122
|
+
- **Missing wrapper row → unbranded send, logged.** If a client has no active wrapper row the alert
|
|
123
|
+
still goes out (raw body) and an `error_log` line records the missing uuid. Branding is *not*
|
|
124
|
+
guaranteed on this path by design (internal mail). Don't rely on it for client-facing email.
|
|
125
|
+
- **`{subject}`/`{body}` substitution is `strtr`, not sequential `str_replace`.** All placeholders
|
|
126
|
+
are replaced simultaneously, so a `{body}` literal inside the subject can't be re-expanded.
|
|
127
|
+
|
|
128
|
+
## Change history
|
|
129
|
+
- 2026-06-24 — Built the DB-driven notification-email mechanism (TRUE-79240): new
|
|
130
|
+
`_Worker_Notification_Email::Send` entry point; branded shell moved out of code (deleted
|
|
131
|
+
`_underscore/Email/Template.php`, which also carried the 1.0 double-render bug) into a reserved
|
|
132
|
+
per-client `EmailTemplates` wrapper row seeded by dbchanges2 (`Client/` baseline +
|
|
133
|
+
`Client_True/` backfill). Missing wrapper degrades to an unbranded send (logged). Documented the
|
|
134
|
+
Outlook-bulletproof header recipe (VML + `background=` attribute, no CSS background-image, inset
|
|
135
|
+
on the title `<div>`). Merge-conflict alerts (`_Worker_Team_GitHub::Merge`) now use this path.
|
|
136
|
+
(mhammontree)
|
|
137
|
+
|
|
138
|
+
## Related docs
|
|
139
|
+
- [`2.0/apps/_underscore/features/email-template-sending.md`](../../_underscore/features/email-template-sending.md)
|
|
140
|
+
— the `_Model_Client_EmailTemplate` model, including `WRAPPER_UUID` + `renderWrappedBody()`.
|
|
141
|
+
- [`creating-worker-actions.md`](./creating-worker-actions.md) — worker action dispatch +
|
|
142
|
+
named-argument parameter contract.
|
|
143
|
+
- `1.0/apps/library/features/email-templates.md` — the **1.0** `App_Email_Template` (a different
|
|
144
|
+
framework/mechanism; cross-link only, do not conflate).
|
package/knowledge/INDEX.md
CHANGED
|
@@ -16,12 +16,12 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
16
16
|
## 2.0 framework
|
|
17
17
|
|
|
18
18
|
- **_underscore** (_Underscore) _(framework core)_ — 12 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
19
|
-
- **worker2** (Worker) —
|
|
19
|
+
- **worker2** (Worker) — 14 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
20
20
|
- **api2** (API) — 6 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
21
21
|
- **dbchanges2** (Database Changes) _(framework core)_ — 2 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
22
22
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
23
23
|
- **saml** (SAML SSO Gateway) — 2 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
24
|
-
- **toga2-view** (TOGa View Frontend) —
|
|
24
|
+
- **toga2-view** (TOGa View Frontend) — 4 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
25
25
|
- **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
|
|
26
26
|
- **talos** (TOGa IQ) — 6 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
|
|
27
27
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
package/package.json
CHANGED