toga-ai 1.0.192 → 1.0.193
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/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 +1 -1
- 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;
|
|
@@ -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,7 +16,7 @@ _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)
|
package/package.json
CHANGED