toga-ai 1.0.773 → 1.0.774
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/worker/INDEX.md +1 -1
- package/knowledge/1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md +156 -11
- package/knowledge/1.0/apps/worker/workflows/tracing-a-worker-cron-run-in-production.md +22 -1
- package/knowledge/2.0/apps/worker2/features/all-client-email-queue-monitor.md +15 -2
- package/knowledge/2.0/apps/worker2/features/oneuptime-worker2-monitoring.md +68 -6
- package/knowledge/2.0/apps/worker2/features/tracking-status-refresh.md +45 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-canada/INDEX.md +2 -2
- package/knowledge/clients/compass-canada/features/french-order-email-localization.md +2 -2
- package/knowledge/clients/compass-canada/features/grand-and-toy-asn-import.md +2 -2
- package/knowledge/clients/compass-canada/features/order-fulfillment-status-per-line.md +1 -1
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/contact-email-resolution.md +149 -0
- package/knowledge/clients/compass-usa/features/order-fulfillment-status-per-line.md +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| [Canon received-PO → NetSuite sales order sync](features/canon-received-po-netsuite-order-sync.md) | A 1.0 `worker` cron that reads Canon customer purchase orders out of the **Vision** databases and creates the matching **sales order in NetSuite**. | worker/crons/sync/sync_received_po.php, library/netsuitetoolkit/NSPHPClient.php |
|
|
7
7
|
| [Compass MA Sales Order Exception Report](features/compass-ma-sales-order-exception-report.md) | A worker cron that emails operations the "Compass Refresh Exception Report" — Compass `MA%` sales orders whose corresponding Office Depot (ODP) sales order has | worker/crons/toga2/compass/workflow/7_generate_ma_sales_order_exception_report.php, worker/schedules/cron.worker.sync.json |
|
|
8
8
|
| [Compass Manager Approval Reminder Emails (1.0 worker crons)](features/compass-manager-approval-reminder-emails.md) | Two 1.0 worker crons nag approvers about sales orders still waiting on a decision — one per Compass tenant. | worker/crons/toga2/compass/compass_email_reminders.php, worker/crons/toga2/compasscanada/compass_email_reminders.php, worker/schedules/cron.worker.sync.json, worker1.5/crons/toga2/compass/compass_email_reminders.php, worker1.5/schedules/cron.worker.json |
|
|
9
|
-
| [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compass/send_delivered_email.php, worker/crons/toga2/compasscanada/
|
|
9
|
+
| [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compass/send_delivered_email.php, worker/crons/toga2/compasscanada/update_salesorder_status_from_g&t.php, worker/crons/toga2/compasscanada/send_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, library/app/client/compasscanada.php, worker/schedules/cron.worker.sync.json, worker2/Worker/Monitor/Compass.php, dbchanges2/Client_Compass/2026-09-03 - compass_intransit_email_backlog_backfill.sql |
|
|
10
10
|
| [Elite TOGA 2.0 → TOGaDeskSupport Standalone Attachment Sync](features/elite-togadesk-attachment-sync.md) | `sync_togadesk_elite_attachments.php` is a standalone cron (every 5 minutes) that syncs file attachments from TOGA 2.0 into TOGaDeskSupport for Elite. | worker/crons/toga2/elite/sync_togadesk_elite_attachments.php, worker/crons/toga2/elite/test_sync_togadesk_elite_attachments.php |
|
|
11
11
|
| [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, worker2/Component/Forecast/SaleImport/SaleImport.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, tools/bin/forecast/fixer.php, tools/bin/forecast/checker.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/reconcile_drift_2023plus.php, test/@dave/probe_invoice_gap_2026.php, test/@dave/probe_creditmemo_gap_detail.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
|
|
12
12
|
| [Forecast2 Supporting-Records Nightly Import (1.0 cron) — Customers & ConsolidatedCustomers](features/forecast2-supporting-records-import.md) | The nightly **1.0** pull that keeps the Forecast2 lookup/dimension tables (`Forecast.Accounts`, `Classifications`, `Customers`, `ConsolidatedCustomers`, `Employ | worker/crons/toga2/forecast2/import_supporting_records.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/schedules/cron.worker.infrastructure.json, worker/.ebextensions/009_setup_phpini.config, worker/crons/sync/netsuite/netsuite_customers.php, library/app/api/netsuite/rest.php, library/app/model/forecast2/customer.php, library/app/model/forecast2/consolidatedcustomer.php |
|
|
@@ -6,18 +6,23 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-04
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/compass/update_salesorder_status_from_odp.php
|
|
13
13
|
- worker/crons/toga2/compass/send_delivered_email.php
|
|
14
|
-
- worker/crons/toga2/compasscanada/
|
|
15
|
-
- worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php
|
|
14
|
+
- worker/crons/toga2/compasscanada/update_salesorder_status_from_g&t.php
|
|
16
15
|
- worker/crons/toga2/compasscanada/send_delivered_email.php
|
|
17
16
|
- worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php
|
|
18
17
|
- worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php
|
|
19
18
|
- library/app/client/compasscanada.php
|
|
19
|
+
- worker/schedules/cron.worker.sync.json
|
|
20
|
+
- worker2/Worker/Monitor/Compass.php
|
|
21
|
+
- dbchanges2/Client_Compass/2026-09-03 - compass_intransit_email_backlog_backfill.sql
|
|
20
22
|
related:
|
|
23
|
+
- ../../../clients/compass-usa/features/contact-email-resolution.md
|
|
24
|
+
- ../../../2.0/apps/worker2/features/tracking-status-refresh.md
|
|
25
|
+
- ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
|
|
21
26
|
- ../../../clients/compass-canada/features/french-order-email-localization.md
|
|
22
27
|
- ../../../clients/compass-usa/features/order-fulfillment-status-per-line.md
|
|
23
28
|
- ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
@@ -34,18 +39,28 @@ and earlier packages). The dynamic HTML is injected into a stored `EmailTemplate
|
|
|
34
39
|
|
|
35
40
|
## Key files / entry points
|
|
36
41
|
|
|
37
|
-
- `worker/crons/toga2/compass/update_salesorder_status_from_odp.php` — Compass USA prod cron
|
|
38
|
-
|
|
39
|
-
—
|
|
42
|
+
- `worker/crons/toga2/compass/update_salesorder_status_from_odp.php` — Compass USA prod cron,
|
|
43
|
+
registered in `worker/schedules/cron.worker.sync.json` as **"Update Compass SalesOrder Status
|
|
44
|
+
From ODP CXML"**, schedule **`0 */4 * * *`** — it fires at 00/04/08/12/16/20 **US/Central**
|
|
45
|
+
(the prod DB timezone). Every threshold below (batch cap, staleness, monitor alarms) is
|
|
46
|
+
calibrated to that 4-hour cadence.
|
|
47
|
+
- `worker/crons/toga2/compasscanada/update_salesorder_status_from_g&t.php` — Compass Canada
|
|
48
|
+
prod cron, registered as "Update CompassCanada SalesOrder Status From G&T CXML".
|
|
49
|
+
**⚠ This filename contains `&` and therefore cannot run — see the gotcha below.** It was
|
|
50
|
+
renamed from `update_salesorder_status_from_odp.php` on 2026-09-03; the duplicate
|
|
51
|
+
`workflow/3_update_salesorder_status_from_grand_and_toy.php` was **deleted** in the same pass and
|
|
52
|
+
its stale `schedules/cron.worker.sync.json` entry removed.
|
|
40
53
|
- Shared helper set in each cron: `getOrderFulfillmentData`, `buildItemRowsHtml`,
|
|
41
54
|
`buildSectionHeaderHtml`, `buildPackageBlockHtml`, `buildTrackingUrl`, `buildOrderItemsHtml`.
|
|
42
55
|
|
|
43
56
|
## How it works
|
|
44
57
|
|
|
45
58
|
1. **Driving query** finds shipped sales orders and their tracking numbers (`computedStatusSlug
|
|
46
|
-
= 'shipped'`, `c_dtInTransitEmailSent IS NULL`, date
|
|
47
|
-
(`AdvanceShippingNoticeItemUnits_TrackingNumbers`), one row per `TrackingNumbers.number
|
|
48
|
-
|
|
59
|
+
= 'shipped'`, `c_dtInTransitEmailSent IS NULL`, rolling date window) via the ASN chain
|
|
60
|
+
(`AdvanceShippingNoticeItemUnits_TrackingNumbers`), one row per `TrackingNumbers.number`,
|
|
61
|
+
**newest first and capped** — see *Draining the candidate set* below.
|
|
62
|
+
2. Per tracking number: skip if the shipment is already finished — checked first against the
|
|
63
|
+
stored `TrackingNumbers.status` (free), and only then with a live carrier call.
|
|
49
64
|
3. **`getOrderFulfillmentData`** returns the order's packages (grouped by tracking number,
|
|
50
65
|
ordered by `dtCreated`) and ordered items, sourced from the **`ItemFulfillmentItems`** line
|
|
51
66
|
bridge (`ItemFulfillmentItems` → `ItemFulfillments` (scoped to the buyer SO) →
|
|
@@ -58,6 +73,73 @@ and earlier packages). The dynamic HTML is injected into a stored `EmailTemplate
|
|
|
58
73
|
`App_Api_Toga2::send(..., 'POST', '/email-templates/sendEmail', $payload, [], true)`.
|
|
59
74
|
6. On a successful (non-throwing) send, set `c_dtInTransitEmailSent = NOW()`.
|
|
60
75
|
|
|
76
|
+
### Draining the candidate set: two-pass, newest-first, bounded
|
|
77
|
+
|
|
78
|
+
**This is the load-bearing part of the cron. Getting it wrong took Compass USA in-transit emails to
|
|
79
|
+
near zero for a week** (2026-08-27 → 2026-09-03: 130–190 a day before, then 7 / 1 / 6 / 0 / 1).
|
|
80
|
+
Customers were not told their order had shipped.
|
|
81
|
+
|
|
82
|
+
The original shape was: select **every** unstamped `TrackingNumbers` row since a hardcoded
|
|
83
|
+
`'2026-06-15'`, then call UPS/FedEx **live, one row at a time**. When a shipment turned out to be
|
|
84
|
+
already delivered the loop `continue`d **without stamping `c_dtInTransitEmailSent`** — so the same
|
|
85
|
+
row came back on the next run, forever. The candidate set grew to **2,379** and the run hit its
|
|
86
|
+
`App_Framework::cronInitialization(3600)` one-hour limit while still inside the June rows. It never
|
|
87
|
+
reached the day's new ASNs. Nothing errored; the cron just ran out of time on every tick.
|
|
88
|
+
|
|
89
|
+
Four changes fix it, and they are the pattern to copy for any *"poll an external API per row, then
|
|
90
|
+
mark the row done"* cron:
|
|
91
|
+
|
|
92
|
+
1. **Two passes.** Pass 1 walks the candidates and **stamps** every already-finished row so it
|
|
93
|
+
retires from the candidate set permanently. Pass 2 sends. A row can never again be examined
|
|
94
|
+
forever without being retired.
|
|
95
|
+
2. **Newest first, hard cap.** `ORDER BY MAX(TrackingNumbers.dtCreated) DESC` +
|
|
96
|
+
`LIMIT IN_TRANSIT_EMAIL_BATCH_SIZE` (**400**). Today's shipments are served before any backlog,
|
|
97
|
+
so a backlog can delay old mail but can never starve new mail.
|
|
98
|
+
3. **Rolling window, not a fixed date.** `IN_TRANSIT_EMAIL_LOOKBACK_DAYS = 14` →
|
|
99
|
+
`dtCreated >= DATE_SUB(NOW(), INTERVAL 14 DAY)`. A hardcoded cutoff date guarantees an
|
|
100
|
+
ever-growing scan; a rolling window is self-limiting.
|
|
101
|
+
4. **Trust the stored status before paying for a carrier call.** `isTrackingStatusFinal(?string)`
|
|
102
|
+
short-circuits when `TrackingNumbers.status` is already `DELIVERED` or `RETURNED_TO_SENDER`
|
|
103
|
+
(`TRACKING_FINAL_STATUSES`). worker2's
|
|
104
|
+
[centralized tracking-status refresh](../../../2.0/apps/worker2/features/tracking-status-refresh.md)
|
|
105
|
+
keeps that column current for every client, so this 1.0 cron should not re-ask the carrier about
|
|
106
|
+
a finished shipment.
|
|
107
|
+
|
|
108
|
+
### Resolving the recipient: a `Users` row is NOT required (USA)
|
|
109
|
+
|
|
110
|
+
The To address originally came only from `Users.email`, via `INNER JOIN Users ON Users.contactId =
|
|
111
|
+
…`. **EDI-created MR orders never get a `Users` row**, so those orders were silently absent from
|
|
112
|
+
the driving query — 12 MR orders with shipped parcels were never emailed and never even showed up
|
|
113
|
+
as a backlog.
|
|
114
|
+
|
|
115
|
+
The USA cron now resolves the address in this precedence:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
LEFT OUTER JOIN Users
|
|
119
|
+
ON Users.contactId = ShippedSalesOrders.contactId
|
|
120
|
+
AND Users.email IS NOT NULL AND Users.email <> ''
|
|
121
|
+
INNER JOIN Contacts
|
|
122
|
+
ON Contacts.id = ShippedSalesOrders.contactId
|
|
123
|
+
LEFT OUTER JOIN ContactEmailAddresses
|
|
124
|
+
ON ContactEmailAddresses.id = Contacts.primaryContactEmailAddressId
|
|
125
|
+
AND ContactEmailAddresses.contactId = Contacts.id # ownership guard
|
|
126
|
+
…
|
|
127
|
+
IFNULL(Users.email, ContactEmailAddresses.emailAddress) AS contactEmail
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The same `IFNULL` resolves the display name, and the `WHERE` requires the resolved address to be
|
|
131
|
+
non-empty.
|
|
132
|
+
|
|
133
|
+
**The `ContactEmailAddresses.contactId = Contacts.id` predicate is not optional.** About 14% of
|
|
134
|
+
Compass contacts point at an email row owned by a *different* contact, so without it the cron mails
|
|
135
|
+
the wrong person — see
|
|
136
|
+
[Compass contact email resolution](../../../clients/compass-usa/features/contact-email-resolution.md).
|
|
137
|
+
|
|
138
|
+
**Regression-checked, not assumed.** Over 60 days and **4,419 SA tracking rows** the old and new
|
|
139
|
+
queries both select **4,416**: 0 newly included, 0 lost, resolved address and name identical on all
|
|
140
|
+
4,416. The `Users` address always wins because it is first in the `IFNULL`, so the change is
|
|
141
|
+
strictly additive — it only reaches orders the old query could not see at all.
|
|
142
|
+
|
|
61
143
|
### Tracking-URL building (`buildTrackingUrl`)
|
|
62
144
|
|
|
63
145
|
The "Track Shipment" button and each per-package link resolve in this precedence:
|
|
@@ -100,7 +182,22 @@ the tracking number as emailed (it retries next run).
|
|
|
100
182
|
|
|
101
183
|
## Client variations
|
|
102
184
|
|
|
103
|
-
- **Compass USA** — English only; `update_salesorder_status_from_odp.php`.
|
|
185
|
+
- **Compass USA** — English only; `update_salesorder_status_from_odp.php`. Has the full
|
|
186
|
+
2026-09-03 hardening: two-pass drain, 400-row cap, 14-day window, `isTrackingStatusFinal()`,
|
|
187
|
+
**and** the `Users` → `Contacts`/`ContactEmailAddresses` recipient fallback with the ownership
|
|
188
|
+
guard.
|
|
189
|
+
- **⚠ Canada is only PARTLY at parity (verified 2026-09-04).**
|
|
190
|
+
`compasscanada/update_salesorder_status_from_g&t.php` has the batching changes
|
|
191
|
+
(`IN_TRANSIT_EMAIL_BATCH_SIZE = 400`, `IN_TRANSIT_EMAIL_LOOKBACK_DAYS = 14`,
|
|
192
|
+
`ORDER BY MAX(TrackingNumbers.dtCreated) DESC`, `isTrackingStatusFinal()`) but **still resolves
|
|
193
|
+
the recipient with `INNER JOIN Users … Users.email AS contactEmail`** — no `Contacts` /
|
|
194
|
+
`ContactEmailAddresses` fallback and no ownership guard. Canada therefore still cannot email a
|
|
195
|
+
contact that has no `Users` row. Port the recipient block if Canada ever gets EDI-created MR
|
|
196
|
+
orders.
|
|
197
|
+
- **Canada keeps its own retry behaviour on a carrier throw.** It leaves the row **unstamped** when
|
|
198
|
+
the carrier lookup throws, so the next run retries it; only rows confirmed *final* are stamped.
|
|
199
|
+
That is deliberate and must survive any future parity pass — stamping on a throw would silently
|
|
200
|
+
drop the email.
|
|
104
201
|
- **Compass Canada** — EN/FR by `UserGlobalSettings.settingId = 2` (FR when the value starts
|
|
105
202
|
with `fr`); French section labels and a French (HTML-entity) status message; uses
|
|
106
203
|
`htmlentities` in the section header builder and a 7-param French-aware `buildPackageBlockHtml`.
|
|
@@ -113,6 +210,33 @@ the tracking number as emailed (it retries next run).
|
|
|
113
210
|
|
|
114
211
|
## Gotchas / known issues
|
|
115
212
|
|
|
213
|
+
- **⚠ A cron filename containing `&` cannot run at all.** `worker/ebs/cron.worker.php:41` builds
|
|
214
|
+
the crontab command by plain unquoted concatenation
|
|
215
|
+
(`'php /var/www/html/crons/' . $cron->cron`), so cron reads `&` as *"background this and start a
|
|
216
|
+
new command"*: `update_salesorder_status_from_g&t.php` executes as
|
|
217
|
+
`…/update_salesorder_status_from_g` plus a stray `t.php`, and the real script never runs —
|
|
218
|
+
silently, on every tick. **The Canada cron is in this state as of 2026-09-04 and must be renamed
|
|
219
|
+
(e.g. `update_salesorder_status_from_gt.php`) with `schedules/cron.worker.sync.json` repointed.**
|
|
220
|
+
Never put `&` (or any shell metacharacter) in a cron filename. See
|
|
221
|
+
[tracing a 1.0 worker cron run](../workflows/tracing-a-worker-cron-run-in-production.md).
|
|
222
|
+
- **⚠ Invoice tracking numbers are NOT in scope and must never be counted as in-transit
|
|
223
|
+
backlog.** `POST /v2/invoice-tracking-numbers` creates `TrackingNumbers` rows bridged through the
|
|
224
|
+
**`InvoiceTrackingNumbers`** table — never the ASN chain. On 2026-09-03 alone, 98 were created
|
|
225
|
+
(11:15–11:40) and **zero** of them touch `AdvanceShippingNoticeItemUnits_TrackingNumbers`.
|
|
226
|
+
Anything that walks the ASN unit bridge (this cron, and the OneUptime queue monitor) correctly
|
|
227
|
+
ignores them. Their shape is distinctive: **`shippingCarrierId` set, `shippingMethodId` NULL**.
|
|
228
|
+
A naive `TrackingNumbers WHERE c_dtInTransitEmailSent IS NULL` count will read these as a backlog
|
|
229
|
+
that never drains.
|
|
230
|
+
- **Clear an in-transit backlog by STAMPING, not by emailing — and refresh statuses first.**
|
|
231
|
+
Decided 2026-09-03 while draining the outage backlog
|
|
232
|
+
(`dbchanges2/Client_Compass/2026-09-03 - compass_intransit_email_backlog_backfill.sql`):
|
|
233
|
+
`c_dtInTransitEmailSent` was stamped on **64,132** rows older than the 14-day window and **421**
|
|
234
|
+
in-window rows already on a final status, leaving **68** genuinely still moving. Sending an
|
|
235
|
+
"in transit" email for a June order that arrived two months ago confuses the customer, so old
|
|
236
|
+
rows retire silently. **Ordering matters:** let worker2 `Sync/TrackingNumbers` refresh any
|
|
237
|
+
recently-unparked rows **before** running the backfill, otherwise a row with a stale status gets
|
|
238
|
+
a wrong in-transit email. (A parked row holds a stale status indefinitely — see
|
|
239
|
+
[tracking-status refresh](../../../2.0/apps/worker2/features/tracking-status-refresh.md).)
|
|
116
240
|
- **Canada now uses the `ItemFulfillmentItems` line bridge, not the ASN chain**, for
|
|
117
241
|
`getOrderFulfillmentData` — same query as Compass USA. Canada was originally on the ASN chain
|
|
118
242
|
because it had **zero `ItemFulfillmentItems`**; if that line-bridge data is not populated for a
|
|
@@ -138,7 +262,7 @@ the tracking number as emailed (it retries next run).
|
|
|
138
262
|
language even though the mail goes to the **manager**. Fixed 2026-08-03.
|
|
139
263
|
- **⚠ Each cron keeps its OWN duplicated copy of the 2.0 `_status` CASE — a standing divergence
|
|
140
264
|
risk.** `compass/update_salesorder_status_from_odp.php`,
|
|
141
|
-
`compasscanada/
|
|
265
|
+
`compasscanada/update_salesorder_status_from_g&t.php` and
|
|
142
266
|
`compass/workflow/test_partial_in_transit_email.php` each re-implement the model's status SQL as
|
|
143
267
|
`computedStatusSlug`. Today that is safe by luck of scope: the copies only ever produce canceled /
|
|
144
268
|
pendingApprovalUnknown / pendingFulfillment / shipped and they filter on
|
|
@@ -161,6 +285,27 @@ the tracking number as emailed (it retries next run).
|
|
|
161
285
|
|
|
162
286
|
## Change history
|
|
163
287
|
|
|
288
|
+
- 2026-09-04 — **Fixed the Compass USA in-transit email outage (2026-08-27 → 2026-09-03, 130–190
|
|
289
|
+
emails/day → near zero).** Root cause: already-delivered rows were `continue`d without stamping
|
|
290
|
+
`c_dtInTransitEmailSent`, so the candidate set grew to 2,379 and every run burned its
|
|
291
|
+
`cronInitialization(3600)` hour inside rows dating back to a hardcoded `'2026-06-15'`, never
|
|
292
|
+
reaching new ASNs. Fix: two-pass drain (stamp finished rows, then send), newest-first
|
|
293
|
+
`ORDER BY MAX(dtCreated) DESC` with `IN_TRANSIT_EMAIL_BATCH_SIZE = 400`, rolling
|
|
294
|
+
`IN_TRANSIT_EMAIL_LOOKBACK_DAYS = 14` replacing the fixed date, and `isTrackingStatusFinal()`
|
|
295
|
+
short-circuiting the carrier call on `DELIVERED`/`RETURNED_TO_SENDER` using the status worker2
|
|
296
|
+
`Sync/TrackingNumbers` already maintains. Also **widened the recipient**: `LEFT OUTER JOIN Users`
|
|
297
|
+
+ `Contacts`/`ContactEmailAddresses` with an ownership guard and
|
|
298
|
+
`IFNULL(Users.email, ContactEmailAddresses.emailAddress)`, which recovered 12 EDI-created MR
|
|
299
|
+
orders that have no `Users` row (regression-checked: 4,416 of 4,419 SA rows selected by both old
|
|
300
|
+
and new queries, 0 gained, 0 lost, identical resolved address/name). Backlog cleared by
|
|
301
|
+
**stamping** 64,132 out-of-window + 421 final-status rows rather than emailing them. Recorded
|
|
302
|
+
that **invoice tracking numbers are not in scope** (bridged via `InvoiceTrackingNumbers`, never
|
|
303
|
+
the ASN chain) and that a **`&` in a cron filename stops the cron dead**. Canada got the batching
|
|
304
|
+
changes only (renamed to `update_salesorder_status_from_g&t.php`; duplicate
|
|
305
|
+
`workflow/3_update_salesorder_status_from_grand_and_toy.php` deleted); its recipient block is
|
|
306
|
+
**still `INNER JOIN Users`**. New OneUptime monitor `Monitor/Compass/InTransitEmailQueue` now
|
|
307
|
+
watches this queue — see
|
|
308
|
+
[OneUptime push-metric monitors](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md). (bala)
|
|
164
309
|
- 2026-08-26 — Flagged the **duplicated `_status` CASE** in the three crons as a standing divergence
|
|
165
310
|
risk after the 2.0 Compass fulfillment rule was rewritten. Verified the crons are unaffected: their
|
|
166
311
|
copies only emit canceled / pendingApprovalUnknown / pendingFulfillment / shipped and filter on
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-04
|
|
10
10
|
owners: ["bala", "kyalamarthi"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/ebs/cron.worker.php
|
|
@@ -170,7 +170,28 @@ to **2.0 only**. The 1.0 standard wins in `library/`, `worker/` and every other
|
|
|
170
170
|
`library/app/systemmonitor/500error.php` uses `str_contains()`; that file is not a version
|
|
171
171
|
signal, it is a latent bug.
|
|
172
172
|
|
|
173
|
+
## Deploy-time trap: a `&` (or any shell metacharacter) in the cron FILENAME silently kills it
|
|
174
|
+
`ebs/cron.worker.php:41` builds each crontab line by plain string concatenation
|
|
175
|
+
(`'php /var/www/html/crons/' . $cron->cron`) with **no quoting and no escaping**. Cron then parses
|
|
176
|
+
that line as a shell command, so a `&` in the filename is read as *"run everything before this in
|
|
177
|
+
the background and start a new command."*
|
|
178
|
+
|
|
179
|
+
Worked example (found 2026-09-03): a developer renamed the Compass Canada status cron to
|
|
180
|
+
`crons/toga2/compasscanada/update_salesorder_status_from_g&t.php` and repointed
|
|
181
|
+
`schedules/cron.worker.sync.json` at it. The crontab line becomes two commands —
|
|
182
|
+
`php .../update_salesorder_status_from_g` (a file that does not exist) and a stray `t.php` — so the
|
|
183
|
+
real script **never executes**, on every tick, with no error anywhere. It looks exactly like
|
|
184
|
+
"the cron never fired".
|
|
185
|
+
|
|
186
|
+
**Rule: a cron filename must be plain `[a-z0-9_]` plus `.php`.** No `&`, no spaces, no `;`, `|`,
|
|
187
|
+
`$`, `(`, `)`, `*`, `?`, or quotes. The *display name* in `schedules/*.json` is free text and may
|
|
188
|
+
contain anything; only the `cron` path is shell-parsed.
|
|
189
|
+
|
|
173
190
|
## Change history
|
|
191
|
+
- 2026-09-04 — Added the **`&`-in-a-cron-filename trap**: `ebs/cron.worker.php:41` concatenates the
|
|
192
|
+
path into the crontab line unquoted, so a shell metacharacter splits the command and the script
|
|
193
|
+
never runs while leaving no trace. Found via the renamed Compass Canada
|
|
194
|
+
`update_salesorder_status_from_g&t.php`. (bala)
|
|
174
195
|
- 2026-09-01 — Added **Step 6: check the email log, not the inbox** — a 1.0 cron's sends land in
|
|
175
196
|
the **legacy** env's `Logs.Emails` (schema `Logs`, table `Emails`), never prod `Logs` or
|
|
176
197
|
`Logs_<Client>`; gave the verification query, and noted that a missing row usually means the job
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: ["jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Monitor/Operations.php
|
|
13
13
|
- _underscore/Database.php
|
|
@@ -111,6 +111,15 @@ matter are the **log database name** and whether the `Email` migration has been
|
|
|
111
111
|
|
|
112
112
|
## Gotchas / known issues
|
|
113
113
|
|
|
114
|
+
- **⚠ This monitor is blind to an email that was never queued.** It reads `Logs_<Client>.Email`,
|
|
115
|
+
so it can only see a send that already produced a row. If the producing cron dies, stalls or
|
|
116
|
+
never selects the work, this monitor reports a **healthy empty queue**. That is exactly what
|
|
117
|
+
happened during the Compass USA in-transit outage (2026-08-27 → 2026-09-03): the 1.0 sending
|
|
118
|
+
cron timed out before creating a single email row and this monitor stayed green all week. Pair
|
|
119
|
+
any email-queue monitor with a **backlog monitor on the producer's input** — first instance
|
|
120
|
+
`Monitor/Compass/InTransitEmailQueue`, see
|
|
121
|
+
[OneUptime push-metric monitors](./oneuptime-worker2-monitoring.md).
|
|
122
|
+
|
|
114
123
|
- **A client's database name is NOT derivable from `clientIdentifier`.** `Compass_Usa` →
|
|
115
124
|
`Logs_Compass`. Always resolve `Clients.logDatabaseId` → `Databases.name`. Building
|
|
116
125
|
`'Logs_' . $clientIdentifier` silently targets a nonexistent database. See
|
|
@@ -150,6 +159,10 @@ matter are the **log database name** and whether the `Email` migration has been
|
|
|
150
159
|
against framework source only, not against real rows.
|
|
151
160
|
|
|
152
161
|
## Change history
|
|
162
|
+
- 2026-09-04 — Recorded the monitor's **structural blind spot**: it only sees emails that reached
|
|
163
|
+
`Logs_<Client>.Email`, so a producer that never queues anything reads as healthy (proved by the
|
|
164
|
+
week-long Compass USA in-transit outage). Points at the new
|
|
165
|
+
`Monitor/Compass/InTransitEmailQueue` input-backlog monitor as the complement. (bala)
|
|
153
166
|
|
|
154
167
|
- 2026-07-30 — Refactored `EmailQueue()` from Compass-only (hardcoded `Logs_Compass` via
|
|
155
168
|
`initialize()` + `const DB_LOGS_COMPASS`, both removed) to an all-client fan-out
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-04
|
|
10
10
|
owners: ["jcardinal", "mhammontree", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Monitor/Compass.php
|
|
@@ -23,6 +23,7 @@ files:
|
|
|
23
23
|
- worker2/Config/sandbox-dev.ini
|
|
24
24
|
related:
|
|
25
25
|
- ./oneuptime-incident-clickup-sync.md
|
|
26
|
+
- ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
|
|
26
27
|
- ./monitoring-framework.md
|
|
27
28
|
- ./all-client-email-queue-monitor.md
|
|
28
29
|
- ../../_underscore/features/cloud-s3-helpers.md
|
|
@@ -36,7 +37,7 @@ A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, p
|
|
|
36
37
|
the 1.0 `App_SystemMonitor_Compass` monitors. A worker2 cron action runs a self-contained
|
|
37
38
|
check, decides pass/fail itself, and POSTs a JSON metric body to a OneUptime "Incoming
|
|
38
39
|
Request" monitor — replacing the 1.0 tier's email alerts. The class is
|
|
39
|
-
`abstract class _Worker_Monitor_Compass` and now carries a **suite of
|
|
40
|
+
`abstract class _Worker_Monitor_Compass` and now carries a **suite of 10 monitors** covering
|
|
40
41
|
the Compass USA / Office Depot integration (S3, DB, and mailbox checks). Designed to be
|
|
41
42
|
**client-agnostic** — a reusable template for future monitors and clients.
|
|
42
43
|
|
|
@@ -59,7 +60,7 @@ that reports straight to OneUptime. Note the folder difference: these live under
|
|
|
59
60
|
- `_underscore/Cloud.php` — `_Cloud::getS3Objects()` list helper the S3 monitors rely on
|
|
60
61
|
(see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
|
|
61
62
|
|
|
62
|
-
## The
|
|
63
|
+
## The 10 monitors
|
|
63
64
|
|
|
64
65
|
Each measures one Compass/Office Depot integration metric and pushes a decided alarm token.
|
|
65
66
|
- **OfficeDepotEdiImportQueue** (S3) — counts files >10 min old in
|
|
@@ -106,6 +107,11 @@ Each measures one Compass/Office Depot integration metric and pushes a decided a
|
|
|
106
107
|
logic is superseded by this corrected definition.)
|
|
107
108
|
- **OfficeDepotAs2Outbox** (S3) — counts ONLY top-level files in
|
|
108
109
|
`s3://agilant-as2/OfficeDepot/OUTBOX/` (skips subfolders + directory placeholders).
|
|
110
|
+
- **InTransitEmailQueue** (DB, added 2026-09-04) — watches the Compass USA **in-transit
|
|
111
|
+
shipping-notification queue**: `TrackingNumbers` rows on the ASN unit bridge with
|
|
112
|
+
`c_dtInTransitEmailSent IS NULL` inside a 14-day window (the window the 1.0 sending cron uses).
|
|
113
|
+
Status-page name *"Shipping Notification Emails (USA, In Transit)"*. See
|
|
114
|
+
[why it exists](#watch-the-step-before-the-log-row-exists) below.
|
|
109
115
|
|
|
110
116
|
## How it works
|
|
111
117
|
|
|
@@ -185,12 +191,58 @@ its error messages are the only documentation, so the verified shape:
|
|
|
185
191
|
- `incidentSeverityId` is wrapped: `{_type: "ObjectID", value: "…"}`.
|
|
186
192
|
- There is **no `projectId`** in the export, and **status/severity ObjectIDs are
|
|
187
193
|
project-specific** — clone them out of an existing monitor's export rather than inventing them.
|
|
194
|
+
- **⚠ Every description in the file is capped at 500 characters.** The importer rejects the whole
|
|
195
|
+
file with `description length cannot be more than 500 characters`. It applies to the **monitor**
|
|
196
|
+
description **and** to every **criteria** and **incident** description in the same file, so a
|
|
197
|
+
chatty runbook paragraph inside a criterion silently blocks the import. Keep each description
|
|
198
|
+
short and put the detail in the knowledge base.
|
|
188
199
|
|
|
189
200
|
**Practical fix for the [activation-ordering trap](#gotchas--known-issues):** instead of
|
|
190
201
|
importing the monitor twice (body criteria first, absence criteria after the first push), import
|
|
191
202
|
the absence criteria **in the same file with `isEnabled: false`** and simply flip them on once a
|
|
192
203
|
push has landed.
|
|
193
204
|
|
|
205
|
+
### Watch the step BEFORE the log row exists
|
|
206
|
+
|
|
207
|
+
A queue monitor that reads a **log** table can only see work that already produced a row. The
|
|
208
|
+
all-client [Email Queue Monitor](./all-client-email-queue-monitor.md) watches
|
|
209
|
+
`Logs_<Client>.Email` for `FAILED` and stale `PENDING` — excellent once an email row exists, and
|
|
210
|
+
**structurally blind before that**. During the Compass USA in-transit outage (2026-08-27 →
|
|
211
|
+
2026-09-03) the sending cron never got far enough to create a single email row, so that monitor
|
|
212
|
+
reported a perfectly healthy, empty queue for a week while customers were not being told their
|
|
213
|
+
orders had shipped.
|
|
214
|
+
|
|
215
|
+
**Rule: for any producer→log pipeline, monitor the producer's INPUT backlog, not only its output
|
|
216
|
+
log.** `InTransitEmailQueue` is the worked example — it counts unsent candidates in the source
|
|
217
|
+
table, so the alarm fires even when nothing downstream ever runs. Its thresholds
|
|
218
|
+
(`Monitor/Compass/InTransitEmailQueue`, `*/5 * * * *`):
|
|
219
|
+
|
|
220
|
+
| Signal | DEGRADED | HIGH |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| `queuedRows` (unsent, in-window) | > 400 | > 800 |
|
|
223
|
+
| `oldestQueuedHours` | > 5 | > 9 |
|
|
224
|
+
| `lastStampHoursAgo` while `queuedRows > 0` | — | > 9 |
|
|
225
|
+
| heartbeat not received | 10 min | 15 min |
|
|
226
|
+
|
|
227
|
+
The row thresholds come straight from the sending cron's `IN_TRANSIT_EMAIL_BATCH_SIZE = 400`
|
|
228
|
+
(past 400 it cannot catch up in one pass; 800 is two passes behind) and the hour thresholds from its
|
|
229
|
+
`0 */4 * * *` schedule (worst normal wait is just under 4h, so 5h means a shipment missed its run
|
|
230
|
+
and 9h means two). This is the *"calibrate against the tier that runs the business logic"* rule
|
|
231
|
+
applied literally — see below. A DB read failure pushes `"status":"error"` so "we cannot see"
|
|
232
|
+
never reads as healthy.
|
|
233
|
+
|
|
234
|
+
**It reports `unroutableRows` (candidates with no emailable recipient) as a plain number that
|
|
235
|
+
deliberately does not drive the alarm** — otherwise the monitor would sit red forever on rows the
|
|
236
|
+
cron structurally cannot send. That is the *"backlog monitors must exclude known-permanently-stranded
|
|
237
|
+
rows"* rule.
|
|
238
|
+
|
|
239
|
+
> **⚠ Known divergence (2026-09-04):** the monitor decides routable/unroutable from the
|
|
240
|
+
> `Users` join **only**, while the sending cron now also falls back to
|
|
241
|
+
> `Contacts` / `ContactEmailAddresses`. So some rows counted as `unroutableRows` are in fact
|
|
242
|
+
> sendable, and they are excluded from `queuedRows`. Port the cron's recipient fallback into the
|
|
243
|
+
> monitor query to bring the two definitions back together — see
|
|
244
|
+
> [Compass in-transit emails](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
|
|
245
|
+
|
|
194
246
|
### Heartbeat / cron cadence timing
|
|
195
247
|
|
|
196
248
|
The 10-min-Degraded / 15-min-Offline heartbeat thresholds pair with a **5-minute** cron
|
|
@@ -317,9 +369,9 @@ Testing **recovery** is the step people skip and the most valuable one:
|
|
|
317
369
|
### CronJob scheduling for these monitors
|
|
318
370
|
|
|
319
371
|
Run **all hours**, `*/5 * * * *` (every 5 min) — **not** the 1.0 `6-20` business-hours
|
|
320
|
-
window. Integration backlogs must be caught outside business hours too. The nine
|
|
321
|
-
`Monitor/Compass/*` actions are `Core.CronJobs` **ids 27–35** (EmailedAsnImport = id 31),
|
|
322
|
-
each `maxExecutionTime` **120s** — the reason any in-run retry budget must stay well under 120s.
|
|
372
|
+
window. Integration backlogs must be caught outside business hours too. The original nine
|
|
373
|
+
`Monitor/Compass/*` actions are `Core.CronJobs` **ids 27–35** (EmailedAsnImport = id 31), with
|
|
374
|
+
`InTransitEmailQueue` added on the same `*/5` cadence, each `maxExecutionTime` **120s** — the reason any in-run retry budget must stay well under 120s.
|
|
323
375
|
|
|
324
376
|
## DB monitor conventions
|
|
325
377
|
|
|
@@ -438,6 +490,16 @@ check for another client.
|
|
|
438
490
|
monitors must pass the region (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
|
|
439
491
|
|
|
440
492
|
## Change history
|
|
493
|
+
- 2026-09-04 — Added a **10th monitor, `InTransitEmailQueue`** (Compass USA shipping-notification
|
|
494
|
+
backlog: unsent `TrackingNumbers` on the ASN unit bridge inside the cron's 14-day window;
|
|
495
|
+
DEGRADED >400 rows / >5h oldest, HIGH >800 rows / >9h oldest / >9h with nothing stamped while
|
|
496
|
+
rows wait; thresholds derived from the 1.0 cron's 400-row cap and `0 */4 * * *` schedule;
|
|
497
|
+
`unroutableRows` reported but never alarmed; DB failure pushes `status:"error"`). Wrote up the
|
|
498
|
+
general lesson it embodies — **monitor the producer's input backlog, not only its output log** —
|
|
499
|
+
after the all-client Email Queue Monitor showed a healthy empty queue through a week-long outage
|
|
500
|
+
in which no email row was ever created. Recorded the **500-character cap on every description**
|
|
501
|
+
in a OneUptime resource-import file (monitor, criteria and incident descriptions alike), and the
|
|
502
|
+
monitor-vs-cron `unroutableRows` divergence. (bala)
|
|
441
503
|
- 2026-09-03 — **Fixed a false HIGH age alarm** in `CompassSalesOrderTransmission()`.
|
|
442
504
|
`oldestPendingAgeMinutes` was measured from **stage-2 approval only**
|
|
443
505
|
(`TIMESTAMPDIFF(MINUTE, ApprovalDecisionsStage2.dtDecision, NOW())`), but stage 2 is **not**
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-04
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Sync/TrackingNumbers.php
|
|
@@ -169,6 +169,42 @@ to slug **`beta`** or **`sandbox-dev`**, **not `dev`**, and those enumerate a **
|
|
|
169
169
|
client set than `dev`. Confirm the box's actual `ENVIRONMENT` value before trusting the dispatcher's
|
|
170
170
|
client enumeration on beta.
|
|
171
171
|
|
|
172
|
+
### ⚠ A PARKED row holds a STALE status forever — and downstream code now trusts that status
|
|
173
|
+
|
|
174
|
+
A row parked after repeated carrier failures gets `c_dtTrackingParked` /
|
|
175
|
+
`c_trackingParkReason = 'MAX_FAILURES'` (20 failed lookups) and its `c_dtTrackingCheckNext` cursor
|
|
176
|
+
stops being advanced, so **the row is never re-checked and `TrackingNumbers.status` freezes at
|
|
177
|
+
whatever it last was** — typically an early `IN_TRANSIT`. Parking is silent: nothing distinguishes
|
|
178
|
+
a stale status from a fresh one at the point of read.
|
|
179
|
+
|
|
180
|
+
Measured 2026-09-03: **189 Compass rows created 2026-08-20 → 2026-08-29 were parked on
|
|
181
|
+
`MAX_FAILURES`.** Unparking restores them:
|
|
182
|
+
|
|
183
|
+
```sql
|
|
184
|
+
UPDATE TrackingNumbers
|
|
185
|
+
SET
|
|
186
|
+
c_dtTrackingParked = NULL,
|
|
187
|
+
c_trackingParkReason = NULL,
|
|
188
|
+
c_trackingCheckFailures = 0,
|
|
189
|
+
c_dtTrackingCheckNext = NOW()
|
|
190
|
+
WHERE
|
|
191
|
+
c_trackingParkReason = 'MAX_FAILURES'
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The refresh cron is **`Core.CronJobs` id 69, action `Sync/TrackingNumbers/Refresh`, every 10
|
|
195
|
+
minutes**, so the unparked rows pick up a real status within a couple of ticks.
|
|
196
|
+
|
|
197
|
+
**Why this now matters more than it used to.** The 1.0 Compass in-transit email cron short-circuits
|
|
198
|
+
its carrier call when `TrackingNumbers.status` is already `DELIVERED` / `RETURNED_TO_SENDER`, i.e.
|
|
199
|
+
it **trusts this column**. A parked row therefore keeps a shipment in the email queue (or, in the
|
|
200
|
+
opposite direction, can drive a wrong decision) with no visible error. **Before any bulk operation
|
|
201
|
+
that reads `TrackingNumbers.status` — a backfill, a reconciliation, a mass stamp — unpark and let
|
|
202
|
+
the refresh catch up first.** See
|
|
203
|
+
[Compass in-transit emails](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
|
|
204
|
+
|
|
205
|
+
Open question: nothing today re-tries or reports parked rows. A periodic unpark (or a monitor on
|
|
206
|
+
parked-row count) is unbuilt work.
|
|
207
|
+
|
|
172
208
|
### ⚠ Conflict with the 1.0 Compass delivered-email cron
|
|
173
209
|
The in-transit / delivered emails **today** come from a separate **1.0** Compass worker cron
|
|
174
210
|
(`worker/crons/toga2/compass/send_delivered_email.php`) that polls UPS itself, sends via api2
|
|
@@ -190,6 +226,14 @@ Plan (not built): move the email logic into a Compass `postPut` interceptor and
|
|
|
190
226
|
check, and **resolve the PHP-8.5 worker → api2 crash** (the gate).
|
|
191
227
|
|
|
192
228
|
## Change history
|
|
229
|
+
- 2026-09-04 — Recorded that **parked rows hold a stale status indefinitely**: 189 Compass rows
|
|
230
|
+
(created 2026-08-20 → 08-29) sat on `c_trackingParkReason = 'MAX_FAILURES'` after 20 failed
|
|
231
|
+
carrier lookups, with `c_dtTrackingCheckNext` no longer advancing, so `TrackingNumbers.status`
|
|
232
|
+
froze silently. Added the unpark recipe and noted the cron identity (`Core.CronJobs` id 69,
|
|
233
|
+
`Sync/TrackingNumbers/Refresh`, `*/10`). This got sharper because the 1.0 Compass in-transit
|
|
234
|
+
email cron now **trusts** `TrackingNumbers.status` to skip a carrier call, so any bulk read of
|
|
235
|
+
that column must unpark and let the refresh catch up first. No periodic unpark or parked-row
|
|
236
|
+
monitor exists yet. (bala)
|
|
193
237
|
- 2026-08-24 — Built the centralized tracking-status refresh: dispatcher + per-client child
|
|
194
238
|
(`_Worker_Sync_TrackingNumbers`), per-row `c_dtTrackingCheckNext` re-check cursor (+1h imminent /
|
|
195
239
|
+6h else, jitter, terminal → NULL, 45-day cutoff), `PAGE_CAP=50`/`SOFT_TIME_BUDGET_SECONDS=40`
|
package/knowledge/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
18
18
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
+
- **_underscore** (_Underscore) _(framework core)_ — 81 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 59 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 15 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [French (fr-CA) Item Feature Translations (Compass Canada)](features/french-item-feature-translations.md) | 2.0 | Renders item **feature** text on the Compass Canada French storefront — feature names, feature-group headers (e.g. | _underscore/Model/Compass/Canada/Feature.php, _underscore/Model/Compass/Canada/ItemCategoryFeatureGroup.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, worker2/Worker/Etilize/ItemTranslations.php, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql, dbchanges2/Client_CompassCanada/2026-07-13 - FeatureAttributeCustomFields.sql, dbchanges2/Client_CompassCanada/2026-07-13 - DedupeItemFeaturesAndGroups.sql, dbchanges2/Client_CompassCanada/2026-07-13 - SeedFrenchFeatureTranslations.sql |
|
|
6
|
-
| [French (fr-CA) Order Email Localization (Compass Canada)](features/french-order-email-localization.md) | 2.0 | Compass Canada order emails (order requested, manager-approval request, approved/rejected, in-transit, delivered, reminders) are sent in **each recipient's own | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/ApprovalDecision.php, _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/String.php, library/app/client/compasscanada.php, worker/crons/toga2/compasscanada/send_delivered_email.php, worker/crons/toga2/compasscanada/compass_email_reminders.php, worker/crons/toga2/compasscanada/compass_cancel_pending_approval_orders.php, worker/crons/toga2/compasscanada/
|
|
7
|
-
| [Grand & Toy ASN Import (Compass Canada)](features/grand-and-toy-asn-import.md) | 2.0 | Imports Grand & Toy (G&T) Advance Shipping Notices for Compass Canada. | api2/Component/Api/Cxml/Cxml.php, worker/crons/toga2/compasscanada/workflow/
|
|
6
|
+
| [French (fr-CA) Order Email Localization (Compass Canada)](features/french-order-email-localization.md) | 2.0 | Compass Canada order emails (order requested, manager-approval request, approved/rejected, in-transit, delivered, reminders) are sent in **each recipient's own | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/ApprovalDecision.php, _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/String.php, library/app/client/compasscanada.php, worker/crons/toga2/compasscanada/send_delivered_email.php, worker/crons/toga2/compasscanada/compass_email_reminders.php, worker/crons/toga2/compasscanada/compass_cancel_pending_approval_orders.php, worker/crons/toga2/compasscanada/update_salesorder_status_from_g&t.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php, worker/crons/toga2/compasscanada/workflow/3_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_email_previews_prod.php |
|
|
7
|
+
| [Grand & Toy ASN Import (Compass Canada)](features/grand-and-toy-asn-import.md) | 2.0 | Imports Grand & Toy (G&T) Advance Shipping Notices for Compass Canada. | api2/Component/Api/Cxml/Cxml.php, worker/crons/toga2/compasscanada/workflow/3_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php, worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/Canada/AdvanceShippingNotice.php, dbchanges2/Client_CompassCanada/ |
|
|
8
8
|
| [Compass Canada — Item-Fulfillment TableViews (dead-Core-id 500, rebuilt onto the USA shape)](features/item-fulfillment-tableviews.md) | 2.0 | `Client_CompassCanada`'s two item-fulfillment table views — **`item-fulfillments-for-sales-orders`** (`TableViews.id 12`) and **`item-fulfillments-for-sales-ord | dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql, dbchanges2/Client_CompassCanada/2026-08-26 - EnableTableViewFieldFilters.sql, _underscore/Model/Client/TableView.php |
|
|
9
9
|
| [Compass Canada — Fulfilled vs Partially Fulfilled (per-line, ASN bridge only)](features/order-fulfillment-status-per-line.md) | 2.0 | TOGa Supply showed **Fulfilled** on Compass Canada orders that were only partly shipped. | _underscore/Model/Compass/Canada/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php |
|
|
10
10
|
| [Compass Canada](profile.md) | 2.0 | Compass Canada is the Canadian arm of the Compass account — a separate TOGA tenant, related to but distinct from Compass USA. | |
|
|
@@ -16,9 +16,9 @@ files:
|
|
|
16
16
|
- worker/crons/toga2/compasscanada/send_delivered_email.php
|
|
17
17
|
- worker/crons/toga2/compasscanada/compass_email_reminders.php
|
|
18
18
|
- worker/crons/toga2/compasscanada/compass_cancel_pending_approval_orders.php
|
|
19
|
-
- worker/crons/toga2/compasscanada/
|
|
19
|
+
- worker/crons/toga2/compasscanada/update_salesorder_status_from_g&t.php
|
|
20
20
|
- worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php
|
|
21
|
-
- worker/crons/toga2/compasscanada/workflow/
|
|
21
|
+
- worker/crons/toga2/compasscanada/workflow/3_import_grand_and_toy_advance_shipping_notices.php
|
|
22
22
|
- worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php
|
|
23
23
|
- worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php
|
|
24
24
|
- worker/crons/toga2/compasscanada/workflow/test_email_previews_prod.php
|
|
@@ -9,7 +9,7 @@ updated: 2026-08-20
|
|
|
9
9
|
owners: ["bala"]
|
|
10
10
|
files:
|
|
11
11
|
- api2/Component/Api/Cxml/Cxml.php
|
|
12
|
-
- worker/crons/toga2/compasscanada/workflow/
|
|
12
|
+
- worker/crons/toga2/compasscanada/workflow/3_import_grand_and_toy_advance_shipping_notices.php
|
|
13
13
|
- worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php
|
|
14
14
|
- worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php
|
|
15
15
|
- worker/schedules/cron.worker.sync.json
|
|
@@ -65,7 +65,7 @@ serial numbers and the reference for the field mapping.
|
|
|
65
65
|
> non-prod patch also evaporates on the next tenant reseed — the same lesson as AIG TRUE-80562.)
|
|
66
66
|
|
|
67
67
|
## Key files / entry points
|
|
68
|
-
- `worker/crons/toga2/compasscanada/workflow/
|
|
68
|
+
- `worker/crons/toga2/compasscanada/workflow/3_import_grand_and_toy_advance_shipping_notices.php`
|
|
69
69
|
— the scheduled cron (1.0 worker tier). Reads mailbox `compasscanada.status@togatech.com`
|
|
70
70
|
(OAuth2 creds — currently hardcoded with a TODO; must move to `App_Registry::get('config')['compasscanada']['gt_asn_oauth2']`
|
|
71
71
|
before production), parses the 24-column CSV, posts to api2 `/advance-shipping-notices`.
|
|
@@ -160,7 +160,7 @@ them correctly read **pendingFulfillment**.
|
|
|
160
160
|
- **The badge is display-only and never stored.** The status is computed live, so there is no
|
|
161
161
|
backfill and reverting the code fully reverts the behaviour. **No cron, email or report reads the
|
|
162
162
|
fulfilled vs partiallyFulfilled distinction.** Canada's in-transit email cron
|
|
163
|
-
(`worker/crons/toga2/compasscanada/
|
|
163
|
+
(`worker/crons/toga2/compasscanada/update_salesorder_status_from_g&t.php`) keeps its **own
|
|
164
164
|
duplicated copy** of the `_status` CASE, but it only ever produces canceled /
|
|
165
165
|
pendingApprovalUnknown / pendingFulfillment / shipped and filters on
|
|
166
166
|
`computedStatusSlug = 'shipped'`. Note the Canada pendingFulfillment gate is **no longer
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**, VIP auto-approve, and EN/FR localization — lives **entirely i | _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Usa/ApprovalDecision.php, _underscore/Model/Compass/Canada/ApprovalDecision.php, _underscore/Model/Client/ApprovalTemplateStage.php, _underscore/Model/Quad/SalesOrder.php |
|
|
6
6
|
| [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
|
|
7
|
+
| [Compass Contact Email Resolution & Crossed `primaryContactEmailAddressId` Pointers](features/contact-email-resolution.md) | 2.0 | **Any Compass code that reads a contact's email address must also assert that the email row belongs to that contact.** Verified in prod on 2026-09-03: **42,926 | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/update_salesorder_status_from_odp.php |
|
|
7
8
|
| [Cost Centers — Unit Locations, numeric-only policy](features/cost-centers.md) | 2.0 | A Compass "cost center" — the value a user picks in commerce and that lands on an order — is **not** a `CostCenters` row. | toga2-commerce/src/pages/Cart/api/CartApi.ts, worker1.5/crons/toga2/compass/import_locations.php, _underscore/Model/Compass/SalesOrder.php, api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-06 - RemoveNonNumericCostCenters.sql |
|
|
8
9
|
| [Compass isFulfillable — Data Quality & the Type-Derived Rule](features/isfulfillable-data-quality-and-type-rule.md) | 2.0 | A **prod-data investigation (2026-08-12, read-only)** into why so many Compass storefront lines still have a dead **Qty Fulfilled** cell. | _underscore/Model/Client/Item.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php, dbchanges2/Core/2026-07-17 - RegisterItemIsFulfillableInterceptors.sql, toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx |
|
|
9
10
|
| [Compass Item Catalogs (Compass vs Agilant) & the Duplicate Items the NetSuite Sync Creates](features/item-catalogs-and-duplicate-items.md) | 1.0 | `Client_Compass.Items` is **split across catalogs by which pipeline created the item**, and the two halves are routinely mistaken for one. | library/app/api/toga2.php, _underscore/Model/Compass/PurchaseOrder.php, _underscore/Model/Compass/Usa/PurchaseOrder.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php |
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compass Contact Email Resolution & Crossed `primaryContactEmailAddressId` Pointers
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: compass-usa
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Compass/PurchaseOrder.php
|
|
13
|
+
- worker/crons/toga2/compass/update_salesorder_status_from_odp.php
|
|
14
|
+
related:
|
|
15
|
+
- ./mits-po-to-so-item-linking.md
|
|
16
|
+
- ./people-file-user-lifecycle.md
|
|
17
|
+
- ../profile.md
|
|
18
|
+
- ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
**Any Compass code that reads a contact's email address must also assert that the email row
|
|
24
|
+
belongs to that contact.** Verified in prod on 2026-09-03: **42,926 of 313,660** Compass contacts
|
|
25
|
+
(**~14%**) have `Contacts.primaryContactEmailAddressId` pointing at a `ContactEmailAddresses` row
|
|
26
|
+
whose `contactId` is a **different** contact. Read the pointer without that guard and you mail the
|
|
27
|
+
wrong human.
|
|
28
|
+
|
|
29
|
+
A second, related defect sits one level up: on **MR** orders the sales order itself is sometimes
|
|
30
|
+
attached to the **wrong contact**, while the MITS payload named the right person all along. So when
|
|
31
|
+
a Compass contact email "looks wrong" on an MR order, the usual truth is *the email is correct and
|
|
32
|
+
the SO-to-contact link is broken* — check the link before you touch any email data.
|
|
33
|
+
|
|
34
|
+
## The correct way to read a Compass contact's email
|
|
35
|
+
|
|
36
|
+
```sql
|
|
37
|
+
INNER JOIN Contacts
|
|
38
|
+
ON Contacts.id = <someRecord>.contactId
|
|
39
|
+
LEFT OUTER JOIN ContactEmailAddresses
|
|
40
|
+
ON ContactEmailAddresses.id = Contacts.primaryContactEmailAddressId
|
|
41
|
+
AND ContactEmailAddresses.contactId = Contacts.id # <- the ownership guard
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The `AND ContactEmailAddresses.contactId = Contacts.id` predicate is the whole point. Without it
|
|
45
|
+
the join happily follows a pointer into another contact's mailbox. First production consumer to
|
|
46
|
+
carry the guard: the Compass USA in-transit email cron
|
|
47
|
+
(`worker/crons/toga2/compass/update_salesorder_status_from_odp.php`) — see
|
|
48
|
+
[Compass partial in-transit & delivered emails](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
|
|
49
|
+
|
|
50
|
+
Note the guard makes the join *fail closed*: a crossed contact resolves to **no** address rather
|
|
51
|
+
than to a wrong one. That is the desired trade — a missing email is a visible gap, a wrong email is
|
|
52
|
+
a customer-facing incident.
|
|
53
|
+
|
|
54
|
+
## Why the pointers are crossed
|
|
55
|
+
|
|
56
|
+
The Compass tenant carries a large population of **duplicate contacts** created by the MITS
|
|
57
|
+
purchase-order import (e.g. **"Ben Berdan" exists 4 times**). Duplicates are created in adjacent id
|
|
58
|
+
ranges, and the pointer/owner pairing drifts between them.
|
|
59
|
+
|
|
60
|
+
The mechanism is visible in `_Model_Compass_PurchaseOrder::prePost()` (`handleMrOrder` path,
|
|
61
|
+
`_underscore/Model/Compass/PurchaseOrder.php` ~lines 72–105). Before creating the SO it resolves the
|
|
62
|
+
contact like this:
|
|
63
|
+
|
|
64
|
+
```php
|
|
65
|
+
$api->internalApiRequest('GET', '/contacts', null, [
|
|
66
|
+
'join' => [['ContactEmailAddresses' => ['ContactEmailAddresses.id' => 'Contacts.primaryContactEmailAddressId']]],
|
|
67
|
+
'where' => ['and' => [['ContactEmailAddresses.emailAddress' => ['=' => $contactEmail]]]],
|
|
68
|
+
'recordsPerPage' => 1,
|
|
69
|
+
]);
|
|
70
|
+
// no match -> POST /contacts creates a brand-new contact
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Three properties of that lookup matter:
|
|
74
|
+
|
|
75
|
+
1. **No ownership guard** — it joins on `ContactEmailAddresses.id = Contacts.primaryContactEmailAddressId`
|
|
76
|
+
only, so a contact that *points at* someone else's email row is a valid match for that email.
|
|
77
|
+
2. **`recordsPerPage 1` with no `ORDER BY`** — when several contacts match, which one comes back is
|
|
78
|
+
arbitrary and can change between calls.
|
|
79
|
+
3. **Miss → create** — a lookup that fails to see the existing person creates yet another duplicate,
|
|
80
|
+
which feeds the next round.
|
|
81
|
+
|
|
82
|
+
## Consequence: MR sales orders linked to the wrong contact
|
|
83
|
+
|
|
84
|
+
The MITS purchase-order payload names the real person in `contact._name` and
|
|
85
|
+
`contact.primaryContactEmailAddress.emailAddress`, but the SO ends up attached to a **neighbouring
|
|
86
|
+
contact id**. Confirmed cases (prod, 2026-09-03):
|
|
87
|
+
|
|
88
|
+
| MR order | SO is linked to | MITS payload actually named |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| MR245526 | contact **205042** "Luis Cervantes" | "Rusty Evans" / `Rusty.Evans@compass-usa.com` = contact **205041** |
|
|
91
|
+
| MR245513 | Eric Campbell | Benjamin Berdan |
|
|
92
|
+
| MR245463, MR244893, MR244699, MR245221 | same shape | — |
|
|
93
|
+
|
|
94
|
+
Note the adjacent ids (205041 / 205042) — the hallmark of duplicates created together by the import.
|
|
95
|
+
|
|
96
|
+
**Triage rule: the payload is the source of truth.** `Logs_Compass.Api` holds the inbound MITS
|
|
97
|
+
`POST /v2/purchase-orders` body; compare `contact.primaryContactEmailAddress.emailAddress` there
|
|
98
|
+
against the SO's linked contact before concluding the email data is wrong.
|
|
99
|
+
|
|
100
|
+
**10 such orders were deliberately left unfixed pending business input** — repointing a shipped SO
|
|
101
|
+
to a different person is a business decision, not a data cleanup.
|
|
102
|
+
|
|
103
|
+
## The 2026-09-03 data fix (32 safe MR cases)
|
|
104
|
+
|
|
105
|
+
`compass_mr_contact_email_ownership_2026-09-03.sql` — run in prod, all 32 verified. The safe repair
|
|
106
|
+
shape, for the cases where the crossed contact and the owner hold the **same address**:
|
|
107
|
+
|
|
108
|
+
1. Insert a `ContactEmailAddresses` row **owned by the crossed contact**, carrying the same
|
|
109
|
+
`emailAddress`.
|
|
110
|
+
2. Repoint that contact's `Contacts.primaryContactEmailAddressId` at its own new row.
|
|
111
|
+
|
|
112
|
+
Nobody's address changes; each contact simply stops borrowing another contact's row. This is only
|
|
113
|
+
valid when the two addresses already match — a differing address is a real identity question and
|
|
114
|
+
must not be auto-resolved.
|
|
115
|
+
|
|
116
|
+
The remaining ~42,900 crossed contacts were **not** mass-repaired. Consumers carry the ownership
|
|
117
|
+
guard instead, which is safe today; a bulk cleanup is still open work.
|
|
118
|
+
|
|
119
|
+
## Gotchas / known issues
|
|
120
|
+
|
|
121
|
+
- **⚠ The pointer is crossed on ~14% of Compass contacts — treat an unguarded read as a bug.**
|
|
122
|
+
This applies to reports, exports, notification crons, api2 payload interceptors, anything.
|
|
123
|
+
- **A `Users` row is a different lookup and does not have this problem.** `Users.email` is keyed by
|
|
124
|
+
`Users.contactId` directly. Where both exist, prefer `Users.email` and fall back to the guarded
|
|
125
|
+
`ContactEmailAddresses` read — that is the `IFNULL(Users.email, ContactEmailAddresses.emailAddress)`
|
|
126
|
+
precedence used by the in-transit cron. EDI-created MR orders have **no** `Users` row at all,
|
|
127
|
+
which is why the fallback exists.
|
|
128
|
+
- **`_Model_Compass_PurchaseOrder`'s contact lookup still has no ownership guard** (as of
|
|
129
|
+
2026-09-04). Fixing it there stops *new* crossings; the existing 42,926 remain either way.
|
|
130
|
+
- **Compass Canada shares the model** (`Model/Compass/Canada/PurchaseOrder.php` inherits the parent),
|
|
131
|
+
so the same lookup shape runs against `Client_CompassCanada`. Not measured there.
|
|
132
|
+
- Duplicate-contact creation and duplicate-**user** creation are separate problems with separate
|
|
133
|
+
causes — the `Users` duplicates come from the PEOPLE-file reactivation window, see
|
|
134
|
+
[PEOPLE-file user lifecycle](./people-file-user-lifecycle.md).
|
|
135
|
+
|
|
136
|
+
## Change history
|
|
137
|
+
|
|
138
|
+
- 2026-09-04 — Documented that **42,926 / 313,660 (~14%) Compass contacts have
|
|
139
|
+
`Contacts.primaryContactEmailAddressId` pointing at another contact's `ContactEmailAddresses`
|
|
140
|
+
row** (verified prod 2026-09-03), and established the mandatory
|
|
141
|
+
`ContactEmailAddresses.contactId = Contacts.id` ownership guard for every email read. Traced the
|
|
142
|
+
cause to `_Model_Compass_PurchaseOrder`'s MR contact lookup (joins on the pointer only, no
|
|
143
|
+
ownership guard, `recordsPerPage 1` with no `ORDER BY`, miss creates a duplicate) and to the
|
|
144
|
+
resulting duplicate contacts from the MITS PO import. Recorded the related defect that **MR sales
|
|
145
|
+
orders can be linked to the wrong contact** while the MITS payload named the right person
|
|
146
|
+
(MR245526, MR245513, MR245463, MR244893, MR244699, MR245221), with the triage rule to trust the
|
|
147
|
+
logged payload. Fixed 32 safe MR cases via
|
|
148
|
+
`compass_mr_contact_email_ownership_2026-09-03.sql` (give each contact its own email row with the
|
|
149
|
+
same address, then repoint); 10 ambiguous orders left for business input. (bala)
|
|
@@ -204,7 +204,7 @@ Re-confirmed 2026-08-27: **no cron, email or report reads the distinction.**
|
|
|
204
204
|
behaviour.
|
|
205
205
|
- **In-transit / delivered email crons are unaffected.**
|
|
206
206
|
`worker/crons/toga2/compass/update_salesorder_status_from_odp.php`,
|
|
207
|
-
`worker/crons/toga2/compasscanada/
|
|
207
|
+
`worker/crons/toga2/compasscanada/update_salesorder_status_from_g&t.php` and
|
|
208
208
|
`compass/workflow/test_partial_in_transit_email.php` each keep their **own duplicated copy** of the
|
|
209
209
|
`_status` CASE, but those copies only ever produce canceled / pendingApprovalUnknown /
|
|
210
210
|
pendingFulfillment / shipped and they filter on `computedStatusSlug = 'shipped'`. **That
|
package/package.json
CHANGED