toga-ai 1.0.1187 → 1.0.1189
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/2.0/apps/toga2-commerce/features/order-submit-sync-sequencing.md +4 -3
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/oneuptime-order-placement-errors-monitor.md +52 -0
- package/knowledge/clients/compass-usa/profile.md +7 -1
- package/knowledge/clients/nychh/features/transfer-order-notification-emails.md +66 -11
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-29
|
|
10
10
|
owners: ["bala", "apeterson", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-commerce/src/pages/OrderDetails/hooks/useSubmitOrder.ts
|
|
@@ -41,9 +41,9 @@ Submitting an order from the cart fires **two independent sync routines** — on
|
|
|
41
41
|
|
|
42
42
|
`updateSalesOrderEmails()` runs **first** inside `syncSalesOrderData()`, so it is the earliest header request on the wire and the one that collides with the delete burst.
|
|
43
43
|
|
|
44
|
-
### ⚠
|
|
44
|
+
### ⚠ On an edit, lines are NEVER sent nested on the order write — they go one request at a time
|
|
45
45
|
|
|
46
|
-
This is the single most misread thing about this path. **Commerce does not
|
|
46
|
+
This is the single most misread thing about this path. **On `editOrder`, Commerce does not send the order with its `salesOrderItems` inside it.** New placement (`viewType=cart`) is the opposite: `createNewSalesOrderSubmit.ts` sends ONE `POST /v2/sales-orders` with `salesOrderItems` nested (`depth: -1`), no item sync.
|
|
47
47
|
|
|
48
48
|
- `syncSalesOrdersDataFromLocalStorageCartToApi.ts` **line 20 `delete`s `salesOrderItems`** from the edit `PUT` payload outright.
|
|
49
49
|
- `syncSalesOrderItemsFromLocalStorageCartToApi.ts` then writes lines **individually**:
|
|
@@ -100,6 +100,7 @@ Three changes, in `toga2-commerce`:
|
|
|
100
100
|
- **There is NO success toaster on order placement — by design, not a bug.** `useSubmitOrder` calls `addToaster` **only in its `catch` block** (`status: 'error'`). Both success paths navigate away before anything could render: cart → `createNewSalesOrderSubmit()` → `navigate("/order-details?uuid=…&type=orderPlaced")` (no `addToaster` anywhere in it); edit-order → `window.location.href = getSupplyRedirectLink(orderUuid)`. **Consequence for debugging: placing an order is not a valid way to test toaster styling.** Use a path that stays mounted — add/edit shipping address (Cart or Account), Account → My Settings, or Cart → add new user; for the error variant, force a submit failure to hit the catch block. See [blox Toaster](../../toga-blox/features/toaster.md).
|
|
101
101
|
|
|
102
102
|
## Change history
|
|
103
|
+
- 2026-10-29 — Scoped "never nested" to the edit path; new placement nests lines in one `POST /v2/sales-orders` (TRUE-83381). (bala)
|
|
103
104
|
- 2026-10-23 — Price check before both submit paths (TRUE-82729, pending merge); see [cart price refresh](./cart-price-refresh.md). (tcox)
|
|
104
105
|
- 2026-08-11 — Item sync before header sync, `depth: 1`, self-overwrite `PUT` dropped (TRUE-80551). (bala, apeterson)
|
|
105
106
|
|
package/knowledge/INDEX.md
CHANGED
|
@@ -23,7 +23,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
23
23
|
## 2.0 framework
|
|
24
24
|
|
|
25
25
|
- **_underscore** (_Underscore) _(framework core)_ — 117 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
26
|
-
- **worker2** (Worker) —
|
|
26
|
+
- **worker2** (Worker) — 102 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
27
27
|
- **api2** (API) — 29 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
28
28
|
- **dbchanges2** (Database Changes) _(framework core)_ — 24 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
29
29
|
- **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
| [Compass: NetSuite duplicate sales-order monitor (Monitor/Compass/NetsuiteDuplicateSalesOrders)](features/oneuptime-netsuite-duplicate-sales-orders-monitor.md) | 2.0 | worker2 OneUptime push monitor that queries NetSuite (SuiteQL) for Office Depot POs with more than one open NetSuite sales order; open when working on Compass d |
|
|
27
27
|
| [Compass: Office Depot inbound AS2 files monitor (Monitor/Compass/OfficeDepotInboundFiles)](features/oneuptime-office-depot-inbound-files-monitor.md) | 2.0 | worker2 OneUptime push monitor that checks Office Depot 850/997 files are arriving over AS2 and are moved out of `agilant-as2/processed/` to the import folder; |
|
|
28
28
|
| [Compass: Office Depot lost orders monitor (Monitor/Compass/OfficeDepotLostOrders)](features/oneuptime-office-depot-lost-orders-monitor.md) | 2.0 | worker2 OneUptime push monitor that alarms when cron 3a logged an Office Depot 850 in legacy `Logs.FileLog` but no ODP PurchaseOrder exists in `Client_Compass` |
|
|
29
|
+
| [Compass USA: order placement errors monitor (Monitor/Compass/OrderPlacementErrors)](features/oneuptime-order-placement-errors-monitor.md) | 2.0 | worker2 OneUptime push monitor that alarms when a real Compass USA user gets an API error placing an order in TOGa Commerce (`POST /v2/sales-orders` >= 400 in ` |
|
|
29
30
|
| [Compass USA + Canada — order fulfillment status (standard base per-line rule)](features/order-fulfillment-status-per-line.md) | 2.0 | How Compass USA and Canada compute Pending / Picked / Packed / Partially Fulfilled / Fulfilled (the standard base per-line rule since TRUE-79669) and why the ol |
|
|
30
31
|
| [People File Import admin page (tools /compass/people-file-import → worker2)](features/people-file-import-admin-page.md) | 1.0 | Browser page that lists the Compass PEOPLE files on SFTP and runs the importer on demand; open before changing the page or the importer's listing/guard code. |
|
|
31
32
|
| [Compass PEOPLE-File User Lifecycle (duplicate accounts, reactivation grace window, raw-SQL deactivation)](features/people-file-user-lifecycle.md) | 2.0 | The nightly Compass PEOPLE-file cron's Users-row lifecycle (insert/reactivate/deactivate), why it creates duplicate accounts, and its raw-SQL/index traps; open |
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Compass USA: order placement errors monitor (Monitor/Compass/OrderPlacementErrors)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: compass-usa
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-29
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Monitor/Compass.php
|
|
13
|
+
- toga2-commerce/src/pages/OrderDetails/helpers/createNewSalesOrderSubmit.ts
|
|
14
|
+
related:
|
|
15
|
+
- ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
|
|
16
|
+
- ../../../2.0/apps/api2/features/request-logging.md
|
|
17
|
+
- ../../../2.0/apps/toga2-commerce/features/order-submit-sync-sequencing.md
|
|
18
|
+
- ./oneuptime-office-depot-lost-orders-monitor.md
|
|
19
|
+
- ../profile.md
|
|
20
|
+
---
|
|
21
|
+
worker2 OneUptime push monitor that alarms when a real Compass USA user gets an API error placing an order in TOGa Commerce (`POST /v2/sales-orders` >= 400 in `Logs_Compass.Api`); open when it alarms, when tuning it, or when a Compass user says checkout failed.
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
`Monitor/Compass/OrderPlacementErrors` (worker2 `_Worker_Monitor_Compass`, TRUE-83381). `Core.CronJobs` schedule `*/10 * * * *`, `maxExecutionTime` 120. OneUptime heartbeat monitor **"Compass USA Order Placement Errors"**: degraded after 25 min, down after 35 min with no push. Follows the shared [OneUptime push-metric pattern](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md); the push URL is an in-method local and a credential (not recorded here).
|
|
25
|
+
|
|
26
|
+
Why: Commerce shows the user the error and tells nobody else, so the team heard only when Compass reported it. Baseline 30 days to 2026-10-28: 1,748 good user order POSTs, 9 failed (7 by one user in 3 minutes on 2026-10-20, `EV` "error while trying to locate the references for the specified field"; 1 on 10-13; 1 on 10-05 "error while attempting to save the data model").
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
1. Registers `Logs_Compass` (the Logs cluster, `_Config::databaseLogs()`) in-method and calls `_Database::resetQueryCache()` (same SQL every run).
|
|
30
|
+
2. Reads `Logs_Compass.Api`, `dtStamp` window **11 to 1 minutes ago** (10-minute lookback = the cron interval, so no gaps; shifted 1 minute back so a still-running call is not counted as a crash). `dtStamp` is indexed and leads the filter.
|
|
31
|
+
3. Filters: `method = 'POST'`, `route = '/v2/sales-orders'`, `userId IS NOT NULL` (a person, not an API key), `requestPayload LIKE` the Compass customer uuid `4e480554-0c85-11ef-8e3b-0aae2980db55` (`Client_Compass.Customers.id 2` "Compass", the storefront customer), `responseCode >= 400 OR NULL` (NULL = the call crashed).
|
|
32
|
+
4. Any row → `alarm: HIGH`, else `OK`. Clears itself on the next clean window.
|
|
33
|
+
|
|
34
|
+
Payload: `status`, `alarm`, `reason`, `failedCount`, `affectedUserCount`, `failures` (max 20 lines: `dtStamp`, user name from `responsePayload.audience.user`, route, code, `messages[0].message` cut to 200 chars), `lookbackMinutes`, `checkedAtUtc`.
|
|
35
|
+
|
|
36
|
+
**No try/catch** (team choice): a DB failure throws, no push goes out, and the heartbeat goes degraded/down. A failed push only logs the HTTP code (never the URL).
|
|
37
|
+
|
|
38
|
+
Decisions (2026-10-29, with bala): every 10 minutes, 10-minute lookback; customer 2 storefront orders only; integration-key failures left out (API keys `apiId` 1/2 post standalone `/v2/sales-order-items` and have their own owners).
|
|
39
|
+
|
|
40
|
+
## Gotchas
|
|
41
|
+
- **Filter by the customer uuid inside `requestPayload`, not by `SalesOrders.customerId`.** A failed order writes no `SalesOrders` row, and `Logs_*` is a different cluster from `Client_Compass` (no join). The `LIKE` is cheap only because the `dtStamp` window is 10 minutes; an unbounded route/payload scan over 30 days timed out ([request-logging query rules](../../../2.0/apps/api2/features/request-logging.md)).
|
|
42
|
+
- **New placement nests the lines in `POST /v2/sales-orders`** (`createNewSalesOrderSubmit.ts`, `salesOrderItems` in the body), so one route covers checkout. **Order edits are not covered:** the edit path writes lines one at a time (`POST /v2/sales-orders/{uuid}/sales-order-items`, `PUT`/`DELETE /v2/sales-order-items/{uuid}`), see [order-submit sync](../../../2.0/apps/toga2-commerce/features/order-submit-sync-sequencing.md).
|
|
43
|
+
- **Blind spot: a throw from a PRE interceptor leaves no `Api` row**, so that rejection never reaches this monitor ([request-logging](../../../2.0/apps/api2/features/request-logging.md)). A normal 400 is logged with the full response envelope; `audience.app` = "TOGa Commerce", `audience.user` = the user's display name.
|
|
44
|
+
- `userId` is the token's user: a staff "view as" session logs under the Compass user's id, so a hit may be our own staff.
|
|
45
|
+
|
|
46
|
+
## Change history
|
|
47
|
+
- 2026-10-29 — Created: user `POST /v2/sales-orders` errors for customer 2, every 10 minutes (TRUE-83381). (bala)
|
|
48
|
+
|
|
49
|
+
## Related
|
|
50
|
+
- [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md)
|
|
51
|
+
- [V2 request logging](../../../2.0/apps/api2/features/request-logging.md)
|
|
52
|
+
- [Office Depot lost orders monitor](./oneuptime-office-depot-lost-orders-monitor.md)
|
|
@@ -22,7 +22,7 @@ project: _Underscore
|
|
|
22
22
|
client: compass-usa
|
|
23
23
|
type: profile
|
|
24
24
|
status: active
|
|
25
|
-
updated: 2026-10-
|
|
25
|
+
updated: 2026-10-29
|
|
26
26
|
owners: [jcardinal, bala, tcox, apeterson, dfranks, akhokhani, ajean, rgirish, amadda]
|
|
27
27
|
files: []
|
|
28
28
|
related:
|
|
@@ -60,6 +60,7 @@ related:
|
|
|
60
60
|
- features/odp-edi-855-acknowledgement-and-overquantity-guard.md
|
|
61
61
|
- features/odp-edi-file-retention-and-retry.md
|
|
62
62
|
- features/oneuptime-office-depot-lost-orders-monitor.md
|
|
63
|
+
- features/oneuptime-order-placement-errors-monitor.md
|
|
63
64
|
- ../../2.0/apps/worker2/features/compass-vip-support-importer.md
|
|
64
65
|
- ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
|
|
65
66
|
- ../../2.0/apps/toga2-commerce/features/category-tile-sort-order.md
|
|
@@ -137,6 +138,11 @@ separate, related client (see its own profile).
|
|
|
137
138
|
rows across 110 bundles still on the Agilant twins produced a fresh bad order on every kit
|
|
138
139
|
checkout. Fixed by restoring `BundleItems` in the complete catalog-fold reversal (fix is data, not
|
|
139
140
|
frontend): [catalog-fold reversal](workflows/catalog-fold-reversal.md).
|
|
141
|
+
- **Checkout API errors alarm in OneUptime (2026-10-29, TRUE-83381).** worker2
|
|
142
|
+
`Monitor/Compass/OrderPlacementErrors` checks `Logs_Compass.Api` every 10 minutes for a user
|
|
143
|
+
`POST /v2/sales-orders` >= 400 for customer 2 "Compass" (baseline: 9 failed vs 1,748 good in 30
|
|
144
|
+
days). Order edits and PRE-interceptor rejections are not covered:
|
|
145
|
+
[order placement errors monitor](features/oneuptime-order-placement-errors-monitor.md).
|
|
140
146
|
|
|
141
147
|
## Vendors & integrations
|
|
142
148
|
- **Office Depot (ODP)** — vendor id 1. ASNs arrive via **cXML** (direct V2 API) and via the
|
|
@@ -6,10 +6,11 @@ project: Worker
|
|
|
6
6
|
client: nychh
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-29
|
|
10
10
|
owners: ["bala", "rgirish", "jcardinal", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Client/Nychh/TransferOrderEmails.php
|
|
13
|
+
- worker2/Worker/Infrastructure/Email/Send.php
|
|
13
14
|
- _underscore/Model/Nychh/TransferOrder.php
|
|
14
15
|
- _underscore/Model/Nychh/ItemFulfillment.php
|
|
15
16
|
- dbchanges2/Client_Nychh/2026-09-15a - TransferOrderEmails.sql
|
|
@@ -58,6 +59,8 @@ Rules that carry most of the risk:
|
|
|
58
59
|
5. **Shipped fires on the first TRACKING NUMBER, never on the NetSuite shipped stage** (since
|
|
59
60
|
2026-10-05). The warehouse often marks a fulfillment shipped only after delivery, so the stage is
|
|
60
61
|
unreliable. No tracking → no Shipped email, even at stage shipped. See below.
|
|
62
|
+
6. **Shipped is RE-SENT when a new tracking number lands after it** (since 2026-10-21, TRUE-82673).
|
|
63
|
+
Same template and subject, so the customer reads it as a duplicate. See *Shipped resend*.
|
|
61
64
|
|
|
62
65
|
Recipient is `DevTeam@togatech.com` while testing. `2026-09-15b` swaps it to
|
|
63
66
|
`EndUserDeviceLifecycle@nychhc.org` — run that file only after sign-off.
|
|
@@ -68,10 +71,14 @@ Recipient is `DevTeam@togatech.com` while testing. `2026-09-15b` swaps it to
|
|
|
68
71
|
- `SendSubmitted(string $transferOrderUuid)` — queued from the TransferOrder `postPut`.
|
|
69
72
|
- `SendFulfillment(string $itemFulfillmentUuid)` — queued from the ItemFulfillment hooks.
|
|
70
73
|
- `SendPending(bool $dryRun = true)` — the `*/5` backstop cron.
|
|
71
|
-
- `resolveStagesStillToAnnounce(stageName, stageAlreadyEmailed, hasTrackingNumber
|
|
72
|
-
stages are still owed (see *Shipped = tracking number*).
|
|
74
|
+
- `resolveStagesStillToAnnounce(stageName, stageAlreadyEmailed, hasTrackingNumber,
|
|
75
|
+
hasTrackingAddedSinceEmail)` — which stages are still owed (see *Shipped = tracking number*).
|
|
73
76
|
- `buildHasTrackingNumberSql()` — the one "has tracking" test, shared by
|
|
74
77
|
`readItemFulfillmentForEmail()` (`hasTrackingNumber` column) and the `SendPending` sweep.
|
|
78
|
+
- `buildHasTrackingAddedSinceEmailSql()` — the one "tracking arrived after the last Shipped email"
|
|
79
|
+
test, shared the same way (`hasTrackingAddedSinceEmail` column + sweep). See *Shipped resend*.
|
|
80
|
+
- `readTrackingForEmail(itemFulfillmentId, isAddedSinceLastEmailOnly, addedUpToDateTime)` — all
|
|
81
|
+
numbers on the first Shipped, only the new ones on a resend.
|
|
75
82
|
- `readLinesMissingAssetTags()` / `sendPackedExceptionEmail()` — the asset-tag hold.
|
|
76
83
|
- `initialize()` registers the client DB before any query runs.
|
|
77
84
|
- Builders: `buildItemRowsHtml()`, `buildTrackingSectionHtml()`, `buildAssetTagTableHtml()`,
|
|
@@ -131,7 +138,8 @@ they must stay in step: the Submitted guard, the order read, and both `SendPendi
|
|
|
131
138
|
`c_dtTransferOrderSubmittedEmailSent` marker, sends, then stamps the marker.
|
|
132
139
|
2. **Packed / Shipped** — the ItemFulfillment `postPost` / `postPut` queue `SendFulfillment` with
|
|
133
140
|
the fulfillment uuid. The stage reached is recorded on `c_transferOrderEmailStageSent`, with
|
|
134
|
-
`c_dtTransferOrderEmailSent` as the timestamp, so a fulfillment cannot re-announce a stage
|
|
141
|
+
`c_dtTransferOrderEmailSent` as the timestamp, so a fulfillment cannot re-announce a stage
|
|
142
|
+
(one exception: the Shipped resend).
|
|
135
143
|
`resolveStagesStillToAnnounce()` decides what is owed (table in *Shipped = tracking number*).
|
|
136
144
|
When packed was never announced and shipped is now owed it returns **`[packed, shipped]`**, both
|
|
137
145
|
sent in that order in one run — the **asset-tag check lives on packed**, so packed must never be
|
|
@@ -139,7 +147,8 @@ they must stay in step: the Submitted guard, the order read, and both `SendPendi
|
|
|
139
147
|
sync is a **daily** cron, so a fulfillment landing fully shipped/tracked is normal — see
|
|
140
148
|
[the per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
|
|
141
149
|
3. The **backstop cron** sweeps anything the hooks missed (below).
|
|
142
|
-
4. All three paths share the same markers, so nothing sends twice
|
|
150
|
+
4. All three paths share the same markers, so nothing sends twice by accident (the Shipped resend
|
|
151
|
+
is on purpose).
|
|
143
152
|
5. The worker renders the stored `EmailTemplates` body, fills `{orderItems}` and
|
|
144
153
|
`{trackingSection}` itself, and sends through `_Model_Client_EmailTemplate`.
|
|
145
154
|
|
|
@@ -173,7 +182,8 @@ PUT follows.** Fulfillment **2704** in production: header row at 20:33:09, lines
|
|
|
173
182
|
|
|
174
183
|
| Marker (`c_transferOrderEmailStageSent`) | Tracking | Stage | Owed |
|
|
175
184
|
|---|---|---|---|
|
|
176
|
-
| `shipped` |
|
|
185
|
+
| `shipped` | added since last email | any | `[shipped]` again (resend) |
|
|
186
|
+
| `shipped` | otherwise | any | nothing |
|
|
177
187
|
| `packed` | yes | any | `[shipped]` |
|
|
178
188
|
| empty | yes | any (incl. picked — tracking means boxed) | `[packed, shipped]` |
|
|
179
189
|
| empty | no | packed / shipped | `[packed]` |
|
|
@@ -181,16 +191,58 @@ PUT follows.** Fulfillment **2704** in production: header row at 20:33:09, lines
|
|
|
181
191
|
|
|
182
192
|
The asset-tag hold runs whenever packed is owed and blocks both.
|
|
183
193
|
|
|
184
|
-
- **⚠ `SendPending` WHERE must match that table:** marker NULL or `<> 'shipped'` AND (has tracking OR
|
|
185
|
-
(stage IN packed/shipped AND `COALESCE(marker,'') = ''`))
|
|
186
|
-
`LIMIT 25`.
|
|
194
|
+
- **⚠ `SendPending` WHERE must match that table:** (marker NULL or `<> 'shipped'` AND (has tracking OR
|
|
195
|
+
(stage IN packed/shipped AND `COALESCE(marker,'') = ''`))) OR (marker = `shipped` AND
|
|
196
|
+
`buildHasTrackingAddedSinceEmailSql()`). Drift → rows picked then skipped, eating `LIMIT 25`.
|
|
187
197
|
- **Why the cron, not the hook, sends Shipped:** library `syncItemFulfillmentFromNetsuite`
|
|
188
198
|
(`library/app/api/toga2.php`) PUTs the stage first (~L7270), then units/asset tags, and writes
|
|
189
199
|
package tracking **last** (~L8046). The ItemFulfillment `postPut` job runs before tracking exists.
|
|
190
200
|
(Also removes the old race where Shipped read stale tracking/tags.) Tracking imports at picked,
|
|
191
201
|
packed or shipped alike.
|
|
192
|
-
- **Once per fulfillment.** Business rule = 1 tracking number
|
|
193
|
-
(IF 329047 has 2; IFs 329944/330013 share 1).
|
|
202
|
+
- **Once per fulfillment, plus one resend per NEW tracking number.** Business rule = 1 tracking number
|
|
203
|
+
per fulfillment; prod has violations (IF 329047 has 2; IFs 329944/330013 share 1). Numbers present at
|
|
204
|
+
the first Shipped all go in that one email. A number that arrives later re-sends Shipped — see
|
|
205
|
+
*Shipped resend*.
|
|
206
|
+
|
|
207
|
+
### Shipped resend when tracking changes (TRUE-82673, live 2026-10-21)
|
|
208
|
+
|
|
209
|
+
worker2 `c4d5008` + `1a1e3a0`, PR #193. Built for a warehouse re-keying a mistyped number.
|
|
210
|
+
|
|
211
|
+
- **Trigger** = `buildHasTrackingAddedSinceEmailSql()`: a linked `TrackingNumbers` row with
|
|
212
|
+
`dtCreated > ItemFulfillments.c_dtTransferOrderEmailSent`. The sync POSTs a new `TrackingNumbers`
|
|
213
|
+
row for a number the client has never used, so `dtCreated` = arrival. A number the client already
|
|
214
|
+
holds is reused with its OLD `dtCreated`, so re-attaching an existing number is NOT announced.
|
|
215
|
+
- **Cutoff, not NOW().** `SendFulfillment()` reads DB time first (`$trackingCutoff`), uses it as the
|
|
216
|
+
upper bound for the tracking read, and stores it as `c_dtTransferOrderEmailSent`. A number landing
|
|
217
|
+
mid-send is still newer than the marker, so the next run announces it.
|
|
218
|
+
- **Body lists only the new number(s)** (`isAddedSinceLastEmailOnly`). The old number stays linked in
|
|
219
|
+
`ItemFulfillments_TrackingNumbers` (the sync needs it) but is left out of the email.
|
|
220
|
+
- **⚠ Same template, same subject ("Order Shipped: N items"), same wording.** Nothing says it is a
|
|
221
|
+
correction, so the customer reports it as a duplicate. Expected behavior, not a bug.
|
|
222
|
+
|
|
223
|
+
### Triage: customer reports a duplicate Shipped email
|
|
224
|
+
|
|
225
|
+
Read-only prod, in order:
|
|
226
|
+
|
|
227
|
+
1. `Client_Nychh.TransferOrders` by `number` LEFT JOIN `ItemFulfillments` ON `transferOrderId` →
|
|
228
|
+
`c_transferOrderEmailStageSent`, `c_dtTransferOrderEmailSent` per fulfillment.
|
|
229
|
+
2. `Logs_Nychh.Email WHERE INSTR(body, '<TO number>') > 0` → every email for the order, with `to`,
|
|
230
|
+
`cc`, `bcc`, `dtSent` (columns are `to`/`cc`/`bcc`, not `toAddresses`).
|
|
231
|
+
3. `ItemFulfillments_TrackingNumbers` JOIN `TrackingNumbers` for the IF → compare each
|
|
232
|
+
`TrackingNumbers.dtCreated` with `c_dtTransferOrderEmailSent`. A later number = the resend.
|
|
233
|
+
4. Two copies of the SAME email (same number, same minute): `Core.WorkerJobs WHERE
|
|
234
|
+
action = 'Infrastructure/Email/Send/Run'` around `dtSent` (cron `Core.CronJobs` id 37). One run =
|
|
235
|
+
we sent it once; the second copy came from the client side.
|
|
236
|
+
|
|
237
|
+
- **toga-db MCP rejects `REPLACE()`** as a forbidden keyword. Use `INSTR(body,'x') > 0`.
|
|
238
|
+
- **Real case, TRUE-83389 (2026-10-29):** TO 294663 (`TransferOrders.id` 2071), one IF 333376
|
|
239
|
+
(`ItemFulfillments.id` 3104). Shipped #1 = Email 198, 2026-10-26 17:26, tracking `30494276`
|
|
240
|
+
(TrackingNumbers 4059). NetSuite re-keyed it to `34094276` (TrackingNumbers 4063, created
|
|
241
|
+
2026-10-29 08:36, carrier 101 New York Minute) → Shipped #2 = Email 204, 08:41, only the new
|
|
242
|
+
number. Both bridges (4211, 4215) stay linked. The "04"→"40" swap looks like a typo fix (inferred).
|
|
243
|
+
The client's two screenshots were both Email 204, sent in one Email Send run at 08:41:45, so the
|
|
244
|
+
doubled copy came from their side (To `EndUserDeviceLifecycle@nychhc.org` + CC
|
|
245
|
+
`HealthcareteamNYCHHC@togatech.com` reaching one person is a guess, not proven).
|
|
194
246
|
- **Lag:** adding tracking in NetSuite bumps the IF `lastmodifieddate`, so the sync re-reads it, but
|
|
195
247
|
only on the NYCHH sync interval (daily). E.g. IF 330377 (NS 7566823) still Packed (status B) in
|
|
196
248
|
NetSuite, tracking imported 2026-10-05 11:56.
|
|
@@ -417,6 +469,9 @@ loaded out of the dbchanges2 file so an unresolved `{placeholder}` fails a test.
|
|
|
417
469
|
unescaped values and both detail-box defects were all found there, not in production.
|
|
418
470
|
|
|
419
471
|
## Change history
|
|
472
|
+
- 2026-10-29 — Recorded the Shipped resend on a new tracking number (TRUE-82673, live 2026-10-21) and
|
|
473
|
+
the duplicate-email triage steps, from TRUE-83389 (TO 294663). Investigation only, no code change.
|
|
474
|
+
(bala)
|
|
420
475
|
- 2026-10-16 — Linked the emails-owed monitor (TRUE-82758). (mhammontree)
|
|
421
476
|
- 2026-10-09 — **New York Minute shows its logo, and NON_SERIALIZED lines are ignored by the asset-tag
|
|
422
477
|
reads** (TRUE-82527). `CARRIER_LOGO_URL_BY_CARRIER_NAME` gained `NEW YORK MINUTE` (no tracking site,
|
package/package.json
CHANGED