toga-ai 1.0.817 → 1.0.819

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.
@@ -76,6 +76,23 @@ This mirrors the overlap the 2.0 `saml` gateway provides, though the gateway now
76
76
  a multi-cert list out of Secrets Manager rather than two files on disk — see the SP
77
77
  cert-rotation workflow.
78
78
 
79
+ ### Two ACS handlers, two SAML libraries — tell them apart from the stack trace
80
+
81
+ `store/_/app/framework.php` has **two** separate `case 'acs'` handlers that use **different SAML
82
+ libraries**. They are easy to confuse when reading a production stack trace, and they run on
83
+ **different servers**:
84
+
85
+ | Handler | Library | Host / EB environment |
86
+ |---|---|---|
87
+ | The `togahub` host block's `case 'acs'` (the `$samlKeyPairs` dual-keypair decrypt) | **LightSAML** (`decryptMultiAssertion`) | `prudential.togahub.com` → `prudential-store-prod` |
88
+ | The `prudential` host block's `case 'acs'` | **OneLogin_Saml2_Auth** (`isAuthenticated()`, `getNameId()`, `getAttribute('givenname'/'surname'/'emailaddress')`) | `prudential.togacommerce.com` → `true-togacommerce-prod` |
89
+
90
+ **Diagnostic rule:** a trace with `LightSaml\Model\Assertion\*` frames is the **togahub** block
91
+ (`prudential-store-prod`). A trace with `OneLogin` frames is the **prudential** block
92
+ (`true-togacommerce-prod`). This is how a production decrypt error gets pinned to the right
93
+ server — and the two are deployed by different pipeline actions, so fixing the wrong one changes
94
+ nothing. See the Prudential store SAML deploy-topology workflow.
95
+
79
96
  ### Private key loading
80
97
 
81
98
  The three inline `$settingsInfo` SP blocks read the key from disk:
@@ -108,6 +125,23 @@ key; it is a record of what we do, not a recommendation.
108
125
  - **Don't assume where the ACS runs.** The host that terminates `?acs` for a client may be a
109
126
  different EB environment from the storefront. See the Prudential store SAML deploy-topology
110
127
  workflow before deploying a cert change.
128
+ - **A decrypt stack trace CANNOT tell you how many keys were tried.** In
129
+ `library/LightSaml/Model/Assertion/EncryptedElementReader.php`, `decryptMulti()` returns
130
+ `$this->decrypt($key)` from *inside* the `foreach`, and re-throws `$lastException` after the
131
+ loop. The re-thrown exception keeps the stack from where it was first thrown, so the captured
132
+ trace looks **identical** whether one key was tried or several all failed. Seeing
133
+ `decryptMultiAssertion` in a trace therefore does **not** prove the dual-keypair code is
134
+ deployed — the old single-key code called the same method with a one-item array. To tell them
135
+ apart, check the deployed EB `Deployment.VersionLabel`, or look for the new code's distinct log
136
+ line `SAML keypair not readable, skipping:`.
137
+ - **A mismatched cert/key pair fails with an OAEP padding error, not a clear message.** The
138
+ symptom is `Failure decrypting Data (openssl private) -
139
+ RSA_padding_check_PKCS1_OAEP_mgf1: oaep decoding error` (also seen as
140
+ `RSA_EAY_PRIVATE_DECRYPT: padding check failed`) thrown out of the LightSAML
141
+ `EncryptedElementReader`. Confirm a suspected mismatch by comparing the **modulus** of the cert
142
+ and the key with `openssl x509 -noout -modulus` / `openssl rsa -noout -modulus` — identical
143
+ modulus means the pair matches. Compare the **committed git blobs**, not just the local working
144
+ files, or you will verify a pair that was never deployed.
111
145
 
112
146
  ## Client_True migration (deferred 2026-09-15)
113
147
 
@@ -136,6 +170,14 @@ What the migration must handle:
136
170
  architecture doc's Known issues.
137
171
 
138
172
  ## Change history
173
+ - 2026-09-15 — Documented that `framework.php` has two `case 'acs'` handlers on two different
174
+ SAML libraries (LightSAML for `togahub` vs OneLogin for `prudential`) running on two different
175
+ EB environments, and how to read a stack trace to tell which one failed. Recorded that a
176
+ LightSAML `decryptMulti()` trace cannot reveal how many keys were tried (re-thrown exception
177
+ keeps the original stack), so `decryptMultiAssertion` in a trace does not prove the
178
+ dual-keypair build is deployed — check the EB VersionLabel or the
179
+ `SAML keypair not readable, skipping:` log line. Added the OAEP-padding-error signature of a
180
+ mismatched cert/key pair and the openssl-modulus check against committed blobs. (rgirish)
139
181
  - 2026-09-15 — Rotated the `togahub.com` SP cert to the ACM-issued `togahub_com_2027-03-31.crt`
