toga-ai 1.0.309 → 1.0.311

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.
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-07-09
10
- owners: ["jcardinal", "dfranks"]
10
+ owners: ["jcardinal", "dfranks", "mhammontree"]
11
11
  files:
12
12
  - crons/tickets.php
13
13
  - crons/tickets_prod.php
@@ -40,6 +40,19 @@ TOGa Desk ingests support email into tickets through a cron-driven IMAP poller
40
40
  3. **Parse & route (`emailToTicket()`):** extract from/to/cc/subject/body (with encoding fixes);
41
41
  match an existing ticket via `In-Reply-To` + `Message-ID` headers and **plus-addressing**
42
42
  (`support+<ticketnum>@…`). Matched ⇒ `addReply()`; otherwise ⇒ `add()`.
43
+ - **Noise suppression (before any ticket/attachment is created):** both feeders drop known
44
+ automated no-noise mail via the shared helper
45
+ `Ticket::isSuppressedNoiseEmail(?string $from, ?string $subject): bool` (static, in
46
+ `class.ticket.php`). It returns true only when the sender matches `/^no-?reply@/` **AND**
47
+ the subject starts with `staples po ` — both required so a real person is never blocked, and
48
+ the sender check is **domain-agnostic** (matches `noreply@togatech.com` /
49
+ `noreply@goagilant.com` and survives future sender-domain flips; the Staples PO alert sender
50
+ flipped `goagilant.com → togatech.com` on 2026-06-02). Wired into BOTH intake feeders:
51
+ `crons/tickets.php` adds it as one more `&&`-clause in the existing IMAP intake guard
52
+ (alongside the delivery-status / `MAILER-DAEMON` / Compass auto-reply filters);
53
+ `crons/pipe.php` uses it as an early guard-and-`exit(0)` right after the `$from`/`$subject`
54
+ parse. Each feeder logs a "suppressed" line. (TRUE-79975 — ~815 such alerts since Jun 2024,
55
+ ~704 of them landing in the dept-1 "Support Desk" catch-all and being closed by hand.)
43
56
  4. **Side effects:** auto-create a user when the sender domain maps to an existing client; assign
44
57
  to a department via the department email lookup; save attachments locally then push to **S3**
45
58
  (`App_Cloud::copyFileToS3()`, bucket `asifiles`); optionally generate Talos/TogaIQ summaries.
@@ -49,7 +62,12 @@ TOGa Desk ingests support email into tickets through a cron-driven IMAP poller
49
62
  ## Data model
50
63
  Writes `tickets`, `tickets_replies`, attachment `files`; reads config from the `config` table
51
64
  (`tickets_defaultdepartment`, `tickets_defaultclient`, `auto_close_tickets`,
52
- `email_from_address`, `timezone`). Department routing keys on `tickets_departments.email`.
65
+ `email_from_address`, `timezone`). Department routing keys on a `tickets_departments.email`
66
+ `LIKE` match against the To/CC addresses; **anything unmatched falls through to config
67
+ `tickets_defaultdepartment` (= dept 1 "Support Desk" catch-all) and `tickets_defaultclient`** —
68
+ which is why unrouted/automated mail piles into "Support Desk". There is **no allowlist/blocklist
69
+ config framework**: noise filtering is ad-hoc inline guards in the crons (the hardcoded Compass
70
+ "Automatic reply:" filter and now `isSuppressedNoiseEmail`).
53
71
 
54
72
  ## Gotchas / known issues
55
73
  - **`$processName` guard:** `tickets.php` throws if executed directly instead of via an env
@@ -59,7 +77,13 @@ Writes `tickets`, `tickets_replies`, attachment `files`; reads config from the `
59
77
  - **Loop prevention** only excludes FROM==department-TO addresses; CC/reply-all across multiple
60
78
  department addresses can still loop.
61
79
  - **Auto-reply filters** are partly hardcoded (e.g. ignore `TechHub@compass-usa.com`
62
- "Automatic reply:" messages).
80
+ "Automatic reply:" messages), and now the `isSuppressedNoiseEmail` Staples-PO filter — there
81
+ is no config-driven allow/block list, so each new noise source is another inline guard.
82
+ - **`Ticket::extractEmail()` returned an undefined offset on a no-match** (bare `$matches[0]`).
83
+ With a sender-less/malformed email this codebase promotes the warning to a thrown error
84
+ (`E_ALL` + `App_Error::handleError`), so an intake cron could **halt**. Hardened to
85
+ `return $matches[0] ?? '';` (TRUE-79975) — important because `isSuppressedNoiseEmail` routes
86
+ nullable/malformed sender input through `extractEmail`, putting this latent bug on the hot path.
63
87
  - Attachments are deleted locally immediately after the S3 upload with **no retry** if S3 fails.
64
88
  - **`adminid` clobber on reply routing:** after `emailToTicket()` sets `$data['adminid']` from
65
89
  the FROM address (~line 1407), an asset lookup (~line 1437-1442) overwrites it with the
@@ -69,6 +93,10 @@ Writes `tickets`, `tickets_replies`, attachment `files`; reads config from the `
69
93
  classification. See the ticket-lifecycle doc.
