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.
- package/knowledge/1.0/apps/library/INDEX.md +1 -0
- package/knowledge/1.0/apps/library/features/email-templates.md +115 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -1
- package/knowledge/2.0/apps/worker2/features/netsuite-salesorder-open-orders-sync.md +33 -1
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -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-
|
|
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
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ —
|
|
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