toga-ai 1.0.136 → 1.0.138

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.
@@ -4,5 +4,6 @@
4
4
  |-----|---------|-------|
5
5
  | [Library (1.0 Framework) Architecture](architecture.md) | `library` is the shared library repository for **all 1.0 (legacy) applications** — the `App_` framework. | library/_.php, library/app/, library/browser/ |
6
6
  | [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
7
+ | [Branded HTML Email Templates (App_Email_Template)](features/email-templates.md) | `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in the 1.0 (`App_`) framework. | library/app/email/template.php, library/app/email/agilant.php |
7
8
  | [NetSuite SuiteQL/REST API Reference](features/netsuite-suiteql-api-reference.md) | General working reference for the Agilant NetSuite integration: how to authenticate, how SuiteQL behaves, and the confirmed schema of the tables/columns/codes w | library/app/api/netsuite/rest.php, library/ssl/netsuite_ec_key.pem |
8
9
  | [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php |
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Branded HTML Email Templates (App_Email_Template)
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-18
10
+ owners: ["mhammontree"]
11
+ files:
12
+ - library/app/email/template.php
13
+ - library/app/email/agilant.php
14
+ related:
15
+ - ../architecture.md
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in
21
+ the 1.0 (`App_`) framework. Brand-specific subclasses under `app/email/` (e.g.
22
+ `App_Email_Agilant`, `App_Email_Dukefarms`, `App_Email_Hartreepartners`) set a handful of
23
+ brand properties in their constructor; the base `renderHtml()` assembles the full email
24
+ (header bar + body + footer bar). Because it lives in `library` **core**, any change here ships
25
+ to **every 1.0 app that sends these emails** (worker crons, togadesk, etc.) — verify across
26
+ apps, not just one.
27
+
28
+ `App_Email_Agilant` is the **default TOGA Technology brand** despite its legacy class name —
29
+ it sends as `TOGA Technology <noreply@togatech.com>`.
30
+
31
+ ## Key files / entry points
32
+
33
+ - `app/email/template.php` — `App_Email_Template extends App_Email`. Owns the brand properties
34
+ and `renderHtml()` (the whole email markup) and `send()`.
35
+ - `app/email/agilant.php` — `App_Email_Agilant extends App_Email_Template`. The TOGA-branded
36
+ defaults. Other brand subclasses follow the same shape.
37
+
38
+ ## How it works
39
+
40
+ A subclass sets brand properties in its constructor; `send()` calls `renderHtml()` (when
41
+ `sendWithTemplate` is on) to build the body, then defers to `App_Email::send()`.
42
+
43
+ Properties consumed by `renderHtml()`:
44
+
45
+ - `companyName` — used as the footer logo `alt`.
46
+ - `primaryColorHex`, `gradientColorA`, `gradientColorB` — header/footer colors.
47
+ - `logoImageUrl` — the **footer** logo image (rendered at 35×35 in the footer gradient bar).
48
+ If empty, no footer bar renders.
49
+ - `logoLinkToUrl` — href wrapping the footer logo.
50
+ - `includeHeader` — toggles the header bar.
51
+ - `heroImageUrl`, `heroHeight` — optional hero (set via `App_Email_Template`; see DEV-2094).
52
+
53
+ `renderHtml()` builds three rows in a 600px table:
54
+ 1. **Header bar** — a gradient `<td>` whose CSS `background-image` layers the brand line-art
55
+ (`TOGA-TECHNOLOGY-Header-Logo-Vector.png`) over a `linear-gradient`, with the email
56
+ **subject** centered in white as the heading.
57
+ 2. **Body** — `getBody()` inside a white content cell.
58
+ 3. **Footer bar** — a gradient `<td>` containing only the `logoImageUrl` (linked to
59
+ `logoLinkToUrl`).
60
+
61
+ ## Data model
62
+
63
+ None — pure presentation. Brand values are hardcoded in each subclass constructor.
64
+
65
+ ## Client variations
66
+
67
+ One subclass per brand under `app/email/`. They differ only in the brand properties above
68
+ (colors, logo URLs, sender). Some client emails (e.g. `toga.php`, `officedepot.php`) instead
69
+ pull a per-client logo from the client's `logoDocumentId` rather than a static URL.
70
+
71
+ ## Brand assets (TOGA Technology rebrand)
72
+
73
+ - Header line-art (full-bleed background pattern, blue on transparent):
74
+ `https://common-host.s3.us-west-2.amazonaws.com/logos/TOGA-TECHNOLOGY-Header-Logo-Vector.png`
75
+ - Footer symbol (white-on-transparent, 35×36 — shows on the dark gradient footer bar):
76
+ `https://common-host.s3.us-west-2.amazonaws.com/logos/TOGA-TECHNOLOGY-Logo-Symbol.png`
77
+
78
+ Note the host is `s3.us-west-2` (dot), not the older `s3-us-west-2` (hyphen) used by the
79
+ retired Agilant assets.
80
+
81
+ ## Gotchas / known issues
82
+
83
+ - **New Outlook double-renders a cell that has BOTH a `background="…"` HTML attribute and a CSS
84
+ `background-image`.** It paints the attribute image (native size, top-left) *and* the CSS
85
+ `cover` image (offset) — producing a duplicated, shifted header bar with the title spilling
86
+ out. Fix: drive backgrounds from **CSS only** on the cell (and VML for classic Outlook if you
87
+ need the gradient there). Do not set the legacy `background=` attribute alongside CSS.
88
+ - **No VML fallback in the current header/footer** — classic desktop Outlook (the Word engine)
89
+ can't render CSS gradients/background-images, so it falls back to the solid `bgcolor`
90
+ (`#2B47AD`) + white text (no gradient, no line-art). Acceptable, just flatter. Add a VML
91
+ `v:rect`/`v:fill` block if the gradient/line-art must appear in classic Outlook.
92
+ - **Footer logo must be light/white on transparent** — it sits on the dark blue gradient bar.
93
+ A dark logo disappears.
94
+ - **`footerText` and `socialMediaImageUrlsToLinks` are vestigial.** The properties still exist
95
+ on `App_Email_Template`, but the simplified `renderHtml()` (TRUE-79240) no longer renders
96
+ them — the footer is **logo-only**. Setting them has no effect; older subclasses may still
97
+ assign them harmlessly.
98
+ - **Verifying a fix:** these emails are sent by deployed code, so an email already in someone's
99
+ inbox reflects the code that was live when it was sent. Confirm a template change with a
100
+ **fresh send after deploy**, viewed across new Outlook / classic Outlook / Gmail / Apple Mail
101
+ — not against a pre-deploy message.
102
+
103
+ ## Change history
104
+
105
+ - 2026-06-18 — Header double-render fix: removed the redundant `background="…vector.png"`
106
+ attribute from the header cell (new Outlook was painting it twice, offset from the CSS
107
+ background). CSS-only backgrounds now. (mhammontree)
108
+ - 2026-06-18 — TRUE-79240: rebranded `App_Email_Agilant` footer to TOGA — `logoImageUrl` →
109
+ `TOGA-TECHNOLOGY-Logo-Symbol.png`, `logoLinkToUrl` → `https://togatech.com`; removed the
110
+ Agilant footer address text and social-media icons (footer is logo-only per Figma).
111
+ (mhammontree)
112
+
113
+ ## Related docs
114
+
115
+ - [Library Architecture](../architecture.md)
@@ -8,5 +8,5 @@
8
8
  | [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
9
9
  | [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 |
10
10
  | [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 |
11
- | [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, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
11
+ | [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, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
12
12
  | [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 |
@@ -6,12 +6,16 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-17
9
+ updated: 2026-06-18
10
10
  owners: ["dfranks"]
11
11
  files:
12
12
  - worker2/Worker/Netsuite/SalesOrder.php
13
13
  - worker2/Worker/Netsuite.php
14
14
  - test/@dave/probe_salesorder_rest_shape.php
15
+ - test/@dave/probe_open_order_lines.php
16
+ - test/@dave/check_so_status.php
17
+ - test/@dave/check_so_history.php
18
+ - test/@dave/probe_so_rest_lines.php
15
19
  - worker/crons/toga2/forecast2/import_open_orders.php
16
20
  - worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php
17
21
  related:
@@ -77,6 +81,25 @@ None — uniform (platform-wide Forecast2 sync).
77
81
 
78
82
  ## Gotchas / known issues
79
83
 
84
+ - **Async-recalc race → a transiently-empty REST read PURGES a genuinely-open order (silent data loss).**
85
+ The open-status gate is not the only way rows get deleted: on an **open** order, if the REST GET
86
+ momentarily returns **zero importable open lines**, `syncOpenLines` runs its reconcile-delete with an
87
+ empty kept-set and wipes every existing row — there is no separate "removeAll", but the effect is the
88
+ same. This fires when NetSuite's own **asynchronous `-System-` line recompute** (e.g. the open
89
+ gross-profit pass, `CUSTCOL_OPEN_GP`) is still in flight at the instant the webhook's REST GET runs —
90
+ the record reads back open but with its open lines not yet materialized. The webhook is often *triggered
91
+ by that very recompute*, so the GET races it. **Confirmed live 2026-06-18 on SO 7151688** (tranId
92
+ 279345): 4 edit webhooks at 14:02:35–48 CDT all succeeded with **0 rows**, while NetSuite's `-System-`
93
+ open-GP recompute wrote the line values at the same 14:02:30–45 window (NS Eastern = worker Central +1h).
94
+ A manual re-sync 22 min later imported all 4 lines correctly. Nothing self-heals it — there is no retry,
95
+ and the batch cron is being decommissioned. **Fix direction (not yet implemented):** never let "open
96
+ status + zero importable lines" trigger a destructive delete — re-fetch after a short delay, or treat it
97
+ as retryable (`throw` → `isSuccess=0` → re-runs) — and only let a **confidently-closed** status
98
+ (Billed / Closed / Cancelled) purge. Diagnose with `check_so_history.php` (systemnote status timeline)
99
+ + `check_so_status.php` (current REST status) + `probe_so_rest_lines.php` (REST `item.items` as the
100
+ handler sees them). NB: `App_ApiTransaction::execute(false)` returns a raw JSON **string** for a record
101
+ GET — `json_decode` it directly; double-encoding it (`json_decode(json_encode($resp))`) silently yields
102
+ an empty object and a false "empty record" reading.
80
103
  - **REST shape ≠ SOAP shape.** The cron reads the SOAP-shim shape; this handler reads the REST record
81
104
  (`status->refName`, line `quantityBilled`, `class->refName`, `entity->id`, `salesRep->id`,
82
105
  `shippingCost`). Verified against live orders via `test/@dave/probe_salesorder_rest_shape.php`.
@@ -131,6 +154,15 @@ and are a candidate to extract into a shared `_Component_Forecast_Db` before the
131
154
 
132
155
  ## Change history
133
156
 
157
+ - 2026-06-18 — **Verified live in production** after the SalesOrder enqueuer left Testing. 14 orders
158
+ reconciled against NetSuite SuiteQL (`probe_open_order_lines.php`); 13 at $0.00 delta. Found one
159
+ miss — SO 7151688 (open, ~$20.31, 0 rows) — traced to the **async-recalc race** above (see new
160
+ gotcha): the order was approved (Pending Approval → Pending Fulfillment) *before* the enqueuer was
161
+ released (so the approval itself never enqueued — one-time cutover artifact), then NetSuite's
162
+ `-System-` open-GP recompute fired the edit webhooks and the handler's REST GET raced that recompute,
163
+ reading zero open lines → reconcile-delete. A manual re-sync corrected it. Added diagnostics
164
+ `check_so_status.php`, `check_so_history.php`, `probe_so_rest_lines.php`. No code change yet — fix
165
+ direction recorded in the gotcha. (dfranks)
134
166
  - 2026-06-17 — **Fixed dropped zero-revenue/open-cost lines.** REST omits `rate` for $0 lines, and the
135
167
  `if (!is_numeric($rate)) continue;` guard was skipping them — but the cron inserts these (profit =
136
168
  −qtyOpen × unitCost; 575 such rows in prod). Now defaults missing/non-numeric rate to 0 so the both-zero
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 4 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 5 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 10 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **togadesk** (TOGa Desk) — 7 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
10
10
  - **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.136",
3
+ "version": "1.0.138",
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",