70
94
 
71
95
  ## Change history
96
+ - 2026-07-09 — added `Ticket::isSuppressedNoiseEmail()` noise suppression (no-reply sender + "Staples PO"
97
+ subject) wired into both feeders before ticket creation; hardened `extractEmail()` no-match to
98
+ `''`; documented the `tickets_defaultdepartment` → "Support Desk" catch-all fall-through
99
+ (TRUE-79975) (mhammontree)
72
100
  - 2026-07-09 — documented the `emailToTicket()` adminid-overwrite reply misclassification
73
101
  (TRUE-80114 planning investigation) (dfranks)
74
102
  - 2026-06-15 — documented from a source read of `crons/tickets*.php` and `emailToTicket()` (jcardinal)
@@ -6,6 +6,6 @@
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, worker/crons/toga2/prudential/transmissions_to_dell.php, worker/crons/toga2/prudential_beta/transmissions_to_dell_india.php, worker/crons/toga2/prudential_beta/transmissions_to_dell_ireland.php, worker/crons/toga2/prudential_beta/transmissions_to_dell_usa.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: 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 |
9
- | [Prudential Order Shipped Email — transmit_ordershipped_updates_prudential.php](features/transmit-ordershipped-email.md) | 1.0 | Cron script that transmits "Order Shipped" updates to ServiceNow (RITM) and sends a shipped notification email to the end user. | worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php, worker/crons/toga2/prudential/transmit_closecomplete_updates_prudential.php, worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/toga2/prudential_beta/generate_sales_and_purchase_orders_from_service_requests.php |
9
+ | [Prudential Order Shipped Email — transmit_ordershipped_updates_prudential.php](features/transmit-ordershipped-email.md) | 1.0 | Cron script that transmits "Order Shipped" updates to ServiceNow (RITM) and sends a shipped notification email to the end user. | worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php, worker/crons/toga2/prudential/transmit_closecomplete_updates_prudential.php, worker/crons/toga2/prudential/transmit_rejected_cancelled_updates_prudential.php, worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/toga2/prudential_beta/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/notifications/reports/prudential_exception_report.php |
10
10
  | [Prudential Financial](profile.md) | 2.0 | Prudential is a TOGA client whose device-fulfillment flow is driven by **Dell** via the Dell API (`Client_Prudential.Apis.id = 2`). | |