140
182
  and added a dual-keypair fallback in `?acs` so both the old and new certs decrypt during the
141
183
  overlap (`decryptMultiAssertion`). Removed three hardcoded RSA private-key blobs from
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-10
9
+ updated: 2026-09-16
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/update_salesorder_status_from_odp.php
@@ -18,8 +18,10 @@ files:
18
18
  - library/app/client/compasscanada.php
19
19
  - worker/schedules/cron.worker.sync.json
20
20
  - worker2/Worker/Monitor/Compass.php
21
+ - worker/crons/toga2/compass/workflow/3b_import_strategic_systems_advance_shipping_notices.php
21
22
  - dbchanges2/Client_Compass/2026-09-03 - compass_intransit_email_backlog_backfill.sql
22
23
  related:
24
+ - ../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md
23
25
  - ../../../clients/compass-usa/features/contact-email-resolution.md
24
26
  - ../../../2.0/apps/worker2/features/tracking-status-refresh.md
25
27
  - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
@@ -56,9 +58,11 @@ and earlier packages). The dynamic HTML is injected into a stored `EmailTemplate
56
58
  ## How it works
57
59
 
58
60
  1. **Driving query** finds shipped sales orders and their tracking numbers (`computedStatusSlug
59
- = 'shipped'`, `c_dtInTransitEmailSent IS NULL`, rolling date window) via the ASN chain
61
+ = 'shipped'`, `c_dtInTransitEmailSent IS NULL`, rolling date window on
62
+ **`AdvanceShippingNotices.dtCreated`**) via the ASN chain
60
63
  (`AdvanceShippingNoticeItemUnits_TrackingNumbers`), one row per `TrackingNumbers.number`,
61
- **newest first and capped** — see *Draining the candidate set* below.
64
+ **newest first and capped** — see *Draining the candidate set* and *Which clock the window
65
+ runs on* below.
62
66
  2. Per tracking number: skip if the shipment is already finished — checked first against the
63
67
  stored `TrackingNumbers.status` (free), and only then with a live carrier call.
64
68
  3. **`getOrderFulfillmentData`** returns the order's packages (grouped by tracking number,
