toga-ai 1.0.1049 → 1.0.1051

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.
@@ -18,6 +18,8 @@ files:
18
18
  - worker2/Worker/Startech.php
19
19
  - toga25-supply/src/utils/internalCustomer.ts
20
20
  - toga2-commerce/src/utils/internalCustomer.ts
21
+ - toga2-commerce/src/api/CartApi.ts
22
+ - toga2-commerce/src/api/axiosInstance.ts
21
23
  related:
22
24
  - ./client-user-contactid-required.md
23
25
  - ../../api2/features/nested-relationship-writes.md
@@ -28,7 +30,10 @@ Every `Client_<Tenant>.Contacts` row must have a `customerId`; with no real cust
28
30
 
29
31
  ## Summary
30
32
 
31
- Decision (2026-10-16, jcardinal, TRUE-82841): `Contacts.customerId` is **NOT NULL** in every 2.0 client DB. Every tenant has ONE `Customers` row named `Internal` with the **same hardcoded uuid** `20436694-8925-4eea-95b9-c97d7caa813b`. Contacts with no real customer (client staff, imports) point at it.
33
+ Decision (2026-10-16, jcardinal, TRUE-82841): `Contacts.customerId` is **NOT NULL** in every 2.0 client DB. Every tenant has ONE "Internal customer" `Customers` row with the **same hardcoded uuid** `20436694-8925-4eea-95b9-c97d7caa813b`. Contacts with no real customer (client staff, imports) point at it.
34
+
35
+ - **The row is NAMED AFTER THE CLIENT, not "Internal"** (jcardinal). `Client/2026-10-16a` sets the name with a `CASE DATABASE()` map of client DB → client name (copied from prod `Core.Clients` at authoring time, e.g. `Client_Compass` → `'Compass Group'`); an unknown DB falls back to `SUBSTRING(DATABASE(), 8)`.
36
+ - **Find it only by the uuid, never by name.**
32
37
 
33
38
  **Rule:** every code path that creates a Contact must set a customer.