11
11
  | [Prudential: Dell ASN failed POST backfill replay](workflows/dell-asn-backfill-replay.md) | 2.0 | When Dell ASN POSTs fail in bulk (e.g. | |
@@ -6,13 +6,15 @@ project: Worker
6
6
  client: prudential
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-07-06
9
+ updated: 2026-07-10
10
10
  owners: ["rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php
13
13
  - worker/crons/toga2/prudential/transmit_closecomplete_updates_prudential.php
14
+ - worker/crons/toga2/prudential/transmit_rejected_cancelled_updates_prudential.php
14
15
  - worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php
15
16
  - worker/crons/toga2/prudential_beta/generate_sales_and_purchase_orders_from_service_requests.php
17
+ - worker/crons/notifications/reports/prudential_exception_report.php
16
18
  related:
17
19
  - ../profile.md
18
20
  ---
@@ -47,11 +49,37 @@ Iterates the same POs collected in Phase 1. For each:
47
49
  - `Client_Prudential.PurchaseOrders.c_dtEmailSentOrderShipped` — stamped after email send
48
50
  - Both fields are checked by `prudential_exception_report.php` to detect discrepancies
49
51
 
52
+ **⚠ `c_dtTransmittedOrderClosedUpdateToPrudential` is an OVERLOADED column (discovered 2026-07-10).**
53
+ It is written by TWO different crons for THREE distinct terminal outcomes:
54
+ - **Delivered / Closed-Complete** — stamped by `transmit_closecomplete_updates_prudential.php`,
55
+ gated on an ASN with a `DELIVERED` tracking number, and **pairs with a customer email**
56
+ (`c_dtEmailSentOrderClosed`). *Only this path owes an email.*
57
+ - **Rejected / Cancelled** — stamped by `transmit_rejected_cancelled_updates_prudential.php`,
58
+ with **no ASN join**; it only pushes a ServiceNow "rejected"/"cancelled" RITM update and
59
+ **sends no customer email**.
60
+
61
+ So a no-ASN Rejected/Cancelled PO shows a "delivered" transmit stamp with `c_dtEmailSentOrderClosed`
62
+ NULL forever — that is correct, not a failure. Any query keyed on this column alone will
63
+ conflate the two. Example: REQ4448998 (PO 26544) was a Rejected order with no ASN/tracking and
64
+ was a false positive in the "Order Delivered Email Not Sent" report.
65
+
50
66
  ## Exception report link
51
67
  `worker/crons/notifications/reports/prudential_exception_report.php` flags any PO where
52
68
  `c_dtTransmittedOrderShippedUpdateToPrudential IS NOT NULL` AND `c_dtEmailSentOrderShipped IS NULL`
53
69
  as "Order Shipped Email Not Sent". This appears daily in the Prudential REQ Processing Report email.
54
70
 
71
+ **Order-Delivered discrepancy query — DELIVERED gate required (fixed 2026-07-10, ~line 463-474).**
72
+ The Order-Delivered discrepancy query originally filtered only on
73
+ `c_dtTransmittedOrderClosedUpdateToPrudential IS NOT NULL AND c_dtEmailSentOrderClosed IS NULL`
74
+ with no shipment/stage filter. Because that column is overloaded (see Data model), every
75
+ Rejected/Cancelled Dell PO was flagged as an "Order Delivered Email Not Sent" failure even though
76
+ it never owed a delivery email. Fix: add
77
+ `AND EXISTS (AdvanceShippingNotices → ASNItems → ASNItemUnits → ASNItemUnits_TrackingNumbers →
78
+ TrackingNumbers WHERE purchaseOrderId = PurchaseOrders.id AND TrackingNumbers.status = 'DELIVERED')`
79
+ — this mirrors the closecomplete cron's own Phase-1 email precondition, so the report only flags
80
+ orders that genuinely qualified for a delivery email. Over 2026-07-01..07-09 this dropped 6 false
81
+ positives (25 → 19 flagged) while retaining all 19 genuine discrepancies.
82
+
55
83
  ## Sibling scripts sharing this pattern
56
84
  The same two-phase (transmit-then-email) shape and the same PDF-fetch fragility exist in three
57
85
  sibling crons, all fixed the same way this session (2026-07-06):
@@ -65,6 +93,21 @@ sibling crons, all fixed the same way this session (2026-07-06):
65
93
 
66
94
  ## Gotchas / known issues
67
95
 
96
+ **ROOT CAUSE (third regression) — `App_Database::escapeString()` does not exist → fatal on the 404 auto-create path (fixed 2026-07-10):**
97
+ `createAssetAndRetryPut()` in **both** `transmit_ordershipped` and `transmit_closecomplete`
98
+ (~line 158) called `App_Database::escapeString($serialNumber)`. **That method does not exist** on
99
+ `App_Database` — only `sqlEscape`, `sqlProtect`, and `mysqlRealEscapeString` do. Under the 1.0
100
+ framework's `error_reporting(E_ALL)`, calling a non-existent static method is a **fatal `Error`
101
+ that kills the cron mid-run**. It fires whenever an asset PUT to ServiceNow returns 404
102
+ (`isAssetNotFoundError`) — routine for Breakfix/Reclaim/Refresh returned/replacement assets not yet
103
+ in the Prudential CMDB. The fatal occurs in Phase 1 **after** the transmit flag is stamped but
104
+ **before** the email phase, so the email is permanently stranded (email flag left NULL). This was
105
+ the specific unaddressed trigger of the month-long email-stranding — distinct from, but riding on
106
+ top of, the two-phase design flaw below. **Fix: `escapeString` → `sqlEscape` in both files**
107
+ (`sqlEscape` is already used correctly elsewhere in the same files, e.g. ordershipped line 1386).
108
+ This was flagged as a "latent error" in the 2026-07-06 pre-existing-issues note; it turned out to
109
+ be the live trigger.
110
+
68
111
  **ROOT CAUSE — `file_get_contents` in a 1.0 cron THROWS, it does not return false (discovered 2026-07-06):**
69
112
  The 1.0 framework bootstrap sets `error_reporting(E_ALL)` and `App_Error::handleError`
70
113
  (`library/app/error.php` ~line 348) converts **every** PHP warning into an `ErrorException`.
@@ -110,8 +153,8 @@ URL was still live in 3 of the sibling files and was corrected this session.
110
153
  **Pre-existing issues surfaced this session (NOT fixed — awareness only):**
111
154
  - SQL-injection risk: unescaped `$reqNumber` concatenated into a `LIKE '%...%'` query in the
112
155
  Phase-2 body of both `transmit_ordershipped` and `transmit_closecomplete`.
113
- - `transmit_closecomplete` calls `App_Database::escapeString()` (~line 158) which does not exist
114
- on `App_Database` (only `sqlEscape` does) — latent error in the `createAssetAndRetryPut` path.
156
+ - (RESOLVED 2026-07-10) `App_Database::escapeString()` at ~line 158 in **both** crons — see the
157
+ ROOT CAUSE gotcha above; this "latent" error was the actual fatal trigger, now fixed to `sqlEscape`.
115
158
  - Separate inbound issue: Dell SR re-POST hitting `ServiceRequests.number` UNIQUE constraint →
116
159
  API 400 "Duplicate entry SR129013" (REQ4444748). Not part of the email fix.
117
160
 
@@ -132,5 +175,20 @@ stamp (crash hits the next iteration). With multiple New-type POs, a crash on PO
132
175
  all subsequent POs in the same run.
133
176
 
134
177
  ## Change history
178
+ - 2026-07-10 — Third regression of the "Order Shipped / Order Delivered Email Not Sent" symptom.
179
+ True trigger: `createAssetAndRetryPut()` called the nonexistent `App_Database::escapeString()`
180
+ (~line 158 in `transmit_ordershipped` + `transmit_closecomplete`) — a fatal `Error` under E_ALL
181
+ that fired on the ServiceNow 404 asset auto-create path (routine for Breakfix/Reclaim/Refresh),
182
+ killing Phase 1 after the transmit stamp but before the email. Fixed `escapeString` -> `sqlEscape`
183
+ in both files (verified by 3 adversarial subagents; php -l clean). Recovered by nulling the
184
+ transmit flags for 9 stranded shipped POs + 1 closed PO (re-processing is idempotent) — the 9
185
+ owed shipped emails were confirmed sent 2026-07-10 09:34-09:35.
186
+ Also: discovered `c_dtTransmittedOrderClosedUpdateToPrudential` is overloaded across
187
+ Delivered vs Rejected/Cancelled (only Delivered owes an email), and fixed the
188
+ `prudential_exception_report.php` Order-Delivered discrepancy query to require an EXISTS on a
189
+ DELIVERED tracking number (drops Rejected/Cancelled false positives; REQ4448998/PO 26544 was one).
190
+ The structural two-phase stamp-before-email flaw remains **open** — candidate durable fix: stamp
191
+ the email flag only after a successful send and/or wrap each PO's email phase in try/catch. Not
192
+ done this session (scoped to the specific trigger) (rgirish)
135
193
  - 2026-07-06 — Found the real root cause: `file_get_contents` throws (E_ALL→ErrorException) so the `!== false` guards (incl. the 2026-06-18 one) were never effective. Added `safeFetchPdf()` (try/catch ErrorException → false) + independent per-PDF guards; email now always sends even if a PDF fails. Extended the same fix to `transmit_closecomplete` and both `generate_sales_and_purchase_orders_from_service_requests` scripts, and finished the stale Mac PDF URL fix in the 3 files the 2026-06-18 change missed. Documented the permanent two-phase stranding flaw and the date-bounded-recovery requirement; scope kept to fix-forward (no auto self-heal) by developer decision (rgirish)
136
194
  - 2026-06-18 — Fixed 403 crash: corrected Mac PDF URL (`_15.6_v4` → `PrudentialMacSetupGuide.pdf`) and added a `!== false` guard on all S3 fetches in New branch. NOTE (2026-07-06): this guard was ineffective — the exception is thrown inside `file_get_contents`, not returned (rgirish)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.309",
3
+ "version": "1.0.311",
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",