@@ -73,6 +77,13 @@ and earlier packages). The dynamic HTML is injected into a stored `EmailTemplate
73
77
  `App_Api_Toga2::send(..., 'POST', '/email-templates/sendEmail', $payload, [], true)`.
74
78
  6. On a successful (non-throwing) send, set `c_dtInTransitEmailSent = NOW()`.
75
79
 
80
+ **The two loops behave differently, on purpose.** Loop 1 stamps `c_dtInTransitEmailSent` on any
81
+ row whose `TrackingNumbers.status` is in `TRACKING_FINAL_STATUSES` (`DELIVERED`,
82
+ `RETURNED_TO_SENDER`) and **sends nothing**. Loop 2 emails the rest and stamps after a successful
83
+ send. So a parcel that was already delivered by the time the cron reaches it is **silently
84
+ retired, never announced** — expected behaviour, and the reason a genuine gap can show up as
85
+ "22 rows missed, 1 email actually owed".
86
+
76
87
  ### Draining the candidate set: two-pass, newest-first, bounded
77
88
 
78
89
  **This is the load-bearing part of the cron. Getting it wrong took Compass USA in-transit emails to
@@ -95,9 +106,10 @@ mark the row done"* cron:
95
106
  2. **Newest first, hard cap.** `ORDER BY MAX(TrackingNumbers.dtCreated) DESC` +
96
107
  `LIMIT IN_TRANSIT_EMAIL_BATCH_SIZE` (**400**). Today's shipments are served before any backlog,
97
108
  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.
109
+ 3. **Rolling window, not a fixed date.** `IN_TRANSIT_EMAIL_LOOKBACK_DAYS` →
110
+ `dtCreated >= DATE_SUB(NOW(), INTERVAL N DAY)`. A hardcoded cutoff date guarantees an
111
+ ever-growing scan; a rolling window is self-limiting. **Which `dtCreated` matters enormously —
112
+ see the next section.** (USA is at 7 days as of 2026-09-15; Canada is still at 14.)
101
113
  4. **Trust the stored status before paying for a carrier call.** `isTrackingStatusFinal(?string)`
102
114
  short-circuits when `TrackingNumbers.status` is already `DELIVERED` or `RETURNED_TO_SENDER`
103
115
  (`TRACKING_FINAL_STATUSES`). worker2's
@@ -105,6 +117,37 @@ mark the row done"* cron:
105
117
  keeps that column current for every client, so this 1.0 cron should not re-ask the carrier about
106
118
  a finished shipment.
107
119
 
120
+ ### Which clock the window runs on: `AdvanceShippingNotices.dtCreated`, never `TrackingNumbers.dtCreated`
121
+
122
+ **This is the single easiest way to break Compass shipping emails, and it has now broken them
123
+ twice.** For Compass a `TrackingNumbers` row is created by the **NetSuite invoice sync**, not by
124
+ the ASN. The ASN shows up days or weeks later and is the only thing that links the tracking number
125
+ to an order and therefore to a recipient. So `TrackingNumbers.dtCreated` is the **invoice** date,
126
+ and a lookback anchored on it measures the wrong thing entirely. Full derivation:
127
+ [where a Compass TrackingNumbers row comes from](../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md).
128
+
129
+ - **Window (this cron):** filter on `AdvanceShippingNotices.dtCreated` — "how long has this been
130
+ *sendable*". Fixed 2026-09-16 at `update_salesorder_status_from_odp.php` line ~432;
131
+ `IN_TRANSIT_EMAIL_LOOKBACK_DAYS` stays at **7**, and the comment above the constant now says why.
132
+ - **Age / alarm (the monitor):** use `GREATEST(TrackingNumbers.dtCreated,
133
+ AdvanceShippingNotices.dtCreated)`.
134
+
135
+ **What went wrong.** On 2026-09-15 11:35 commit `81824325` ("Changing in transit email limit to 7")
136
+ cut the lookback from 14 to 7 days. Because the window was anchored on the invoice date,
137
+ **22 shipments** invoiced 2026-09-03 11:17–11:38 whose ASNs only landed 2026-09-15 16:00–17:00 fell
138
+ outside the 7-day window and were never emailed: SA136625, SA136627, SA136643, SA136673, SA136680,
139
+ SA136694, MR243688, MR244839, MR244893, MR244904, MR245219, MR245303, MR245337, MR245404, MR245409,
140
+ MR245420, MR245424, MR245429, MR245441, MR245454, MR245462, MR245463. Since 2026-08-01, **53**
141
+ tracking rows had their ASN land more than 7 days after the tracking row (max gap **13 days**), so
142
+ the 14-day window had been masking the same design flaw all along.
143
+
144
+ **Verified on prod before shipping:** the ASN-anchored window returns the same 22 rows, drops **0**
145
+ rows the old window covered, and yields exactly **1** real catch-up email — MR245462 (tracking
146
+ `1Z8696XA0392020614`, still `IN_TRANSIT`). The other 21 already read `DELIVERED`, so loop 1 stamps
147
+ them and sends nothing.
148
+
149
+ **Status: edited in the working tree, not yet committed or deployed (2026-09-16).**
150
+
108
151
  ### Resolving the recipient: a `Users` row is NOT required (USA)
109
152
 
110
153
  The To address originally came only from `Users.email`, via `INNER JOIN Users ON Users.contactId =
@@ -227,14 +270,30 @@ the tracking number as emailed (it retries next run).
227
270
  (e.g. `update_salesorder_status_from_gt.php`) with `schedules/cron.worker.sync.json` repointed.**
228
271
  Never put `&` (or any shell metacharacter) in a cron filename. See
229
272
  [tracing a 1.0 worker cron run](../workflows/tracing-a-worker-cron-run-in-production.md).