34
39
  - api2 payload → `customer: {uuid: <INTERNAL_CUSTOMER_UUID>}` (or the real customer's uuid).
@@ -36,13 +41,16 @@ Decision (2026-10-16, jcardinal, TRUE-82841): `Contacts.customerId` is **NOT NUL
36
41
 
37
42
  Rejected: a `Customers.slug` marker column; app-level validation instead of a DB `NOT NULL`; a silent model-wide default.
38
43
 
39
- **Rollout order:** `Client/2026-10-16a` seed (before deploy) → code deploy → `Client/2026-10-16b` backfill + `NOT NULL` (after deploy). Running `b` before the code fix breaks every unfixed create path.
44
+ **Rollout order:** `Client/2026-10-16a` seed **+ backfill** (before deploy) → code deploy → `Client/2026-10-16b` straggler backfill + `NOT NULL` (after deploy).
45
+ - Backfill must run BEFORE the code that sends `customer`: api2 matches nested contacts on `customerId` too (gotcha below), so an un-backfilled NULL contact would be duplicated.
46
+ - Running `b` before the code fix breaks every unfixed create path.
40
47
 
41
48
  ## How it works
42
49
 
43
50
  - Constant: `_Model_Client_Customer::INTERNAL_CUSTOMER_UUID` (`_underscore/Model/Client/Customer.php`). Front-ends keep their own `INTERNAL_CUSTOMER_UUID` in `src/utils/internalCustomer.ts` (toga25-supply, toga2-commerce).
44
51
  - `internalCustomerId(string $database = DB_CLIENT): int` — resolves the id by uuid on the given connection; ids differ per tenant, the uuid does not.
45
- - `Contacts.customerId` = `Core.RecordFields` id **1651** (record 37 `contacts`), childPolicy `MATCH` — so a `{uuid}` nested customer links, never creates. Base role can write it in Wje / Elite / Pcmaticb2b / Compass / CompassCanada; Public cannot.
52
+ - `Contacts.customerId` = `Core.RecordFields` id **1651** (record 37 `contacts`), childPolicy `MATCH`, **`isIdentifier = 1`** (prod) — a `{uuid}` nested customer links, never creates (`customers.uuid` 675 is an identifier). Base role can write it in Wje / Elite / Pcmaticb2b / Compass / CompassCanada; Public cannot.
53
+ - Nested-write ACL = top-level record create + FIELD write for every nested field sent; a missing 1651 grant is EV-12 (400), never a silent drop — see [nested-relationship-writes](../../api2/features/nested-relationship-writes.md).
46
54
  - A contact can also be created **through a parent's nested `contact` FK** — those paths need the customer too (list of contact FKs that can CREATE: [nested-relationship-writes](../../api2/features/nested-relationship-writes.md)).
47
55
 
48
56
  ### Every contact-creation path (2026-10-16)
@@ -58,15 +66,17 @@ Rejected: a `Customers.slug` marker column; app-level validation instead of a DB
58
66
  | Freshservice syncs (contacts + nested on units/tickets) | `worker2/Worker/Wje.php`, `Worker/Elite.php` | Wje, Elite | Internal uuid |
59
67
  | Startech tickets (nested contact) | `worker2/Worker/Startech.php` | Pcmaticb2b | Internal uuid — see gotcha |
60
68
  | Location contact | toga25-supply `useCreateLocationContact.ts` | all | Internal uuid |
61
- | Cart "Add user" nested contact | toga2-commerce `CartApi.ts` `addUser` | all | Internal uuid |
69
+ | Cart "Add user" nested contact | toga2-commerce `CartApi.ts` `addUser` (signed-in user's token) | all | Internal uuid — see gotcha |
62
70
  | Rate SSO | `Model/Rate/ClientAuthentication.php` | Rate | already a real customer |
63
71
 
64
72
  ## Gotchas
65
73
 
66
- - **⚠ A `MATCH_UPSERT` nested contact that sends `customer` OVERWRITES an existing contact's customer.** Matching is link-and-update. Startech/Pcmaticb2b: 3 contacts with real customers carry `c_startechUserId` and can be flipped to Internal — **open item**.
74
+ - **⚠ A nested contact that sends `customer` is MATCHED on `customerId` too** (1651 is an identifier). `{c_wjeUserId, customer:{uuid}}` searches c_wjeUserId AND customerId; an existing contact with NULL or a different customer does not match, so `MATCH_CREATE`/`MATCH_UPSERT` **creates a duplicate contact** (not an overwrite). Startech/Pcmaticb2b: 3 contacts with a real customer + `c_startechUserId` get a duplicate on their next Startech ticket — **open item**; proposed fix (look up by `c_startechUserId` first, send `{uuid}`) not taken.
75
+ - **Commerce signup ACL (prod 2026-10-16):** `addUser` (`POST /users` depth -1) runs on the signed-in user's token (`src/api/axiosInstance.ts` ~L193), not the public token. Client_Compass: users-create roles = Base(1), API(3), Admin(4), SuperUser(5), Manager(8); 1651 writable for Base, API, Compass Base(7) only — Admin(4) alone would fail EV-12 (no current user depends on it; parity grant offered, not taken). Client_CompassCanada: only Base creates users, and Base can write 1651. Everyone who can sign up today still can.
67
76
  - **Prod baseline 2026-10-16:** ~443k contacts had `customerId` NULL; only Rate (726/728) and Pcmaticb2b (21/398) used the field; many clients had zero `Customers` rows. Most NULL contacts are client staff linked from `Users.contactId`.
68
77
  - New raw-SQL contact INSERTs: include `customerId` in the column list — omitted, the INSERT fails once the column is `NOT NULL`. Same write-order lesson as [client-user-contactid-required](./client-user-contactid-required.md).
69
78
  - Backfill/ALTER technique (`dtUpdated = dtUpdated`, strict-mode in-place ALTER): [rerunnable-additive-inserts](../../dbchanges2/features/rerunnable-additive-inserts.md).
70
79
 
71
80
  ## Change history
81
+ - 2026-10-16 — Row named after the client (find by uuid only); backfill moved into `a` (before deploy) because 1651 is an identifier and unmatched contacts get duplicated, not overwritten; commerce signup ACL check (TRUE-82841). (jcardinal)
72
82
  - 2026-10-16 — Initial. Internal customer (fixed uuid, every tenant) + `Contacts.customerId` NOT NULL; every contact-create path fixed (TRUE-82841). (jcardinal)
@@ -196,10 +196,13 @@ behaviour — it bites any write to a nested child, not one feature. Worked exam
196
196
  - **Declaring a `c_` field on the model does not make it searchable** — the `CustomRecordFields.isIdentifier` flag is the gate; set the flag, not the child policy.
197
197
  - **An identifier that is unique only inside a parent (e.g. `bundles.number` per catalog) links the WRONG record, silently.** The match uses only the identifier fields sent, so `bundle: {number: "001"}` hits another catalog's `001`. Send `uuid` (or add the scoping identifier) resolved in the caller. Seen on Prudential India REQs — see [bundle catalog resolution](../../../clients/prudential/features/service-request-bundle-catalog-resolution.md).
198
198
  - **⚠ A nested `unit` with only an `item` (no serial/asset tag) REUSES one existing unit.** `ServiceRequests_Units.unitId` is `MATCH_UPSERT` and `Units.itemId` is an identifier, so `{unit:{item:{uuid}}}` matches the first unit of that item — every SR links the same unit. For serial-less requests attach a **bundle** instead (`ServiceRequests_Bundles.bundleId` is `MATCH`; send `{bundle:{uuid}}`). See [Adyen reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md).
199
- - **Parent FKs that can CREATE a Contact** (prod `Core.RecordFields.childPolicy`, 2026-10-16): `MATCH_CREATE` — `Users.contactId`, `PurchaseOrders.contactId`; `MATCH_UPSERT` — `Tickets.contactId`, `Tickets.reportedByContactId`, `Units.contactId`, `UnitNotes.contactId`, `Entitlements.contactId`, `ServiceRequests.requestedForContactId`, `ContactPhoneNumbers.contactId`. A nested contact created through any of these must carry a `customer` — see [contact-customer-required-internal-customer](../../_underscore/features/contact-customer-required-internal-customer.md). On `MATCH_UPSERT`, a `customer` sent for a matched contact overwrites its existing customer.
199
+ - **Parent FKs that can CREATE a Contact** (prod `Core.RecordFields.childPolicy`, 2026-10-16): `MATCH_CREATE` — `Users.contactId`, `PurchaseOrders.contactId`; `MATCH_UPSERT` — `Tickets.contactId`, `Tickets.reportedByContactId`, `Units.contactId`, `UnitNotes.contactId`, `Entitlements.contactId`, `ServiceRequests.requestedForContactId`, `ContactPhoneNumbers.contactId`. A nested contact created through any of these must carry a `customer` — see [contact-customer-required-internal-customer](../../_underscore/features/contact-customer-required-internal-customer.md).
200
+ - **⚠ Every identifier field SENT joins the match.** `contacts.customerId` (1651) is `isIdentifier = 1`, so `{c_wjeUserId, customer:{uuid}}` matches on c_wjeUserId AND customerId. A contact with a NULL/different customer does not match → `MATCH_CREATE`/`MATCH_UPSERT` **creates a duplicate**. Backfill the field before code starts sending it.
201
+ - **Nested-write ACL** (`V2.php` `getForeignKeyValue` ~L9790-10469): no record-level ACL on the nested record. Checked = top-level record create (e.g. users, Core.Records 2) + FIELD write for every field sent on the nested record (e.g. contacts, 37), using the caller's `id.client.roles`. A missing field grant fails **EV-12** (400, "lack write permissions"), never a silent drop.
200
202
  - **Singular-FK back-reference defects:** UPDATE path drops the parent's forward FK (echoes `null`); CREATE path re-parents a value-matched child to the newest writer (stolen child row). One defect, two symptoms.
201
203
 
202
204
  ## Change history
205
+ - 2026-10-16 — Corrected: a sent identifier field (contacts.customerId 1651) joins the match, so a mismatched contact is duplicated, not overwritten; nested-write ACL = top-level create + per-field write, EV-12 on a gap (TRUE-82841). (jcardinal)
203
206
  - 2026-10-16 — Listed the parent FKs whose nested contact can CREATE a Contact (each must send `customer`); MATCH_UPSERT overwrites a matched contact's customer (TRUE-82841). (jcardinal)
204
207
  - 2026-09-30 — Gotcha: item-only nested unit matches an existing unit; use a bundle for serial-less SRs. (rgirish)
205
208
  - 2026-09-29 — Gotcha: a per-parent identifier (`bundles.number`) matches across parents and links the wrong row. (rgirish)
@@ -326,6 +326,10 @@ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
326
326
  NULL (no silent `0`), and in-place NOT NULL **needs** strict mode (error 1846 otherwise, even with
327
327
  0 NULLs). Backfill with `dtUpdated = dtUpdated` so syncs do not see every row as changed. Worked
328
328
  file: `Client/2026-10-16b - TRUE-82841 Contacts customerId NOT NULL.sql`.
329
+ - **Backfill BEFORE the deploy when the column is an api2 identifier.** Put the main backfill in the
330
+ pre-deploy seed file, and repeat it for stragglers in the post-deploy ALTER file. Otherwise new code
331
+ that sends the field fails to match un-backfilled rows and api2 creates duplicates (TRUE-82841:
332
+ `Client/2026-10-16a` seed + backfill, `Client/2026-10-16b` straggler backfill + ALTER).
329
333
 
330
334
  ### ⚠ A column DEFAULT does NOT protect a `_Model`-written column
331
335
 
@@ -555,6 +559,7 @@ an INSERT that names only `uuid`/`tableViewId`/`recordFieldId`/`isVisible`/`inde
555
559
  [FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
556
560
 
557
561
  ## Change history
562
+ - 2026-10-16 — Backfill an api2-identifier column before the deploy (seed file), stragglers + ALTER after (TRUE-82841). (jcardinal)
558
563
  - 2026-10-16 — NOT NULL alternative for big tables: strict session + `ALGORITHM=INPLACE, LOCK=NONE`
559
564
  (1846 without strict), `lock_wait_timeout`, `dtUpdated = dtUpdated` backfill (TRUE-82841). (jcardinal)
560
565
  - 2026-10-16 — Fan-out seed keyed on a slug another folder sets can silently no-op; back-fill slugs
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-15
9
+ updated: 2026-10-16
10
10
  owners: ["jcardinal", "mhammontree", "bala", "rgirish"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Compass.php
@@ -39,6 +39,7 @@ related:
39
39
  - ../../../clients/compass-canada/features/oneuptime-po-transmissions-to-vendors-monitor.md
40
40
  - ../../../../clients/nychh/features/oneuptime-asset-tag-backfill-monitor.md
41
41
  - ../../../../clients/compass-usa/features/oneuptime-office-depot-inbound-files-monitor.md
42
+ - ../../../clients/nychh/features/oneuptime-transfer-order-emails-owed-monitor.md
42
43
  ---
43
44
 
44
45
  OneUptime push-metric monitor pattern for worker2 (2.0): a self-contained cron action decides pass/fail and POSTs a string token to a OneUptime Incoming Request monitor; open when building/tuning a worker2 monitor, its thresholds, or the OneUptime import JSON.
@@ -595,6 +596,12 @@ production deploy.**
595
596
  - `_Query::fetchRow()` returns an object, not an array — access `$row->field`.
596
597
  - Keep the metric POST non-fatal (`throwExceptionsOnFailure` off) so a monitoring outage never breaks the
597
598
  worker job it rides in.
599
+ - **Two branches adding a monitor to the same `Monitor/<Client>.php` conflict if both insert at the
600
+ same spot.** Blank-line padding does not help — git merges by surrounding context. Put the new
601
+ method somewhere else (e.g. after the last helper) so the hunks sit far apart. Check with
602
+ `git merge-file` on base/ours/theirs, but normalize line endings first: `git show` gives LF, so a
603
+ CRLF working copy turns the whole file into one conflict. (Seen on `Monitor/Nychh.php`, TRUE-82760 +
604
+ TRUE-82758.)
598
605
  - The `agilant-as2` S3 bucket is in **us-west-2**, not the config-default us-east-1 — S3 monitors must
599
606
  pass the region (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
600
607
 
@@ -606,6 +613,7 @@ production deploy.**
606
613
  - [OneUptime 1.0 worker uptime monitoring](../../../1.0/apps/worker/features/oneuptime-worker-uptime-monitoring.md) — the 1.0 push-heartbeat predecessor
607
614
 
608
615
  ## Change history
616
+ - 2026-10-16 — Added same-class merge-conflict gotcha; linked NYCHH `TransferOrderEmailsOwed` (TRUE-82758). (mhammontree)
609
617
  - 2026-10-15 — Absence rule must exceed the longest gap between pushes for non-`*/5` monitors; linked NYCHH `AssetTagBackfill` (twice daily, 960 min). (mhammontree)
610
618
  - 2026-10-13 — Added `OfficeDepotInboundFiles` (cron 93, CloudWatch + S3) after `OfficeDepotEdiImportQueue` stayed green through the 2026-10-10 AS2 outage; import JSONs are never committed to worker2. (bala)
611
619
  - 2026-10-05 — Added `CompassCanadaPurchaseOrderTransmissionsToVendors` (cron 87), the first monitor reading `Client_CompassCanada`. (bala)
@@ -62,9 +62,10 @@ Both filters use `fields` to request only `id`/`uuid`. Filtering on `ticketTypeI
62
62
 
63
63
  - `c_escalateToStartech: 0` must be in the payload. If omitted, the interceptor still skips Startech (null is also falsy), but the field stays NULL in TOGA 2.0 — making webhook tickets indistinguishable from TOGaDesk-created ones.
64
64
  - `ticketType` is resolved via `c_startechTicketTypeId` on `TicketTypes` — not a hardcoded string. Startech type IDs: SR=6, PCS=54, API=201.
65
- - **⚠ The nested `contact` sends the Internal customer** (`Contacts.customerId` NOT NULL — see [contact-customer-required-internal-customer](../../_underscore/features/contact-customer-required-internal-customer.md)). `Tickets.contactId` is `MATCH_UPSERT`, so a matched existing contact gets its customer overwritten to Internal. Open item (2026-10-16): 3 Pcmaticb2b contacts with real customers carry `c_startechUserId` and are exposed.
65
+ - **⚠ The nested `contact` sends the Internal customer** (`Contacts.customerId` NOT NULL — see [contact-customer-required-internal-customer](../../_underscore/features/contact-customer-required-internal-customer.md)). `Tickets.contactId` is `MATCH_UPSERT` and `customerId` is an identifier, so the match uses `c_startechUserId` AND customerId. A contact whose customer is not the Internal one does not match and **gets a DUPLICATE contact** (not an overwrite). Open item (2026-10-16): 3 Pcmaticb2b contacts with a real customer + `c_startechUserId` will be duplicated on their next ticket. Proposed fix, not taken: look up the contact by `c_startechUserId` first and send `{uuid}`.
66
66
 
67
67
  ## Change history
68
+ - 2026-10-16 — Corrected: a mismatched-customer contact is duplicated, not overwritten; proposed lookup-first fix recorded (TRUE-82841). (jcardinal)
68
69
  - 2026-10-16 — Nested contact now carries the Internal customer; noted the customer-overwrite risk on matched contacts (TRUE-82841). (jcardinal)
69
70
 
70
71
  ## Related
@@ -22,7 +22,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
22
22
  ## 2.0 framework
23
23
 
24
24
  - **_underscore** (_Underscore) _(framework core)_ — 101 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
25
- - **worker2** (Worker) — 87 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
25
+ - **worker2** (Worker) — 88 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
26
26
  - **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
27
27
  - **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
28
28
  - **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -6,6 +6,7 @@
6
6
  | [NYCHH inventory-adjustment → item-fulfillment linking (many bridges, REST)](features/netsuite-inventory-adjustment-fulfillment-link.md) | 1.0 | NYCHH import of the NetSuite inventory-adjustment→item-fulfillment link (direct FK, REST); open when a NYCHH adjustment's FK link is missing or the INVENTORY_AD |
7
7
  | [NYCHH NetSuite → TransferOrders import (stocking-flag routing, PO bridges, origin location)](features/netsuite-transfer-order-import.md) | 1.0 | The 1.0 NetSuite→`Client_Nychh.TransferOrders` importer (stocking-flag routing, PO bridge links, origin/destination resolution, status map); open for any NYCHH |
8
8
  | [NYCHH: asset-tag backfill monitor (Monitor/Nychh/AssetTagBackfill)](features/oneuptime-asset-tag-backfill-monitor.md) | 2.0 | worker2 OneUptime push monitor that alarms when new `Client_Nychh` units stay blank on asset tag while NetSuite has a tag for their serial; open when it fires o |
9
+ | [NYCHH: transfer-order emails owed monitor (Monitor/Nychh/TransferOrderEmailsOwed)](features/oneuptime-transfer-order-emails-owed-monitor.md) | 2.0 | worker2 OneUptime push monitor that alarms when a NYCHH Packed/Shipped transfer-order email has been owed too long; open when it fires or when changing the owed |
9
10
  | [NYCHH: transfer-order NetSuite push monitor (Monitor/Nychh/TransferOrderNetsuitePush)](features/oneuptime-transfer-order-netsuite-push-monitor.md) | 2.0 | worker2 OneUptime push monitor that alarms when a `Client_Nychh` transfer order has no NetSuite id 15+ minutes after creation; open when it fires, or when chang |
10
11
  | [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | Why NYCHH PO Number is empty on the downstream SO→PO route/`_purchaseOrders` field (links are upstream); open when a NYCHH SO shows a PO in the list but not the |
11
12
  | [NYCHH Talos Assistant Enablement](features/talos-assistant-enablement.md) | 2.0 | NYCHH enables the shared Talos Supply panel through its Surface overrides and independent backend grants; open for suggestion configuration, exact tenant identi |
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: "NYCHH: transfer-order emails owed monitor (Monitor/Nychh/TransferOrderEmailsOwed)"
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: nychh
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-10-16
10
+ owners: ["mhammontree"]
11
+ files:
12
+ - worker2/Worker/Monitor/Nychh.php
13
+ - dbchanges2/Core/2026-10-15b - NYCHH Transfer Order Emails Owed Monitor.sql
14
+ related:
15
+ - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
16
+ - ./transfer-order-notification-emails.md
17
+ - ./oneuptime-asset-tag-backfill-monitor.md
18
+ - ../profile.md
19
+ ---
20
+ worker2 OneUptime push monitor that alarms when a NYCHH Packed/Shipped transfer-order email has been owed too long; open when it fires or when changing the owed test in `TransferOrderEmails.php`.
21
+
22
+ ## Summary
23
+ `Monitor/Nychh/TransferOrderEmailsOwed` (`_Worker_Monitor_Nychh::TransferOrderEmailsOwed(?int $owedHours = null)`) watches the [transfer-order emails](./transfer-order-notification-emails.md) backstop cron. Built on TRUE-82758. Follows the shared [OneUptime push-metric pattern](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md).
24
+
25
+ - `Core.CronJobs` uuid `5648dc93-5ad7-4a17-a552-ef1a37917161`, `17 * * * *`, `maxExecutionTime` 300, `parameters` `{"owedHours": 24}`, shipped `isActive = 0`.
26
+ - Push URL = in-method `$ONEUPTIME_URL` local (credential; never record the value).
27
+ - Committed worker2 `c26186b` + `972d231`, dbchanges2 `61f7c6b`; pushed, not deployed as of 2026-10-16.
28
+
29
+ ## How it works
30
+ 1. "Owed" = the same test as `TransferOrderEmails::sendPendingFulfillmentEmails()` + `buildHasTrackingNumberSql()`, plus `ItemFulfillments.dtCreated >= 2026-10-15` (new data only).
31
+ 2. Owed-since = `c_dtTransferOrderPackedExceptionSent` only while still held (`c_transferOrderEmailStageSent` blank); else `ItemFulfillments.dtUpdated`.
32
+ 3. Any row owed longer than `owedHours` → `"alarm":"HIGH"`; none → `"alarm":"OK"`; missing `owedHours` or DB failure → `"status":"error"`.
33
+ 4. Payload: `owedCount`, `heldForAssetTagsCount`, `owedFulfillments` (`TO/IF`, ` (held)` suffix, first 25), `oldestOwedSince`, `owedHours`, `cutoffDate`.
34
+
35
+ OneUptime: Incoming Request monitor "Monitor - NYCHH Transfer Order Emails Owed". `"alarm":"HIGH"` or `"status":"error"` → Offline + incident; `"alarm":"OK"` → Online. Not Received In Minutes **90** — add only after the first scheduled push.
36
+
37
+ ## Decisions
38
+ - `owedHours` 24 to start; cutoff 2026-10-15 (developer).
39
+ - **Held-for-asset-tag rows still alarm HIGH** (ticket asked for it); payload counts them apart so the incident shows which kind.
40
+ - **Dropped "NetSuite stage shipped but no tracking"** — never seen (Skyler). The reverse (tracking set, stage not shipped) is already covered by the tracking-based Shipped email.
41
+ - Owed test is **copied, not shared** with `TransferOrderEmails.php`. Change both together.
42
+
43
+ ## Gotchas
44
+ - **Do not use the exception stamp after packed is sent.** `c_dtTransferOrderPackedExceptionSent` is never cleared, so once tracking makes Shipped owed it would false-alarm at once (CTO review). `ItemFulfillments_TrackingNumbers` has no timestamp, hence `dtUpdated`.
45
+ - Accepted risk: tracking that lands without bumping the IF row leaves an old `dtUpdated`; the row can flap HIGH once if the hourly run beats the `*/5` backstop.
46
+ - Local test 2026-10-16 (POST to local worker2 vhost): OK, HIGH, HIGH-held and error all reached OneUptime; incident opened and auto-resolved.
47
+ - Shares `Monitor/Nychh.php` with TRUE-82760 (`AssetTagBackfill`) — merge TRUE-82760 first. See the merge gotcha in the [monitor pattern doc](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md).
48
+
49
+ ## Change history
50
+ - 2026-10-16 — Created (TRUE-82758); pushed, not deployed. (mhammontree)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-10-09
10
- owners: ["bala", "rgirish", "jcardinal"]
9
+ updated: 2026-10-16
10
+ owners: ["bala", "rgirish", "jcardinal", "mhammontree"]
11
11
  files:
12
12
  - worker2/Worker/Client/Nychh/TransferOrderEmails.php
13
13
  - _underscore/Model/Nychh/TransferOrder.php
@@ -31,6 +31,7 @@ related:
31
31
  - ../../../2.0/apps/worker2/features/sandbox-dev-worker-environments.md
32
32
  - ../profile.md
33
33
  - ../../../1.0/apps/library/features/carrier-detection-from-tracking-number.md
34
+ - ./oneuptime-transfer-order-emails-owed-monitor.md
34
35
  ---
35
36
 
36
37
  ## Summary
@@ -160,6 +161,7 @@ PUT follows.** Fulfillment **2704** in production: header row at 20:33:09, lines
160
161
  - It also picks up any send that threw (the marker was never written).
161
162
  - Caps at **25 of each kind per run** (`MAXIMUM_EMAILS_PER_RUN`).
162
163
  - **It is the real sender of Shipped** — see below.
164
+ - Alarm when a Packed/Shipped email stays owed > 24h: [emails owed monitor](./oneuptime-transfer-order-emails-owed-monitor.md). It copies this cron's owed test — change both together.
163
165
 
164
166
  ## ⚠ Shipped = tracking number, not the NetSuite stage (2026-10-05)
165
167
 
@@ -415,6 +417,7 @@ loaded out of the dbchanges2 file so an unresolved `{placeholder}` fails a test.
415
417
  unescaped values and both detail-box defects were all found there, not in production.
416
418
 
417
419
  ## Change history
420
+ - 2026-10-16 — Linked the emails-owed monitor (TRUE-82758). (mhammontree)
418
421
  - 2026-10-09 — **New York Minute shows its logo, and NON_SERIALIZED lines are ignored by the asset-tag
419
422
  reads** (TRUE-82527). `CARRIER_LOGO_URL_BY_CARRIER_NAME` gained `NEW YORK MINUTE` (no tracking site,
420
423
  so logo + plain number, no link), paired with `Client_Nychh/2026-10-09 - NewYorkMinuteShippingCarrier.sql`
@@ -19,7 +19,7 @@ project: _Underscore
19
19
  client: nychh
20
20
  type: profile
21
21
  status: active
22
- updated: 2026-10-15
22
+ updated: 2026-10-16
23
23
  owners: [jcardinal, apeterson, bala, akhokhani, mhammontree]
24
24
  files:
25
25
  - dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
@@ -57,6 +57,8 @@ related:
57
57
  - ../../2.0/apps/toga25-supply/features/transfer-orders-page.md
58
58
  - ./features/location-shipping-addresses.md
59
59
  - ./features/oneuptime-asset-tag-backfill-monitor.md
60
+ - ./features/oneuptime-transfer-order-emails-owed-monitor.md
61
+ - ./features/transfer-order-notification-emails.md
60
62
  ---
61
63
 
62
64
  NYC Health & Hospitals (NYCHH) client profile — open for supply-chain / TransferOrder / NetSuite-sync work on prod schema `Client_Nychh`.
@@ -70,7 +72,7 @@ TOGA 2.0 client on `_underscore`, prod schema `Client_Nychh`. Supply-chain / ful
70
72
 
71
73
  ## Key features (this client)
72
74
  - **ITEM_FULFILLMENTS froze `1-RUNNING` 2026-09-30 → 2026-10-06 (Event issue 483) — fixed + prod-verified 2026-10-06 (library).** NS order 4512137 (#165198, stocking) existed twice: stale TransferOrder 508 (pre-2026-09-03 stocking rule) + SalesOrder 168. IF reconcile tried to delete TO line 7409 that an IA line linked to → FK 1451. New reverse retire (TO→SO) + IA-link move before IF-line deletes; TO 508 retired, cursor past `4605943`. Remaining doubles: #233366 (SO 172 / TO 1811 — retires when its IF 2392 syncs), #290752 (SO 167 / TO 1958, no IFs — waits for SALES_ORDERS feed). See [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
73
- - **Transfer-order "Order Shipped" email fires on the first tracking number, not the NetSuite shipped stage (2026-10-05, prod).** Warehouse marks shipped after delivery. NYCHH is the only client emailing off the NS shipped stage. See [TO notification emails](./features/transfer-order-notification-emails.md).
75
+ - **Transfer-order "Order Shipped" email fires on the first tracking number, not the NetSuite shipped stage (2026-10-05, prod).** Warehouse marks shipped after delivery. NYCHH is the only client emailing off the NS shipped stage. See [TO notification emails](./features/transfer-order-notification-emails.md). Alarm when one stays owed > 24h: [emails owed monitor](./features/oneuptime-transfer-order-emails-owed-monitor.md) (TRUE-82758, not deployed 2026-10-16).
74
76
  - **Prod NetSuite feeds (2026-10-02):** only ITEM_FULFILLMENTS + INVENTORY_ADJUSTMENTS run (worker `511a7fd9`, 2026-09-30), cursors rewound to 2022/2023.
75
77
  - **TO destination + own ship-to (2026-10-02, library `8f974d93`, _underscore `6839dd3e`).** NetSuite-entered TOs landed on catch-all Location 2 (text match failed on Queens hyphens + multi-address sites). Destination now = mapped NetSuite address-book entry → text match → Location 2; TOs own `shipToAddressId`; Location auto-create uses default shipping. Mapping: 61 entries → 18 sites. See [TO import](./features/netsuite-transfer-order-import.md), [address-book mapping](../../1.0/apps/worker/workflows/netsuite-addressbook-location-mapping.md).
76
78
  - **TO↔customer-PO line matching fixed (2026-10-02, `1bc4de52`).** Old matcher picked unstocked duplicate PO lines and created empty PO lines; now ranks stocked lines and allows reuse. 161 TOs re-processed; 86 bad links over 62 TOs remained at last measure (re-process result pending). See [reconcile](../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md).
@@ -122,6 +124,7 @@ TOGA 2.0 client on `_underscore`, prod schema `Client_Nychh`. Supply-chain / ful
122
124
  - **PO links are UPSTREAM (`PurchaseOrders_SalesOrders`), not downstream.** The downstream `sales-order-purchase-orders` route returns an empty array for NYCHH by design, and the shared `_purchaseOrders` field (downstream-only) resolves to `null` for this tenant — open gap. See [PO Number direction](./features/po-number-upstream-direction.md).
123
125
 
124
126
  ## Notes
127
+ - **1.0 cron "Send NYCHH Sales Orders" sends nothing (2026-10-15, TRUE-82761, parked).** `worker/crons/toga2/nychh/send_sales_order.php` (`*/5`) emails each `SalesOrders` row with `c_dtTransmittedToNYCHH IS NULL` as an xlsx to the warehouse. In prod all 125 rows have it NULL and none has `createdByUserId` (all NetSuite-imported), so its `INNER JOIN Users ON createdByUserId` drops every row. Supply orders are now `TransferOrders`. A monitor on that column would be always HIGH or always OK — don't build one. Open: ask Jelena if the transmission is still used.
125
128
  - **`Items.title` is NULL on most NYCHH rows — `description` is the populated label field.** Confirmed 2026-09-03 on client-sandbox (a `transfer-order-items` line returned `"title": null` with a real `partNumber`). Every screen that labels an item for this tenant needs the `title || description` chain — the Transfer Order record modal and the Create modal's item picker both carry it. The real fix is populating `Items.title`.
126
129
  - **Role ids in `Client_Nychh` (verified 2026-09-01):** `1 = Base`, `2 = Developer`, `3 = API`. Role ids differ per client DB — resolve by **name** in migrations, never hard-code these.
127
130
  - **The Agilant API (`Client_Nychh.Apis` id 1) holds BOTH Base (1) and API (3) via `Apis_Roles` (2026-10-06).** Record ACL = union, so checking only the API role misleads. Base grants full CRUD on 28/312/313 (ItemFulfillments, TransferOrders, TransferOrderItems); API role has no DELETE on 312/313 and no row for 28. Interceptors: record 312 has POST/POST + POST/PUT only (none on DELETE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1049",
3
+ "version": "1.0.1051",
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",