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.
@@ -6,13 +6,14 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-18
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` JSON as positional arguments.
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).
@@ -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) — 13 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.192",
3
+ "version": "1.0.193",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",