230
- - **⚠ Invoice tracking numbers are NOT in scope and must never be counted as in-transit
231
- backlog.** `POST /v2/invoice-tracking-numbers` creates `TrackingNumbers` rows bridged through the
232
- **`InvoiceTrackingNumbers`** table — never the ASN chain. On 2026-09-03 alone, 98 were created
233
- (11:15–11:40) and **zero** of them touch `AdvanceShippingNoticeItemUnits_TrackingNumbers`.
234
- Anything that walks the ASN unit bridge (this cron, and the OneUptime queue monitor) correctly
235
- ignores them. Their shape is distinctive: **`shippingCarrierId` set, `shippingMethodId` NULL**.
236
- A naive `TrackingNumbers WHERE c_dtInTransitEmailSent IS NULL` count will read these as a backlog
237
- that never drains.
273
+ - **⚠ CORRECTED 2026-09-16 — invoice tracking numbers ARE in scope; they are the normal origin.**
274
+ This doc previously said `POST /v2/invoice-tracking-numbers` rows are bridged only through
275
+ `InvoiceTrackingNumbers` and "never the ASN chain". **That was wrong.** For Compass the invoice
276
+ sync is what *creates* nearly every `TrackingNumbers` row (98 POSTs / 100 rows in the 2026-09-03
277
+ 11:15–11:40 window, against 4 ASNs created all day); the ASN attaches to those same rows days or
278
+ weeks later. What survives is the narrow version: a tracking number that **never** receives an
279
+ ASN can never be emailed, so a naive `TrackingNumbers WHERE c_dtInTransitEmailSent IS NULL` count
280
+ with no ASN join reads as a backlog that never drains. And because the row is born at invoice
281
+ time, `TrackingNumbers.dtCreated` must never be used as the window clock — see
282
+ [tracking-number origin](../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md).
283
+
284
+ - **⚠ Do NOT make `3b_import_strategic_systems_advance_shipping_notices.php` email for
285
+ pre-existing tracking numbers — tried 2026-09-16 and REVERTED.** 3b gates its in-transit email on
286
+ `$isTrackingNumberInserted` (line ~722 in `processOfficeDepotFile`, line ~1431 in
287
+ `processStrategicSystems`): it only emails when that run INSERTs the tracking row. When the
288
+ invoice sync already created the number, 3b still builds the ASN link but sends nothing, which
289
+ looks like the bug. Changing the gate to "has this customer been told"
290
+ (`c_dtInTransitEmailSent IS NULL`) was implemented and then backed out, because **3b only ever
291
+ sends the plain in-transit template** `e936aa4b-d5b6-41dd-be1a-938b2b27af9e`, while this catch-up
292
+ cron chooses between the **partial** template `d9b3f2a7-1c84-4e6d-9f50-3a7c1e8b2d46` (split order
293
+ / items still being prepared) and the plain one. Firing 3b for pre-existing tracking numbers would
294
+ **silently downgrade every split order from the partial email to the plain email**. 3b was also
295
+ never the real break: the catch-up cron already covers that case inside its window, so fixing the
296
+ window was the correct and far smaller change.
238
297
  - **Clear an in-transit backlog by STAMPING, not by emailing — and refresh statuses first.**
239
298
  Decided 2026-09-03 while draining the outage backlog
240
299
  (`dbchanges2/Client_Compass/2026-09-03 - compass_intransit_email_backlog_backfill.sql`):
@@ -292,6 +351,20 @@ the tracking number as emailed (it retries next run).
292
351
  interceptor and retire this 1.0 cron.
293
352
 
294
353
  ## Change history
354
+ - 2026-09-16 — **Re-anchored the in-transit window on `AdvanceShippingNotices.dtCreated`** (was
355
+ `TrackingNumbers.dtCreated`, `update_salesorder_status_from_odp.php` line ~432);
356
+ `IN_TRANSIT_EMAIL_LOOKBACK_DAYS` stays 7 with a comment explaining why. Cause: commit `81824325`
357
+ (2026-09-15 11:35) cut the lookback 14 → 7 while the window still measured the **invoice** date,
358
+ so 22 shipments invoiced 2026-09-03 whose ASNs landed 2026-09-15 fell out of the window (only
359
+ MR245462 still owed a real email; the other 21 read DELIVERED). Verified on prod: 22 rows in,
360
+ 0 rows lost, 1 catch-up email. **Corrected the old "invoice tracking numbers never touch the ASN
361
+ chain" gotcha** — the invoice sync is in fact the normal origin of Compass `TrackingNumbers` rows;
362
+ new doc
363
+ [tracking-number origin and the ASN link](../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md).
364
+ Recorded the **rejected** alternative of making 3b email for pre-existing tracking numbers (it
365
+ would downgrade split orders from the partial template to the plain one). Not yet committed or
366
+ deployed. (bala)
367
+
295
368
  - 2026-09-10 — Recorded that a zero-line-item ASN is invisible to this cron (candidates come from
296
369
  `AdvanceShippingNoticeItemUnits(_TrackingNumbers)`), the failure mode Compass Canada's G&T
297
370
  importer produced until 2026-09-08. (bala)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-08
9
+ updated: 2026-09-16
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
+ - ../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md
26
27
  - ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
27
28
  - ./monitoring-framework.md
28
29
  - ./all-client-email-queue-monitor.md
@@ -110,7 +111,9 @@ Each measures one Compass/Office Depot integration metric and pushes a decided a
110
111
  `s3://agilant-as2/OfficeDepot/OUTBOX/` (skips subfolders + directory placeholders).
111
112
  - **InTransitEmailQueue** (DB, added 2026-09-04) — watches the Compass USA **in-transit
112
113
  shipping-notification queue**: `TrackingNumbers` rows on the ASN unit bridge with
113
- `c_dtInTransitEmailSent IS NULL` inside a 14-day window (the window the 1.0 sending cron uses).
114
+ `c_dtInTransitEmailSent IS NULL` inside a **7-day window on `AdvanceShippingNotices.dtCreated`**
115
+ (exactly the window the 1.0 sending cron uses — see *Age the backlog on the clock the work
116
+ actually starts on* below).
114
117
  Status-page name *"Shipping Notification Emails (USA, In Transit)"*. See
