toga-ai 1.0.705 → 1.0.707
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-queue-attachments.md +6 -2
- package/knowledge/1.0/apps/library/features/email-test-mode-and-write-free-sends.md +128 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-canada/profile.md +10 -1
- package/knowledge/clients/compass-usa/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/features/mits-sales-order-transmission-alerting.md +89 -28
- package/knowledge/clients/prudential/INDEX.md +1 -1
- package/knowledge/clients/prudential/features/order-email-pdf-attachments.md +120 -17
- package/knowledge/clients/prudential/features/transmit-ordershipped-email.md +19 -1
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| [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 |
|
|
12
12
|
| [App_Email Queued Sending & Attachments (Common.EmailsQueued)](features/email-queue-attachments.md) | `App_Email::send()` can either send **inline** (PHPMailer talks to SES right there) or **queue** the message: `base64(serialize($this))` is inserted into `Commo | library/app/email.php, library/phpmailer/class.phpmailer.php, worker/crons/notifications/infrastructure/send_emails.php, worker/crons/notifications/covid/send_covid_pending_vaccination_approval.php, togadesk/desk/includes/functions.php |
|
|
13
13
|
| [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 |
|
|
14
|
+
| [App_Email Side Effects & Test Mode (how to send a real email that writes nothing)](features/email-test-mode-and-write-free-sends.md) | `App_Email::send()` is **not** side-effect free. | library/app/email.php, library/app/framework.php |
|
|
14
15
|
| [Error Capture in 1.0 (App_Error_Capture → shared 2.0 Logs DB)](features/error-capture-1-0.md) | The 1.0 side of the platform error-reporting pipeline (TRUE-78188). | library/app/error/capture.php, library/app/error.php, library/app/exception/business.php, library/app/api/toga2.php, library/app/cloud.php, worker/config.worker.ini, worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php |
|
|
15
16
|
| [HTTP 500 Error Monitor (App_SystemMonitor_500Error) — and why its \"Error Type\" is not a diagnosis](features/http-500-error-monitor.md) | `App_SystemMonitor_500Error` (`library/app/systemmonitor/500error.php`, title **"HTTP 500 Error Alert"**) is the 1.0 system monitor that watches **`Logs.Api` fo | library/app/systemmonitor/500error.php, worker/crons/infrastructure/system_monitors.php, api2/Controller/Index.php, _underscore/Error.php |
|
|
16
17
|
| [1.0 MVC Page Pattern & New-App Skeleton](features/mvc-page-pattern-and-app-skeleton.md) | This is the **reusable recipe for standing up a new 1.0 (`App_`) application** and for adding pages to one — the folder-based MVC routing, the page lifecycle, t | library/app/framework.php, library/app/frameworkindex.php, library/app/mvc.php, library/app/database.php, library/app/model.php, library/app/config.php |
|
|
@@ -6,8 +6,8 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["mhammontree"]
|
|
9
|
+
updated: 2026-08-31
|
|
10
|
+
owners: ["mhammontree", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/email.php
|
|
13
13
|
- library/phpmailer/class.phpmailer.php
|
|
@@ -16,6 +16,7 @@ files:
|
|
|
16
16
|
- togadesk/desk/includes/functions.php
|
|
17
17
|
related:
|
|
18
18
|
- ./email-templates.md
|
|
19
|
+
- ./email-test-mode-and-write-free-sends.md
|
|
19
20
|
- ../architecture.md
|
|
20
21
|
- ../../togadesk/features/notifications.md
|
|
21
22
|
---
|
|
@@ -105,6 +106,9 @@ togadesk first, let the queue drain empty, *then* worker.
|
|
|
105
106
|
no DB, no SMTP, confirmed failing against pre-fix HEAD.
|
|
106
107
|
|
|
107
108
|
## Change history
|
|
109
|
+
- 2026-08-31 — Cross-linked the companion doc on `send()`'s other side effects (the `Logs.Email`
|
|
110
|
+
write and `setIsLogEnabled(false)`, the `OUTBOUND_EMAIL_ENABLED` queue branch) and on how
|
|
111
|
+
`test_mode` rewrites the recipient list to `dev_email_to` (bala)
|
|
108
112
|
- 2026-08-20 — TRUE-80952: queued attachments now embed base64 bytes (`embedAttachmentsForQueue()`
|
|
109
113
|
+ `AddStringAttachment`) instead of paths that do not exist on the drain host; added
|
|
110
114
|
`MAX_QUEUED_ATTACHMENT_BYTES` with inline-send fallback and a scoped warning guard around the
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: App_Email Side Effects & Test Mode (how to send a real email that writes nothing)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: library
|
|
5
|
+
project: Library
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-31
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- library/app/email.php
|
|
13
|
+
- library/app/framework.php
|
|
14
|
+
related:
|
|
15
|
+
- ./email-queue-attachments.md
|
|
16
|
+
- ./email-templates.md
|
|
17
|
+
- ../architecture.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
`App_Email::send()` is **not** side-effect free. By default it INSERTs a log row, and under
|
|
23
|
+
several ordinary conditions it INSERTs a queue row and sends nothing at all. Separately, it
|
|
24
|
+
**rewrites the recipient list** in test mode. Anyone writing a dry-run script, a template
|
|
25
|
+
preview, or a "let me just fire one test email" harness in the 1.0 tier needs both facts, or
|
|
26
|
+
they will either pollute a production table or wonder why the intended recipient never got the
|
|
27
|
+
mail.
|
|
28
|
+
|
|
29
|
+
Verified against `library/app/email.php` on 2026-08-31.
|
|
30
|
+
|
|
31
|
+
## What `send()` writes
|
|
32
|
+
|
|
33
|
+
| Write | When | How to suppress |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `Logs.Email` row (`App_Model_Logs_Email`) | every **inline** send, success or failure | `setIsLogEnabled(false)` |
|
|
36
|
+
| `Common.EmailsQueued` row (`App_Model_Common_EmailsQueued`) | `queueForSending` truthy, **or** outbound email is disabled and `sendEmailEvenIfOutboundEmailIsDisabled` is false | `setQueueForSending(false)` + `sendEmailEvenIfOutboundEmailIsDisabled = true` |
|
|
37
|
+
|
|
38
|
+
The queue-vs-send decision is a single condition (`email.php` ~L313):
|
|
39
|
+
|
|
40
|
+
```php
|
|
41
|
+
if ($this->queueForSending || (!$this->sendEmailEvenIfOutboundEmailIsDisabled && !$isOutboundEmailEnabled)) {
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`$isOutboundEmailEnabled` is read from the `Defaults` table
|
|
45
|
+
(`setting = 'OUTBOUND_EMAIL_ENABLED'`), so **an environment can silently convert your "send" into
|
|
46
|
+
a queue insert** with no code change on your side. The log write is inside the `else` branch, so a
|
|
47
|
+
queued message writes the queue row and **no** log row.
|
|
48
|
+
|
|
49
|
+
## The three switches for a genuinely write-free send
|
|
50
|
+
|
|
51
|
+
All three are required — any one of them left at its default reintroduces a write or swallows the
|
|
52
|
+
send:
|
|
53
|
+
|
|
54
|
+
1. `setQueueForSending(false)` — do not insert into `Common.EmailsQueued`.
|
|
55
|
+
2. `sendEmailEvenIfOutboundEmailIsDisabled = true` — do not fall into the queue branch just
|
|
56
|
+
because the `Defaults` flag is off.
|
|
57
|
+
3. `setIsLogEnabled(false)` — do not insert into `Logs.Email`.
|
|
58
|
+
|
|
59
|
+
## `App_Email` only ever *narrows* the recipient list
|
|
60
|
+
|
|
61
|
+
This is worth stating plainly because people assume the opposite when a test email "goes
|
|
62
|
+
somewhere unexpected". `send()` does exactly two things to the addresses you set:
|
|
63
|
+
|
|
64
|
+
- **Suppression** — every `to`/`cc`/`bcc` entry is dropped if
|
|
65
|
+
`App_Model_Common_EmailSuppressionList::isOkayToSendEmailTo()` says no.
|
|
66
|
+
- **Test mode** — if `forceInTestMode` is set **or** config `internal.test_mode == 1`, the whole
|
|
67
|
+
`to`/`cc`/`bcc` list is **discarded** and replaced with the single config value
|
|
68
|
+
`email.dev_email_to`. The body is prefixed with a red `DEVELOPMENT MODE - Intended Recipients:
|
|
69
|
+
…` banner naming who *would* have received it. With `test_mode = 1` and no `dev_email_to`
|
|
70
|
+
configured, `send()` **throws**.
|
|
71
|
+
|
|
72
|
+
There is **no** hidden bcc, no admin copy, no silent additional recipient anywhere in `send()`.
|
|
73
|
+
If someone received a test message, they were on your list or they are `dev_email_to`.
|
|
74
|
+
|
|
75
|
+
**Practical consequence:** a local/alpha/beta config has `test_mode = 1`, so *only* `dev_email_to`
|
|
76
|
+
ever receives your test. To have a second person actually receive a test message you must run the
|
|
77
|
+
script **on the worker server**, whose `config.worker.ini` has `test_mode = 0` — and at that point
|
|
78
|
+
your recipient list is real, so get it right.
|
|
79
|
+
|
|
80
|
+
## `cronInitialization()` writes too
|
|
81
|
+
|
|
82
|
+
`App_Framework::cronInitialization()` (`library/app/framework.php` ~L689) INSERTs a
|
|
83
|
+
`CronJobExecutions` row before your code runs (and UPDATEs it at the end). A write-free script
|
|
84
|
+
must **not** call it. It also does the two things you still need, so do them yourself:
|
|
85
|
+
|
|
86
|
+
```php
|
|
87
|
+
set_time_limit(900);
|
|
88
|
+
date_default_timezone_set('America/Chicago');
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Call `App_Framework_Worker::initialize()` (or the app's equivalent) for config/registry/DB, and
|
|
92
|
+
stop there.
|
|
93
|
+
|
|
94
|
+
## Recipe — a dry-run email script in the 1.0 worker tier
|
|
95
|
+
|
|
96
|
+
1. Bootstrap with `App_Framework_Worker::initialize()` only; set `set_time_limit()` and
|
|
97
|
+
`date_default_timezone_set()` yourself. **Do not** call `cronInitialization()`.
|
|
98
|
+
2. Read your sample data with a **single SELECT** — no models that `save()`, no api2 calls.
|
|
99
|
+
3. Build the mail exactly as the real code does (same template class, same subject, same S3
|
|
100
|
+
fetches) so you are testing the real body, not a paraphrase of it.
|
|
101
|
+
4. Set the three switches above.
|
|
102
|
+
5. Print, before sending, which attachments resolved and their byte sizes — a missing PDF is
|
|
103
|
+
otherwise invisible (see `email-queue-attachments.md`: PHPMailer 5 sends anyway).
|
|
104
|
+
|
|
105
|
+
Run it with the framework repos on the include path:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
php -d include_path=".;<library>;<_underscore>" <path/to/script>.php
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Gotchas
|
|
112
|
+
|
|
113
|
+
- **A "successful" local test proves less than it looks.** `test_mode = 1` collapses both the
|
|
114
|
+
queue path (see `email-queue-attachments.md`) and the recipient list. Two different behaviors
|
|
115
|
+
you cannot exercise locally.
|
|
116
|
+
- **Deleting the log write is not the same as being read-only.** `setIsLogEnabled(false)` only
|
|
117
|
+
suppresses `Logs.Email`; the queue insert has its own switch and fires *instead of* sending.
|
|
118
|
+
- **Dry-run harness scripts are throwaway.** Keep them out of the repo — they carry hardcoded
|
|
119
|
+
recipient lists and bypass the real gating. Build one, use it, delete it; document the
|
|
120
|
+
*technique* here rather than committing the file.
|
|
121
|
+
|
|
122
|
+
## Change history
|
|
123
|
+
- 2026-08-31 — Documented that `App_Email::send()` writes `Logs.Email` unless
|
|
124
|
+
`setIsLogEnabled(false)`, that the queue branch also fires when `OUTBOUND_EMAIL_ENABLED` is off,
|
|
125
|
+
that `cronInitialization()` INSERTs `CronJobExecutions`, and that `send()` only ever narrows
|
|
126
|
+
recipients (suppression list + `test_mode` → `dev_email_to` with a DEVELOPMENT MODE banner —
|
|
127
|
+
never adds one). Written up after building a throwaway write-free harness for the Prudential
|
|
128
|
+
order emails (bala)
|
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)_ — 23 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
8
|
- **worker** (Worker) — 31 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
|
|
10
10
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
|
@@ -16,7 +16,7 @@ project: _Underscore
|
|
|
16
16
|
client: compass-canada
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-08-
|
|
19
|
+
updated: 2026-08-31
|
|
20
20
|
owners: [jcardinal, bala, tcox, apeterson]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
@@ -28,6 +28,7 @@ related:
|
|
|
28
28
|
- ../compass-usa/features/stranded-approval-reassignment.md
|
|
29
29
|
- ../compass-usa/features/people-file-user-lifecycle.md
|
|
30
30
|
- ../compass-usa/features/isfulfillable-data-quality-and-type-rule.md
|
|
31
|
+
- ../compass-usa/features/mits-sales-order-transmission-alerting.md
|
|
31
32
|
- features/french-order-email-localization.md
|
|
32
33
|
- features/order-fulfillment-status-per-line.md
|
|
33
34
|
- features/item-fulfillment-tableviews.md
|
|
@@ -72,6 +73,14 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
72
73
|
- Worker crons for the G&T flow live under `worker/crons/toga2/compasscanada/workflow/`
|
|
73
74
|
(`1_…` transmit SOs to MITS, `2_…` transmit POs to vendors, `3_…` status from G&T cXML,
|
|
74
75
|
`4_…` import G&T ASNs).
|
|
76
|
+
- **⚠ MITS transmit alerting has diverged from Compass USA.** Canada's `1_…transmit…to_mits`
|
|
77
|
+
cron still raises all **three** of its failure alerts as in-cron `App_Email_Agilant` emails
|
|
78
|
+
(No Items Found / more than 15 items / Invalid response from MITS API), so changing who is
|
|
79
|
+
notified here **is** a code edit — whereas USA's rejection path now routes through the Logs
|
|
80
|
+
Issue/Event pipeline and is configured against the issue instead. Canada's "Invalid response"
|
|
81
|
+
email also `print_r`s the full payload (ship-to PII) into the body and has no dedupe. Recipients
|
|
82
|
+
are currently `devteam@togatech.com` + `vburks@togatech.com`. See
|
|
83
|
+
[MITS Sales-Order Transmit Alerting](../compass-usa/features/mits-sales-order-transmission-alerting.md).
|
|
75
84
|
|
|
76
85
|
## Storefront notes (toga2-commerce)
|
|
77
86
|
- Runs the same `toga2-commerce` storefront (2.0, consumes api2), with English + French cart
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
11
11
|
| [Compass MITS PO Transmission to Vendors](features/mits-po-transmission-to-vendors.md) | 2.0 | The 1.0 worker cron `2_transmit_mits_purchase_orders_to_vendors.php` transmits Compass PurchaseOrders to their vendors (Office Depot, Strategic Systems, Compass | worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compasscanada/workflow/2_transmit_mits_purchase_orders_to_vendors.php, library/app/client/compass.php |
|
|
12
12
|
| [Compass MITS PO rejection — zero line items (400 + Issue + PM alert email)](features/mits-po-zero-line-item-rejection.md) | 2.0 | MITS transmits Compass purchase orders that carry **`"purchaseOrderItems": []`** (real example: PO `50309196-1`, `mitsSalesOrder` `MR244630`). | _underscore/Model/Compass/PurchaseOrder.php, _underscore/Model/Compass/Usa/PurchaseOrder.php, _underscore/Model/Compass/Canada/PurchaseOrder.php, worker2/Worker/Client/Compass/reports/PurchaseOrderRejectionEmail.php |
|
|
13
|
-
| [Compass MITS Sales-Order
|
|
13
|
+
| [Compass MITS Sales-Order Transmit Alerting (US Issue/Event vs Canada in-cron email)](features/mits-sales-order-transmission-alerting.md) | 2.0 | How a failure in the `1_transmit_..._sales_orders_to_mits.php` cron reaches a human — for **both** Compass tenants, because **they no longer work the same way** | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker/crons/toga2/compasscanada/workflow/1_transmit_compassCanada_sales_orders_to_mits.php, worker2/Worker/Infrastructure/Errors.php, tools/mvc/errors/post.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, dbchanges2/Logs/2026-08-04a - MITS rejection business recipients.sql |
|
|
14
14
|
| [Compass MR/MA Order Auto-Approval & Status Gate](features/mr-ma-order-approval-and-status.md) | 2.0 | Compass **MR** and **MA** sales orders are system-generated from the MITS / Office Depot EDI pipeline (they do not originate as user-entered SA orders) and must | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
|
|
15
15
|
| [Compass ODP EDI 850 Line-Item Resolution (VA part number to IN SKU fallback)](features/odp-edi-850-item-resolution.md) | 1.0 | How each **PO1 line** on an inbound Office Depot (ODP) **EDI 850** is resolved to a real `Client_Compass` catalog item before cron `3a_import_office_depot_purch | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, library/app/Edi.php |
|
|
16
16
|
| [Compass Office Depot EDI 855 Acknowledgement + Over-Quantity PO Guard](features/odp-edi-855-acknowledgement-and-overquantity-guard.md) | 1.0 | How Compass acknowledges Office Depot (ODP) inbound **EDI 850** purchase orders with an **X12 855**, and the **over-quantity guard** that rejects a duplicate PO | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/4_transmit_office_depot_po_acknowledgements.php, library/app/edi.php |
|
|
@@ -1,59 +1,99 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Compass MITS Sales-Order
|
|
2
|
+
title: Compass MITS Sales-Order Transmit Alerting (US Issue/Event vs Canada in-cron email)
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
project: _Underscore
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
9
|
-
owners: ["dfranks"]
|
|
8
|
+
updated: 2026-08-31
|
|
9
|
+
owners: ["dfranks", "bala"]
|
|
10
10
|
files:
|
|
11
11
|
- worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
|
|
12
|
+
- worker/crons/toga2/compasscanada/workflow/1_transmit_compassCanada_sales_orders_to_mits.php
|
|
12
13
|
- worker2/Worker/Infrastructure/Errors.php
|
|
14
|
+
- tools/mvc/errors/post.php
|
|
13
15
|
- _underscore/Model/Core/Logs/Issue.php
|
|
14
16
|
- _underscore/Model/Core/Logs/IssueEmailAddress.php
|
|
15
17
|
- dbchanges2/Logs/2026-08-04a - MITS rejection business recipients.sql
|
|
16
18
|
related:
|
|
17
19
|
- mits-po-transmission-to-vendors.md
|
|
20
|
+
- ../../compass-canada/profile.md
|
|
18
21
|
- ../../../2.0/apps/worker2/features/error-escalation-cron.md
|
|
19
22
|
- ../../../2.0/apps/_underscore/features/error-reporting-issue-event.md
|
|
20
23
|
- ../../../1.0/apps/library/features/error-capture-1-0.md
|
|
24
|
+
- ../../../1.0/apps/tools/features/errors-curation-console.md
|
|
21
25
|
---
|
|
22
26
|
|
|
23
27
|
## Summary
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
How a failure in the `1_transmit_..._sales_orders_to_mits.php` cron reaches a human — for
|
|
30
|
+
**both** Compass tenants, because **they no longer work the same way**.
|
|
31
|
+
|
|
32
|
+
When MITS **rejects** a Compass **USA** sales order, the alert is not an email built inside the
|
|
27
33
|
cron. It is a **business exception** raised into the shared Logs Issue/Event pipeline, and its
|
|
28
|
-
recipients are **rows in `Logs.IssueEmailAddress`**.
|
|
34
|
+
recipients are **rows in `Logs.IssueEmailAddress`**. The Compass **Canada** cron still sends a
|
|
35
|
+
direct `App_Email_Agilant` for the same rejection.
|
|
36
|
+
|
|
37
|
+
**Consequence for anyone handed a ticket like "add <person> to the MITS error emails": the
|
|
38
|
+
answer depends on the tenant and on which failure.** Check the table below before opening
|
|
39
|
+
either file — half of these alerts have no PHP to change, and the other half have no
|
|
40
|
+
configuration surface at all.
|
|
41
|
+
|
|
42
|
+
## Which alert goes where (check this first)
|
|
43
|
+
|
|
44
|
+
| Tenant / failure | Mechanism | To add a recipient |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| **USA** — MITS rejects the order (`responseCode != 200`) | `App_Exception_Business` → Logs Issue/Event | **Data/UI change.** Add a row to `Logs.IssueEmailAddress` against the issue — no code edit |
|
|
47
|
+
| **USA** — "MITS API Script Error: No Items Found" | in-cron `App_Email_Agilant` | edit `addTo()` in the cron |
|
|
48
|
+
| **Canada** — "No Items Found" | in-cron `App_Email_Agilant` | edit `addTo()` in the cron |
|
|
49
|
+
| **Canada** — "Sales Order Items are more than 15" | in-cron `App_Email_Agilant` | edit `addTo()` in the cron |
|
|
50
|
+
| **Canada** — "Invalid response from MITS API" (the rejection path) | in-cron `App_Email_Agilant` | edit `addTo()` in the cron |
|
|
29
51
|
|
|
30
|
-
|
|
31
|
-
|
|
52
|
+
So the USA cron is a **hybrid**: its rejection path is issue-routed, but it still holds one
|
|
53
|
+
hardcoded email for the no-items case. Only the rejection branch was migrated.
|
|
32
54
|
|
|
33
|
-
|
|
55
|
+
Both crons are live — `worker/schedules/cron.worker.sync.json` schedules
|
|
56
|
+
`toga2/compass/workflow/1_...` and `toga2/compasscanada/workflow/1_...`. (Per the
|
|
57
|
+
[ODP pipeline doc's](../workflows/odp-order-pipeline-to-netsuite.md) rule, a grep hit under
|
|
58
|
+
`crons/toga2/**` is not proof a script runs — these two were verified against the schedule.)
|
|
59
|
+
|
|
60
|
+
**Current recipients on every in-cron email above:** `devteam@togatech.com` +
|
|
61
|
+
`vburks@togatech.com` (business stakeholder, added 2026-08-31).
|
|
62
|
+
|
|
63
|
+
## How the USA rejection path works
|
|
34
64
|
|
|
35
65
|
- The cron raises
|
|
36
66
|
`throw new App_Exception_Business('COMPASS_SALES_ORDER_MITS_TRANSMIT_REJECTED', …, URGENCY_HIGH)`
|
|
37
67
|
on a MITS rejection. (Before 2026-08-03 this block was a hardcoded `new App_Email_Agilant()`
|
|
38
|
-
with `addTo('devteam@togatech.com')
|
|
39
|
-
- Capture creates/dedupes a `Logs.Issue` keyed on the `issueKey`,
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
with `addTo('devteam@togatech.com')` — i.e. exactly what Canada still does.)
|
|
69
|
+
- Capture creates/dedupes a `Logs.Issue` keyed on the `issueKey`, so repeated rejections are
|
|
70
|
+
**one issue with an occurrence count**, not one email per failure. It is visible to internal
|
|
71
|
+
staff in the Tools `/errors` console.
|
|
72
|
+
- The worker2 `Infrastructure/Errors/Escalate` cron does all outbound notification — business
|
|
73
|
+
email to the configured recipients **and**, independently, the dev-team ClickUp task. See
|
|
42
74
|
[error-escalation-cron](../../../2.0/apps/worker2/features/error-escalation-cron.md).
|
|
43
75
|
- Recipients live in `Logs.IssueEmailAddress` (FK `issueId` → `Logs.Issue`), scoped by
|
|
44
76
|
`clientId` (`0` = all clients).
|
|
45
77
|
|
|
46
|
-
### Adding a recipient
|
|
78
|
+
### Adding a recipient — two routes
|
|
79
|
+
|
|
80
|
+
**Route 1 (preferred): the Tools `/errors` console.** `tools/mvc/errors/post.php` exposes
|
|
81
|
+
`add-recipient` / `delete-recipient`, which write `IssueEmailAddress`
|
|
82
|
+
(`uuid, issueId, clientId, emailAddress, toCcBcc`) and set `Issue.isManaged = 1`. `clientId = 0`
|
|
83
|
+
requires an explicit second confirmation checkbox, because one mistyped `0` silently sends one
|
|
84
|
+
client's business users every other client's error content. See
|
|
85
|
+
[errors-curation-console](../../../1.0/apps/tools/features/errors-curation-console.md).
|
|
47
86
|
|
|
48
|
-
|
|
87
|
+
**Route 2: a `dbchanges2/Logs/YYYY-MM-DD<letter>` migration** — use this when the change must
|
|
88
|
+
ride a deploy across environments. It must:
|
|
49
89
|
|
|
50
|
-
1. **
|
|
90
|
+
1. **Look the issue up by `issueKey`**, never by its `id`. The id is assigned on the issue's
|
|
51
91
|
**first occurrence** and is therefore not stable across environments.
|
|
52
|
-
2.
|
|
92
|
+
2. Use **`clientId = 0`**. This `issueKey` is raised from a Compass-only code path, and an
|
|
53
93
|
occurrence recorded without client attribution resolves against scope `0` only — a row scoped
|
|
54
94
|
to the Compass `clientId` can deliver nothing. (Never use `0` for an `issueKey` raised from
|
|
55
95
|
shared code — that is a cross-tenant leak.)
|
|
56
|
-
3.
|
|
96
|
+
3. Be **idempotent** via `INSERT … WHERE NOT EXISTS`, mirroring the unique key
|
|
57
97
|
`uq_issue_client_address_type`.
|
|
58
98
|
|
|
59
99
|
Configuration is **never retroactive**: `shouldNotify()` fires on the first occurrence, on
|
|
@@ -62,28 +102,49 @@ zero events. The first email lands on the **next live rejection**, not on existi
|
|
|
62
102
|
|
|
63
103
|
## Gotchas / known issues
|
|
64
104
|
|
|
65
|
-
- **
|
|
105
|
+
- **Canada is the pre-2026-08-03 design, and it still carries the two problems that migration
|
|
106
|
+
fixed.** Its "Invalid response from MITS API" email `print_r`s the **whole transmission
|
|
107
|
+
payload** into the body — which includes ship-to addressee, attention line, street address and
|
|
108
|
+
phone number — and there is no issue dedupe, so a bad run emails once per order. Treat
|
|
109
|
+
bringing Canada onto `App_Exception_Business` as known outstanding work, not a new idea; the
|
|
110
|
+
USA block is the reference implementation.
|
|
111
|
+
- **Canada's alerts do not send where outbound email is disabled.** The USA no-items email sets
|
|
112
|
+
`sendEmailEvenIfOutboundEmailIsDisabled = true`; none of Canada's three do. On any environment
|
|
113
|
+
with outbound email off, Canada fails **silently**.
|
|
114
|
+
- **Do not "modernize" the USA rejection alert back into the cron.** Migrating it to
|
|
66
115
|
`_Model_Client_EmailTemplate::send` (or any in-cron email) reverts the move to the Issue/Event
|
|
67
116
|
pipeline and removes the recipient configuration surface entirely.
|
|
68
|
-
- **Never re-introduce the payload dump into the alert.** The
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- **The MITS cron holds its API credentials inline in the file.** Treat that file as
|
|
117
|
+
- **Never re-introduce the payload dump into the USA alert.** The Issue message is stored in a
|
|
118
|
+
table **shared across every client**, displayed to internal staff in the Tools `/errors`
|
|
119
|
+
console, pushed to ClickUp, and emailed — so the payload was deliberately removed. Alerts here
|
|
120
|
+
name the order, not the person. (This is precisely what Canada still does wrong.)
|
|
121
|
+
- **The MITS crons hold their API credentials inline in the file.** Treat both files as
|
|
74
122
|
secret-bearing when copying snippets anywhere (tickets, PR bodies, knowledge docs).
|
|
75
123
|
- **Adding a recipient does not turn ClickUp ticketing off.** Routing is both-and, not
|
|
76
|
-
either-or
|
|
124
|
+
either-or: `_Worker_Infrastructure_Errors` runs `if ($recipients !== null)` and
|
|
125
|
+
`if ((bool) $issue->createsClickupTask)` as **two independent** branches.
|
|
126
|
+
**⚠ The comment above the insert in `tools/mvc/errors/post.php` still claims the opposite**
|
|
127
|
+
("it stops creating ClickUp tasks for the dev team and starts emailing these people
|
|
128
|
+
instead") — that describes the old `elseif` form and is **stale**. Trust `Errors.php`.
|
|
77
129
|
- As of 2026-08-04 prod `Logs.IssueEmailAddress` is empty, so this is the **first business-routed
|
|
78
130
|
issue on the platform**: `processBusinessIssue()` will execute in production for the first time
|
|
79
131
|
when this issue next fires. Watch that run rather than assuming it works.
|
|
80
132
|
|
|
81
133
|
## Change history
|
|
134
|
+
- 2026-08-31 — Added `vburks@togatech.com` alongside `devteam@togatech.com` on **every** in-cron
|
|
135
|
+
`App_Email_Agilant` alert in both MITS transmit crons (USA has one, Canada has three), so the
|
|
136
|
+
business stakeholder is alerted on transmission failures. Broadened this doc to cover **both**
|
|
137
|
+
tenants after finding they had diverged: the USA rejection path is Issue/Event-routed
|
|
138
|
+
(recipients configured against the issue, no `addTo()` to edit) while **Canada still uses the
|
|
139
|
+
old direct email** for the same branch — including the PII-bearing payload dump and with no
|
|
140
|
+
dedupe. Also recorded that the USA cron is a hybrid (its no-items alert is still an in-cron
|
|
141
|
+
email), that recipients can be managed from the Tools `/errors` console as well as a
|
|
142
|
+
`dbchanges2/Logs` migration, that Canada's alerts are suppressed wherever outbound email is
|
|
143
|
+
disabled, and that the stale `tools/mvc/errors/post.php` comment misstates ClickUp-vs-email
|
|
144
|
+
routing. (bala)
|
|
82
145
|
- 2026-08-04 — Documented that MITS rejection alerting moved (worker `5521854b`, 2026-08-03) from a
|
|
83
146
|
hardcoded in-cron `App_Email_Agilant` to `App_Exception_Business` +
|
|
84
147
|
`Logs.IssueEmailAddress` recipients, so adding an alert recipient is a data change, not a code
|
|
85
148
|
change. Recorded the deliberate removal of the PII-bearing payload dump from the alert, the
|
|
86
149
|
look-up-by-`issueKey` / `clientId = 0` / `NOT EXISTS` migration pattern, and that recipient
|
|
87
150
|
configuration only takes effect on the next live occurrence. (dfranks)
|
|
88
|
-
</content>
|
|
89
|
-
</invoke>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| [Prudential: Dell LCH IOP transmissions (LCHRequestV2)](features/dell-lch-iop-transmissions.md) | 1.0 | Outbound order transmissions from the 1.0 worker tier to Dell's Lifecycle Hub (LCH) ITSM integration. | library/app/api/delllch.php, library/app/apitransaction.php, worker/crons/toga2/prudential/transmissions_to_dell_usa.php, worker/crons/toga2/prudential/transmissions_to_dell_ireland.php, worker/crons/toga2/prudential/transmissions_to_dell_india.php, worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php |
|
|
7
7
|
| [Prudential: Device information import + unit→contact linking (import_device_information.php)](features/device-information-import-and-contact-linking.md) | 1.0 | Prudential's **device-sync** cron (`worker/crons/toga2/prudential/import_device_information.php`) pulls device/asset records (from ServiceNow / the Dell CMDB fe | worker/crons/toga2/prudential/import_device_information.php, worker/crons/toga2/prudential/backfill_unit_contacts.php, dbchanges2/Client_Prudential/2026-07-07 - Contact Dedup Merge.sql |
|
|
8
8
|
| [Prudential: OneUptime order-pipeline backlog monitors (Monitor/Prudential/*)](features/oneuptime-order-pipeline-monitors.md) | 2.0 | Three worker2 OneUptime push monitors (TRUE-80587) that watch the Prudential/Dell order pipeline for **stalls**, which were previously invisible — a stuck pipel | worker2/Worker/Monitor/Prudential.php, worker2/Config/production.ini, dbchanges2/Core/2026-08-07a - Prudential AIG ServiceNow OneUptime monitor crons.sql, dbchanges2/Client_Prudential/2026-04-08 - Insert Customers.sql |
|
|
9
|
-
| [Prudential: order email PDF attachments (which guide goes on which email)](features/order-email-pdf-attachments.md) | 1.0 | Every customer-facing Prudential order email carries S3-hosted PDF guides, and **which** guide is attached is decided by the service request type. | worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/toga2/prudential/transmissions_to_netsuite.php, worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php, worker/crons/toga2/prudential/transmit_closecomplete_updates_prudential.php
|
|
9
|
+
| [Prudential: order email PDF attachments (which guide goes on which email)](features/order-email-pdf-attachments.md) | 1.0 | Every customer-facing Prudential order email carries S3-hosted PDF guides, and **which** guide is attached is decided by the service request type. | worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/toga2/prudential/transmissions_to_netsuite.php, worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php, worker/crons/toga2/prudential/transmit_closecomplete_updates_prudential.php |
|
|
10
10
|
| [Prudential: Service Request Regional Address Validation](features/service-request-address-validation.md) | 2.0 | The `prePost` interceptor on `_Model_Prudential_ServiceRequest` validates `deliverToAddress` fields differently depending on which Prudential regional customer | _underscore/Model/Prudential/ServiceRequest.php, _underscore/Test/Prudential/ServiceRequestTest.php |
|
|
11
11
|
| [Prudential: Service Request rejection alert email](features/service-request-rejection-alert-email.md) | 2.0 | When a Prudential ServiceNow→TOGa service-request submission (`POST /v2/service-requests`) is **rejected by validation**, TOGa now sends a real-time internal al | worker2/Worker/Client/Prudential/reports/ReqRejectionEmail.php, _underscore/Model/Prudential/ServiceRequest.php |
|
|
12
12
|
| [Prudential: Sales Orders CSV export (TOGa 2.5 Supply)](features/supply-orders-csv-export.md) | 2.0 | The **Export** button on the TOGa 2.5 Supply *Sales Orders* page is **Prudential-only**: it downloads a 22-column CSV (`prudential-orders-<YYYY-MM-DD>.csv`) cov | toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json, toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts, toga25-supply/src/pages/SalesOrders/hooks/usePrudentialOrdersExport.ts, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx |
|
|
@@ -6,17 +6,17 @@ project: Worker
|
|
|
6
6
|
client: prudential
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-31
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php
|
|
13
13
|
- worker/crons/toga2/prudential/transmissions_to_netsuite.php
|
|
14
14
|
- worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php
|
|
15
15
|
- worker/crons/toga2/prudential/transmit_closecomplete_updates_prudential.php
|
|
16
|
-
- worker/crons/toga2/prudential/test_new_hire_email.php
|
|
17
16
|
related:
|
|
18
17
|
- transmit-ordershipped-email.md
|
|
19
18
|
- ../profile.md
|
|
19
|
+
- ../../../1.0/apps/library/features/email-test-mode-and-write-free-sends.md
|
|
20
20
|
---
|
|
21
21
|
|
|
22
22
|
## Summary
|
|
@@ -83,6 +83,64 @@ all three returned HTTP 200 (verified 2026-08-27). Two of them are **renamed on
|
|
|
83
83
|
The Mac guide dominates: a new-hire email is roughly **11 MB** once base64-encoded. Grepping for
|
|
84
84
|
the attached-as filename will not find the S3 object — search the S3 basename.
|
|
85
85
|
|
|
86
|
+
## The verbiage has to match what actually attached
|
|
87
|
+
|
|
88
|
+
Every attach is guarded by `if ($pdfGuideX !== false)`, but until 2026-08-31 the `$attachmentText`
|
|
89
|
+
paragraph that *describes* the attachments was unconditional. A failed S3 fetch therefore produced
|
|
90
|
+
an email whose copy pointed the recipient at guides that were not on the message — and it always
|
|
91
|
+
said "guides" plural even when a single guide attached.
|
|
92
|
+
|
|
93
|
+
The rule now, in every branch that was touched: **derive the sentence from the guards, not from
|
|
94
|
+
the intent.**
|
|
95
|
+
|
|
96
|
+
- Count (or list) the guides that actually attached.
|
|
97
|
+
- Nothing attached → `$attachmentText = ''` and the paragraph disappears entirely.
|
|
98
|
+
- One guide → "guide" / "is provided"; two → "guides" / "are provided"
|
|
99
|
+
(`$guideNoun`, `$guideVerb`).
|
|
100
|
+
- Name only the devices whose guide attached, built from `$pdfGuide11` / `$pdfGuideMac` and joined
|
|
101
|
+
with `implode(' and ', $attachedGuideDevices)`.
|
|
102
|
+
|
|
103
|
+
One subtlety in the Breakfix/Swap/Label order-created branch: that branch attaches the Windows 11
|
|
104
|
+
guide **and** the return-instructions doc, but the sentence is a *getting started* sentence. It is
|
|
105
|
+
therefore tied to `$pdfGuide11` alone — the return doc is not a getting started guide and must not
|
|
106
|
+
make that sentence appear:
|
|
107
|
+
|
|
108
|
+
```php
|
|
109
|
+
$attachmentText = ($pdfGuide11 !== false ? 'When you do end up receiving equipment, you can refer to the attached guide for step-by-step instructions on getting started.' : '');
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Fixed in four places (TRUE-81393, PR #1707, merged to `_production` 2026-08-31): the new-hire
|
|
113
|
+
order-created branch, the Breakfix/Swap/Label order-created catch-all, and the new-hire **and**
|
|
114
|
+
non-new-hire blocks in **both** shipped crons.
|
|
115
|
+
|
|
116
|
+
## Email template anatomy (and the dead per-item rows)
|
|
117
|
+
|
|
118
|
+
**The ordered items are never listed in these emails.** In each of the four crons, the
|
|
119
|
+
`$data = $data . "<tr>…"` append that sits *inside* the `foreach (... as $item)` loop is wrapped in
|
|
120
|
+
a `/* … */` comment. The loop still runs, and it still calls the Etilize/spexlive catalog API
|
|
121
|
+
(`ws-na1.spexlive.net/service/rest/catalog`) to resolve a product thumbnail into `$url` — but
|
|
122
|
+
nothing it produces reaches the message. Each body is only a header chunk plus a footer chunk. If
|
|
123
|
+
you are asked "why doesn't the item show up in the email", this is why; if you are asked to add
|
|
124
|
+
items back, the markup is already there, commented, and the thumbnail lookup already works.
|
|
125
|
+
|
|
126
|
+
There are **8 distinct templates**, each roughly 150–300 lines of inline HTML:
|
|
127
|
+
|
|
128
|
+
| Stage | Templates | Where |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| Order created | 4 — new hire, reclaim, refresh, Breakfix/Swap/Label catch-all | `generate_sales_and_purchase_orders_from_service_requests.php` |
|
|
131
|
+
| Shipped | 2 — new hire, non-new-hire | duplicated **identically** in `transmissions_to_netsuite.php` and `transmit_ordershipped_updates_prudential.php` |
|
|
132
|
+
| Delivered | 2 — new hire, non-new-hire | `transmit_closecomplete_updates_prudential.php` |
|
|
133
|
+
|
|
134
|
+
Only the new-hire and Breakfix/Swap/Label order-created templates carry the (commented) item loop;
|
|
135
|
+
the reclaim and refresh order-created branches have no item loop at all.
|
|
136
|
+
|
|
137
|
+
Variables the templates interpolate — useful if you are reproducing one outside its cron:
|
|
138
|
+
|
|
139
|
+
- **Order created** — fields off `$rowServiceRequest` (incl. `reqNumber`), plus `$state`,
|
|
140
|
+
`$salesOrderNumber` and `$attachmentText`.
|
|
141
|
+
- **Shipped / delivered** — flat locals: `$name`, `$line1`, `$line2`, `$city`, `$state`, `$zip`,
|
|
142
|
+
`$orderNumber`, `$reqNumber`, `$trackingNumber` / `$trackingNumbers[$index]`, `$attachmentText`.
|
|
143
|
+
|
|
86
144
|
## How to change an attachment safely
|
|
87
145
|
|
|
88
146
|
1. Every fetch goes through the file-scope `safeFetchPdf()` helper, never bare
|
|
@@ -100,24 +158,59 @@ the attached-as filename will not find the S3 object — search the S3 basename.
|
|
|
100
158
|
precisely the type that most needs the return instructions. That guard is how Reclaim ended up
|
|
101
159
|
receiving zero attachments.
|
|
102
160
|
|
|
103
|
-
## Testing
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
161
|
+
## Testing these emails without writing anything
|
|
162
|
+
|
|
163
|
+
**There is no committed test harness — do not go looking for one.** Three throwaway scripts
|
|
164
|
+
(`test_new_hire_email.php`, `test_prudential_order_emails.php`,
|
|
165
|
+
`test_prudential_email_templates.php`) were written under `crons/toga2/prudential/` on 2026-08-31
|
|
166
|
+
to mail every stage/type combination with the real templates and the real S3 PDFs. They were
|
|
167
|
+
**never committed and are no longer in the working tree**. What is durable is the technique.
|
|
168
|
+
|
|
169
|
+
A harness that mails a real, byte-identical copy of one of these emails while writing **nothing**
|
|
170
|
+
to the database needs all of this:
|
|
171
|
+
|
|
172
|
+
- `setQueueForSending(false)`, `sendEmailEvenIfOutboundEmailIsDisabled = true`, and
|
|
173
|
+
`setIsLogEnabled(false)` on the `App_Email` object. All three — the first two keep it out of
|
|
174
|
+
`Common.EmailsQueued`, the third keeps it out of `Logs.Email`. Full detail in
|
|
175
|
+
[App_Email Side Effects & Test Mode](../../../1.0/apps/library/features/email-test-mode-and-write-free-sends.md).
|
|
176
|
+
- `App_Framework_Worker::initialize()` **only**. Do **not** call
|
|
177
|
+
`App_Framework::cronInitialization()` — it INSERTs a `CronJobExecutions` row. Set
|
|
178
|
+
`set_time_limit()` and `date_default_timezone_set()` yourself instead.
|
|
179
|
+
- One read-only SELECT for the sample data (below), rather than the cron's api2 fetches.
|
|
180
|
+
- Print each PDF's resolved/missing state and byte size **before** sending; PHPMailer 5 will send
|
|
181
|
+
the message anyway if an attachment is unreadable.
|
|
182
|
+
|
|
183
|
+
**Run it on the worker server if anyone other than you needs to receive it.** Local and beta
|
|
184
|
+
configs have `internal.test_mode = 1`, which discards the recipient list and sends only to
|
|
185
|
+
`email.dev_email_to`. `config.worker.ini` on the worker server has `test_mode = 0`.
|
|
186
|
+
|
|
187
|
+
### Read-only query for a real sample request
|
|
188
|
+
|
|
189
|
+
One row per request type is enough to exercise every template. Join `ServiceRequests` to
|
|
190
|
+
`Addresses` on `deliverToAddressId`, `States` on `Addresses.stateId`, `Contacts` on
|
|
191
|
+
`requestedForContactId`, and **LEFT JOIN** `SalesOrders` on `SalesOrders.serviceRequestId`
|
|
192
|
+
(left, because an unprocessed request has no SO yet). Select
|
|
193
|
+
`ServiceRequests.c_reqNumber AS reqNumber`, `Contacts.firstName`, the address parts,
|
|
194
|
+
`States.code`, and `SalesOrders.number` — that is the exact set the shipped/delivered templates
|
|
195
|
+
interpolate.
|
|
196
|
+
|
|
197
|
+
All six request types have live rows in prod `Client_Prudential` (verified 2026-08-31); **Swap has
|
|
198
|
+
only 2 rows**, the rest have thousands, so filter narrowly for Swap or you will miss it.
|
|
118
199
|
|
|
119
200
|
## Gotchas / known gaps
|
|
120
201
|
|
|
202
|
+
- **`transmit_closecomplete_updates_prudential.php` still over-promises (~line 447).** Its
|
|
203
|
+
delivered email hardcodes "Guides for Windows 11 and MacBook devices are provided" with no
|
|
204
|
+
reference to whether either PDF actually attached — the exact bug fixed everywhere else on
|
|
205
|
+
2026-08-31. Left alone deliberately to keep PR #1707's diff to files already being changed.
|
|
206
|
+
**Worth a follow-up ticket.**
|
|
207
|
+
- **`transmissions_to_netsuite.php` also sends *delivered* emails — with no attachments at all.**
|
|
208
|
+
It carries its own "New hire equipment has been delivered" / "Your equipment has been delivered"
|
|
209
|
+
blocks (~L1765 and ~L2149) that make **zero** `addAttachment()` calls, while
|
|
210
|
+
`transmit_closecomplete_updates_prudential.php` sends the **same two subjects** with the guides
|
|
211
|
+
attached. **Whether both fire for the same order was not established** — if they do, the
|
|
212
|
+
recipient gets a duplicate delivered email, one of them attachment-less. Check this before
|
|
213
|
+
changing either.
|
|
121
214
|
- **`prudential_beta/` does not match prod.**
|
|
122
215
|
`crons/toga2/prudential_beta/generate_sales_and_purchase_orders_from_service_requests.php` is
|
|
123
216
|
`active: 1` on the same schedule but points at the beta DB and still references the retired
|
|
@@ -129,6 +222,16 @@ php -d include_path=".;<library>;<_underscore>" toga2/prudential/test_new_hire_e
|
|
|
129
222
|
and was deliberately **not** touched.
|
|
130
223
|
|
|
131
224
|
## Change history
|
|
225
|
+
- 2026-08-31 — CodeRabbit review follow-up (TRUE-81393, PR #1707, merged to `_production`): the
|
|
226
|
+
`$attachmentText` paragraph was unconditional while every attach was guarded, so a failed S3
|
|
227
|
+
fetch mailed copy referencing guides that were not attached, always pluralised. Now derived from
|
|
228
|
+
the guards in four places (new-hire and Breakfix/Swap/Label order-created branches, plus the
|
|
229
|
+
new-hire and non-new-hire blocks in both shipped crons), with the getting-started sentence tied
|
|
230
|
+
to the Windows 11 guide only. Also recorded: the per-item rows are commented out in all four
|
|
231
|
+
crons (items are never listed in any of these emails), the 8-template inventory, the
|
|
232
|
+
write-nothing test technique (the three throwaway harness scripts were never committed), and two
|
|
233
|
+
open items — the same over-promise still live in the delivered cron ~L447, and a second,
|
|
234
|
+
attachment-less delivered sender in `transmissions_to_netsuite.php` (bala)
|
|
132
235
|
- 2026-08-27 — Client-reported: the new-hire order-created email attached the return-instructions
|
|
133
236
|
PDF and carried no Mac guide. Swapped it to Win11 + Mac
|
|
134
237
|
(`2026_Aug_PrudentialMacSetupGuide_ZCC.pdf` attached as `PrudentialMacSetupGuide.pdf`) and
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: prudential
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-31
|
|
10
10
|
owners: ["rgirish", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php
|
|
@@ -93,6 +93,14 @@ same subjects, same branch shape — and also stamps `c_dtTransmittedOrderShippe
|
|
|
93
93
|
It is easy to miss: it is not named like the other `transmit_*` crons. Any change to the shipped
|
|
94
94
|
email's content or attachments must be applied to **both** files or the two senders drift.
|
|
95
95
|
|
|
96
|
+
**And it does not stop at the shipped email — it also sends the two *delivered* emails.**
|
|
97
|
+
`transmissions_to_netsuite.php` carries "Prudential: New hire equipment has been delivered"
|
|
98
|
+
(~L1765) and "Prudential: Your equipment has been delivered" (~L2149), the **same two subjects**
|
|
99
|
+
that `transmit_closecomplete_updates_prudential.php` sends — except the `transmissions_to_netsuite`
|
|
100
|
+
versions make **no `addAttachment()` calls at all**. Whether both crons fire for the same order was
|
|
101
|
+
not established (2026-08-31); if they do, the customer receives a duplicate delivered email, one
|
|
102
|
+
copy of which is missing the guides. Establish this before touching either delivered path.
|
|
103
|
+
|
|
96
104
|
It was also the last Prudential cron still calling `file_get_contents()` directly for its PDFs and
|
|
97
105
|
calling `addAttachment()` unconditionally — so a failed S3 pull threw and killed the batch, and a
|
|
98
106
|
false return would have mailed a 0-byte file under a real guide's name. Converted to
|
|
@@ -193,6 +201,16 @@ stamp (crash hits the next iteration). With multiple New-type POs, a crash on PO
|
|
|
193
201
|
all subsequent POs in the same run.
|
|
194
202
|
|
|
195
203
|
## Change history
|
|
204
|
+
- 2026-08-31 — TRUE-81393 / PR #1707 (merged to `_production`): the shipped email's
|
|
205
|
+
`$attachmentText` paragraph was unconditional while each attach was guarded, so a failed S3 pull
|
|
206
|
+
produced copy promising guides that were not attached — and it said "guides" even for a single
|
|
207
|
+
guide. Both blocks in this cron (new hire and non-new-hire) and both in
|
|
208
|
+
`transmissions_to_netsuite.php` now build the sentence from `$pdfGuide11`/`$pdfGuideMac`, naming
|
|
209
|
+
only what attached and dropping the paragraph when nothing did. Also discovered: the per-item
|
|
210
|
+
`<tr>` append inside the `foreach ($purchaseOrderItems as $item)` loop is **commented out** here
|
|
211
|
+
and in the sibling crons, so ordered items never appear in these emails (the loop still calls the
|
|
212
|
+
Etilize thumbnail API for nothing); and `transmissions_to_netsuite.php` additionally sends the
|
|
213
|
+
two **delivered** emails with no attachments. See order-email-pdf-attachments.md (bala)
|
|
196
214
|
- 2026-08-27 — Extended the `safeFetchPdf()` + per-PDF-guard pattern to
|
|
197
215
|
`transmissions_to_netsuite.php`, the last cron still fetching PDFs with bare
|
|
198
216
|
`file_get_contents()` and attaching unconditionally. Also recorded that this cron is a **second
|
package/package.json
CHANGED