115
118
  [why it exists](#watch-the-step-before-the-log-row-exists) below.
116
119
  - **MaRefreshOrderExceptions** (DB, added 2026-09-08) — Compass **`MA%` employee-refresh**
@@ -244,6 +247,36 @@ deliberately does not drive the alarm** — otherwise the monitor would sit red
244
247
  cron structurally cannot send. That is the *"backlog monitors must exclude known-permanently-stranded
245
248
  rows"* rule.
246
249
 
250
+ ### Age the backlog on the clock the work actually starts on
251
+
252
+ `InTransitEmailQueue` originally did both of these off `TrackingNumbers.dtCreated`, and both were
253
+ wrong for Compass, because a Compass `TrackingNumbers` row is created by the **NetSuite invoice
254
+ sync** and only becomes sendable when the ASN lands days or weeks later
255
+ ([derivation](../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md)).
256
+ Three corrections (a. committed as `82d614c` and deployed; b. and c. in the working tree, not yet
257
+ deployed as of 2026-09-16):
258
+
259
+ a. **`oldestQueuedHours` / `lateRows` now age from
260
+ `GREATEST(TrackingNumbers.dtCreated, AdvanceShippingNotices.dtCreated)`** — "how long has this
261
+ been *sendable*", not "how long since we invoiced it".
262
+ b. **`$LOOKBACK_DAYS` 14 → 7**, to match the cron's `IN_TRANSIT_EMAIL_LOOKBACK_DAYS`.
263
+ c. **The WHERE lookback filters on `AdvanceShippingNotices.dtCreated`**, so the monitor counts
264
+ exactly the set the cron will work on.
265
+
266
+ **Why it matters — the monitor cried wolf, then muddied a real incident.** Two HIGH alarms fired on
267
+ 2026-09-15 (15:00:46 and 15:35:46) reporting "oldest 90h / 91h" for rows that had been sendable for
268
+ *minutes*. Replaying both moments with the `GREATEST(...)` expression gives **1h** and **0h** with
269
+ **0** late rows, i.e. OK. A third alarm on 2026-09-16 08:55 was a **real** gap, and the 14-vs-7 day
270
+ mismatch made it confusing: the monitor was counting 22 rows the cron would never look at.
271
+
272
+ **Rule: a backlog monitor must age and window on the same clock, and the same column, as the tier
273
+ that does the work.** Any drift between the two turns the monitor into noise in both directions —
274
+ false alarms on rows that are fine, and silence on rows the cron cannot see.
275
+
276
+ Rows with no `Users` row and no `ContactEmailAddresses` fallback (typically EDI-created MR orders)
277
+ can never be emailed at all; the monitor reports those as `unroutableRows` and deliberately never
278
+ alarms on them.
279
+
247
280
  > **⚠ Known divergence (2026-09-04):** the monitor decides routable/unroutable from the
248
281
  > `Users` join **only**, while the sending cron now also falls back to
249
282
  > `Contacts` / `ContactEmailAddresses`. So some rows counted as `unroutableRows` are in fact
@@ -543,6 +576,15 @@ check for another client.
543
576
  monitors must pass the region (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
544
577
 
545
578
  ## Change history
579
+ - 2026-09-16 — **`InTransitEmailQueue` re-anchored onto the sendable clock.** `oldestQueuedHours` /
580
+ `lateRows` now age from `GREATEST(TrackingNumbers.dtCreated, AdvanceShippingNotices.dtCreated)`
581
+ (committed `82d614c`, deployed); `$LOOKBACK_DAYS` 14 → 7 and the WHERE lookback moved onto
582
+ `AdvanceShippingNotices.dtCreated` to match the 1.0 cron exactly (working tree, not deployed).
583
+ Cause: a Compass `TrackingNumbers` row is born from the NetSuite invoice sync, so the old age
584
+ measured invoice age — two false HIGH alarms on 2026-09-15 ("oldest 90h / 91h") replay as 1h / 0h
585
+ with 0 late rows. Added the general rule that a backlog monitor must age and window on the same
586
+ clock and column as the tier that does the work. (bala)
587
+
546
588
  - 2026-09-08 — Added an **11th monitor, `MaRefreshOrderExceptions`** (Compass `MA%`
547
589
  employee-refresh orders with no NetSuite order; `Core.CronJobs` id 74; DEGRADED >10 waiting /
548
590
  >32h, HIGH >20 / >72h) — full derivation in
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 26 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 35 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 36 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)
11
11
  - **togadesk** (TOGa Desk) — 13 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -23,6 +23,7 @@
23
23
  | [Persona Model & Levy-Sector Gating (worker2 PEOPLE cron)](features/persona-model-and-levy-gating.md) | 2.0 | Compass USA catalogue visibility is driven by **personas** in `Client_Compass`. |
24
24
  | [Compass Sales-Order Line-Number Renumbering (and why it hangs off the SO hooks)](features/sales-order-line-renumbering.md) | 2.0 | Compass sales-order lines must stay numbered **1..N with no gaps** after any add, edit, or delete — downstream MITS/PO linking reads `lineNumber` as an identity |
25
25
  | [Stranded Approval Reassignment (repointing approvals off dead duplicate Compass Users rows)](features/stranded-approval-reassignment.md) | 2.0 | A Compass employee who leaves and comes back after more than `CONTACT_UNLINK_GRACE_DAYS` (5) is **inserted as a brand-new `Users` row** by the PEOPLE importer i |
26
+ | [Compass: where a `TrackingNumbers` row comes from (invoice sync, NOT the ASN) and how it reaches a customer](features/tracking-number-origin-and-asn-link.md) | 1.0 | For Compass USA a `TrackingNumbers` row is **born from the NetSuite invoice sync**, not from the ASN. |
26
27
  | [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. |
27
28
  | [Compass USA — reversing the 199-item catalog fold (MITS outage recovery)](workflows/catalog-fold-reversal.md) | 2.0 | The **root cause** of the Compass MITS transmission outage (the `assetType`-NULL and duplicate-vendor symptoms) was an ad-hoc cross-client **duplicate-items mer |
28
29
  | [Changing what is IN a Compass kit (adding / swapping a BundleItems line)](workflows/changing-a-kit-line-item.md) | 2.0 | "Add this fee/component to every kit that has X" is a recurring Compass request, and it looks like one `INSERT` into `Client_Compass.BundleItems`. |
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Compass: where a `TrackingNumbers` row comes from (invoice sync, NOT the ASN) and how it reaches a customer
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: compass-usa
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-09-16
10
+ owners: ["bala"]
11
+ files:
12
+ - library/app/api/toga2.php
13
+ - worker/crons/toga2/netsuite/common_sync_togasupply.php
14
+ - worker/crons/toga2/netsuite/sync_togasupply_compass_usa.php
15
+ - worker/crons/toga2/compass/update_salesorder_status_from_odp.php
16
+ - worker2/Worker/Monitor/Compass.php
17
+ related:
18
+ - ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
19
+ - ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
20
+ - ./asn-to-item-fulfillment.md
21
+ - ./contact-email-resolution.md
22
+ - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
23
+ ---
24
+
25
+ ## Summary
26
+
27
+ For Compass USA a `TrackingNumbers` row is **born from the NetSuite invoice sync**, not from the
28
+ ASN. The ASN turns up days or weeks later and is the **only** thing that ties that tracking number
29
+ to a sales order, and therefore to a customer's email address.
30
+
31
+ The consequence, and the reason this doc exists: **`TrackingNumbers.dtCreated` is the invoice
32
+ date.** It is the wrong clock to time any shipping notification, backlog age, or SLA from. Anything
33
+ that asks *"when did we get this tracking number?"* and means *"when could we first have told the
34
+ customer?"* is wrong for Compass by up to two weeks.
35
+
36
+ ## Key files / entry points
37
+
38
+ - `worker/crons/toga2/netsuite/sync_togasupply_compass_usa.php` → `common_sync_togasupply.php` →
39
+ `App_Api_Toga2::syncInvoiceFromNetsuite()` (`library/app/api/toga2.php`, function opens at
40
+ line 3203; the tracking-number POST is at line 3662).
41
+ - The POST is `POST /v2/invoice-tracking-numbers`, payload shaped as a nested object:
42
+ `{"invoice":{"uuid":"…"},"trackingNumber":{"number":"…","shippingCarrier":{…}}}`. api2 creates
43
+ the `TrackingNumbers` row **from that nested object** — so the row exists long before any ASN.
44
+ - `worker/crons/toga2/compass/update_salesorder_status_from_odp.php` — the consumer that must use
45
+ the ASN clock, not this one.
46
+ - `worker2/Worker/Monitor/Compass.php` → `InTransitEmailQueue` — the monitor over the same queue.
47
+
48
+ ## How it works
49
+
50
+ 1. The invoice sync imports a NetSuite invoice and POSTs each of its tracking numbers. That
51
+ **creates** `TrackingNumbers`, stamping `dtCreated` = the moment of the invoice import.
52
+ 2. The invoice does **not** carry the order link. `Invoices.salesOrderId` is NULL on all of the
53
+ affected invoices, and across Compass since 2026-08-01 it is NULL on **1,868 of 2,022**
54
+ invoices. So at this point the tracking number belongs to nobody.
55
+ 3. Later (hours to weeks) the ASN arrives, and **that** is what links the tracking number to a
56
+ customer. The only path is:
57
+
58
+ ```
59
+ TrackingNumbers
60
+ → AdvanceShippingNoticeItemUnits_TrackingNumbers
61
+ → AdvanceShippingNoticeItemUnits
62
+ → AdvanceShippingNoticeItems
63
+ → AdvanceShippingNotices.purchaseOrderId
64
+ → SalesOrders_PurchaseOrders
65
+ → SalesOrders
66
+ → Contacts / Users (the email address)
67
+ ```
68
+
69
+ 4. Only once that chain exists can the in-transit email be built or sent.
70
+
71
+ ### How big the gap is (measured on prod)
72
+
73
+ - **2026-09-03, 11:15:56–11:40:20** — `Logs_Compass.Api` shows **98** `POST /v2/invoice-tracking-numbers`
74
+ from the worker instances. **100** `TrackingNumbers` rows were created in that window, while only
75
+ **4** ASNs were created in the whole of 09-03.
76
+ - Since **2026-08-01**, **53** tracking rows had their ASN land **more than 7 days** after the
77
+ tracking row, with a **maximum gap of 13 days**.
78
+ - The concrete failure: 22 shipments invoiced 2026-09-03 11:17–11:38 whose ASNs only landed
79
+ 2026-09-15 16:00–17:00.
80
+
81
+ ## Data model
82
+
83
+ - `TrackingNumbers.dtCreated` — **invoice import time** for Compass. Not the ship time, not the
84
+ time the customer became notifiable.
85
+ - `AdvanceShippingNotices.dtCreated` — the first moment a tracking number is attached to an order.
86
+ **This is the notifiable-from clock.**
87
+ - `Invoices.salesOrderId` — usually NULL for Compass; do not rely on it to reach the order.
88
+ - `InvoiceTrackingNumbers` — the invoice-side bridge. A row can sit on this bridge *and* later on
89
+ the ASN unit bridge; the two are not mutually exclusive.
90
+
91
+ ## Gotchas / known issues
92
+
93
+ - **⚠ Never window, age, or alarm a Compass shipping feature on `TrackingNumbers.dtCreated`.** Use
94
+ `AdvanceShippingNotices.dtCreated` for a window, and `GREATEST(TrackingNumbers.dtCreated,
95
+ AdvanceShippingNotices.dtCreated)` when you need "how long has this been sendable". Getting this
96
+ wrong produced both a real missed-email gap and two false HIGH alarms — see
97
+ [Compass in-transit emails](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
98
+ - **⚠ Correction to an earlier belief.** It was previously recorded that invoice-created tracking
99
+ numbers "never touch the ASN chain" and are therefore out of scope for in-transit email. That is
100
+ wrong. They are the *normal* origin for Compass tracking numbers; the ASN chain simply attaches
101
+ to them later. What is true is the narrower statement: a tracking number that **never** gets an
102
+ ASN can never be emailed, and a naive `TrackingNumbers WHERE c_dtInTransitEmailSent IS NULL`
103
+ count (with no ASN join) reads those as a backlog that never drains.
104
+ - **A tracking number with no ASN has no customer.** There is no fallback route from invoice to
105
+ contact, because `Invoices.salesOrderId` is NULL. Waiting for the ASN is the design, not a bug.
106
+ - Credentials for the sync ride on `App_Api_Toga2` class constants (e.g.
107
+ `App_Api_Toga2::CLIENT_UUID_COMPASS`) — reference the constant, never the value.
108
+
109
+ ## Change history
110
+ - 2026-09-16 — First write-up. Established that Compass `TrackingNumbers` rows are created by the
111
+ NetSuite invoice sync (`syncInvoiceFromNetsuite` → `POST /v2/invoice-tracking-numbers`), that
112
+ `Invoices.salesOrderId` is NULL on 1,868 of 2,022 Compass invoices since 2026-08-01, and that the
113
+ ASN chain is the only route from a tracking number to a customer. Measured the invoice→ASN gap
114
+ (53 rows over 7 days since 2026-08-01, max 13 days) and corrected the earlier "invoice tracking
115
+ numbers never touch the ASN chain" claim. (bala)
@@ -23,6 +23,28 @@ opposite one from what you would guess. Read this before deploying anything SAML
23
23
  Prudential. It also records the EB deploy failure mode we hit on 2026-09-15 (a wedged command
24
24
  agent that times out after 14 minutes while the environment still shows Green).
25
25
 
26
+ ## The full login flow — the two legs run on two different servers
27
+
28
+ The outbound (first) leg does **not** hop through togahub. It redirects the browser straight to
29
+ Microsoft Entra:
30
+
31
+ 1. User hits `prudential.togacommerce.com` (**`true-togacommerce-prod`**). The `prudential`
32
+ host block in `store/_/app/framework.php` fires when the session host is `prudential`, the app
33
+ is not in test mode, and the session has no `accountId`.
34
+ 2. With no recognized `?ssoCommand`, it falls to the `default:` case, which calls
35
+ `$auth->login($originUrl)` on `OneLogin_Saml2_Auth`.
36
+ 3. That redirects to Prudential's Entra tenant:
37
+ `https://login.microsoftonline.com/d8fde2f5-9392-4260-8a03-0ad01f4746e9/saml2`.
38
+ 4. The SP block advertises entityId `https://prudential.togahub.com/?meta` and ACS
39
+ `https://prudential.togahub.com/?acs` — so **Entra POSTs the response to togahub**, i.e. the
40
+ inbound leg lands on **`prudential-store-prod`**, a different EB environment.
41
+ 5. `prudential-store-prod` decrypts the assertion (LightSAML), then redirects back to
42
+ `prudential.togacommerce.com/?hash=<base64 of xid:first:last:email>`.
43
+
44
+ **Why this matters:** the outbound and inbound legs are different servers with different
45
+ deploys. A deploy that succeeds on one does **not** fix the other. Decrypt/cert changes belong on
46
+ `prudential-store-prod`.
47
+
26
48
  ## The routing — the pipeline action names are misleading
27
49
 
28
50
  | Hostname | CNAME → EB environment | Deployed by pipeline action |
@@ -92,7 +114,41 @@ Observed 2026-09-15: `prudential-store-prod` on `i-07ae1b81ee48df938` (t3.micro,
92
114
  A reboot restored the EB command channel on 2026-09-15, but SSM still did not report after 3
93
115
  minutes — EB working again is the signal to retry, not SSM.
94
116
 
117
+ ## Incident 2026-09-15 — OAEP decrypt error caused by the stuck deploy
118
+
119
+ Production Sentry showed, from the togahub (LightSAML) ACS handler:
120
+
121
+ ```
122
+ Failure decrypting Data (openssl private) - RSA_padding_check_PKCS1_OAEP_mgf1: oaep decoding error
123
+ ```
124
+ (also seen as `RSA_EAY_PRIVATE_DECRYPT: padding check failed`)
125
+
126
+ **Root cause: not a bad key — a deploy that never landed.** `prudential-store-prod` was still
127
+ running the pre-rotation code (commit `275d16a9`, an Aug 13 build) because its deploy kept timing
128
+ out with the wedged-agent failure above. That old code loaded a **mismatched cert/key pair**: the
129
+ `togahub_com_2025-03-25.crt` cert with `toga_private_key.key`, which belongs to the 2026-02-27
130
+ cert. Confirmed by comparing the two openssl moduli — they differ. The new dual-keypair fallback
131
+ fixes this, but it was not on the box.
132
+
133
+ The `library` repo was verified **clean and not the cause**: all three new files committed and
134
+ pushed on `_production`, both keypairs (2027 active and 2026-02-27 fallback) verified as matching
135
+ pairs against the **committed git blobs** (not just local files), both keys unencrypted (no
136
+ `Proc-Type` / `DEK-Info`), and both loading via the exact call the code makes —
137
+ `openssl_pkey_get_private($pem, "")`.
138
+
139
+ **Lesson:** when a SAML decrypt error appears right after a cert rotation, check *what build is
140
+ actually deployed* before you suspect the key material. Verify against committed blobs, and note
141
+ that the stack trace alone cannot tell you which build is running (see the cert & keys feature
142
+ doc's gotcha on `decryptMulti`).
143
+
95
144
  ## Change history
145
+ - 2026-09-15 — Traced the outbound SAML leg: `prudential.togacommerce.com` redirects the browser
146
+ straight to Prudential's Entra tenant (no togahub hop), and Entra POSTs back to
147
+ `prudential.togahub.com/?acs` on a different EB environment — so a deploy to one leg does not
148
+ fix the other. Recorded the OAEP decrypt incident: the error came from `prudential-store-prod`
149
+ still serving the pre-rotation Aug 13 build (`275d16a9`) with a mismatched cert/key pair,
150
+ because its deploy kept timing out; the `library` repo was verified clean against committed
151
+ git blobs. (rgirish)
96
152
  - 2026-09-15 — Created while rotating the `togahub.com` SAML cert. Documented that
97
153
  `prudential.togahub.com` (SAML ACS) and `prudential.togacommerce.com` (storefront) are separate
98
154
  EB environments with swapped-sounding pipeline action names, the split AWS accounts/regions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.817",
3
+ "version": "1.0.819",
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",