toga-ai 1.0.932 → 1.0.934

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.
@@ -21,6 +21,7 @@
21
21
  | [NetSuite File Cabinet Content Retrieval via RESTlet (fetchInvoiceFile)](features/netsuite-filecabinet-restlet.md) | How 1.0 pulls NetSuite File Cabinet binary content (invoice PDFs) via a SuiteScript RESTlet (`fetchInvoiceFile`); open when fetching NetSuite file bytes over RE |
22
22
  | [assetType from NetSuite itemtype during Item Sync (opt-in per client)](features/netsuite-item-assettype-sync.md) | Stamp `Items.assetTypeId` from NetSuite `item.itemtype` during item sync (opt-in per client, fill-NULL-only); open before enabling asset-type mapping for a clie |
23
23
  | [NetSuite classification → ItemClasses sync (opt-in per client)](features/netsuite-item-class-sync.md) | `App_Api_Toga2::getCreateItemClass()` mirrors NetSuite's **classification tree** into the client's `ItemClasses` table and stamps `Items.itemClassId`. |
24
+ | [Items.description from the NetSuite item record (Sales Description), refreshed every sync](features/netsuite-item-description-sync.md) | How the NetSuite sync sets and refreshes `Items.description` (all NetSuite clients); open when a toga item description is blank, stale, or differs from NetSuite |
24
25
  | [isFulfillable from NetSuite during Item Sync (Phase 1)](features/netsuite-item-isfulfillable-sync.md) | 1.0 Phase-1 stamping of the NetSuite `isfulfillable` flag onto the Agilant source item during sync; open before changing fulfillability sync or a client overrid |
25
26
  | [NetSuite SuiteQL/REST API Reference](features/netsuite-suiteql-api-reference.md) | Working reference for the Agilant NetSuite REST/SuiteQL integration (`App_Api_Netsuite_Rest`): auth, SuiteQL mechanics, table schemas, type/status codes, perfor |
26
27
  | [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | Non-obvious NetSuite SuiteQL/REST field semantics behind `App_Api_Netsuite_Rest` shims (signs, `tl.id`==line, `iscogs`, ShipItem cost, `createdFrom`, shim-shape |
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: Items.description from the NetSuite item record (Sales Description), refreshed every sync
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-01
10
+ owners: ["rgirish"]
11
+ files:
12
+ - library/app/api/toga2.php
13
+ - library/app/api/netsuite/rest.php
14
+ - worker/crons/toga2/netsuite/common_sync_togasupply.php
15
+ related:
16
+ - netsuite-item-class-sync.md
17
+ - netsuite-item-assettype-sync.md
18
+ - netsuite-item-isfulfillable-sync.md
19
+ - toga2-api-client-and-bridge.md
20
+ - ../../worker/features/netsuite-togasupply-per-client-sync.md
21
+ ---
22
+
23
+ How the NetSuite sync sets and refreshes `Items.description` (all NetSuite clients); open when a toga item description is blank, stale, or differs from NetSuite.
24
+
25
+ ## Summary
26
+ - Source = NetSuite **item record** Sales Description: SuiteQL `item.description`, fallback `item.purchasedescription`. Not the transaction line memo.
27
+ - Refreshed every time the sync touches an item or unit, for **all** NetSuite clients (no opt-in flag).
28
+ - Before 2026-10-01: set once at item create from the line memo, never updated; blank when the item was first seen on an item fulfillment or a memo-less line.
29
+
30
+ ## How it works
31
+ - `App_Api_Netsuite_Rest::fetchItemFlagsByIds()` selects `description, purchasedescription` and sets `$shim->description`.
32
+ - `App_Api_Toga2::getNetsuiteItemDescription()` — item-record text, else line text, cut to `MAX_LENGTH_ITEM_DESCRIPTION` (255, the column size).
33
+ - `App_Api_Toga2::refreshItemDescriptionFromNetsuite()` — PUT `/items/{uuid}` `description` only when NetSuite's value is non-empty and different; updates the caller's cached item so repeat lines do not PUT again.
34
+ - Call sites in `toga2.php`: `getCreateItem` (existing + create), `syncItemFulfillmentFromNetsuite` (line not on the SO), `syncInventoryAdjustmentFromNetsuite`, `syncItemReceiptFromNetsuite` (serialized + non-serialized), `syncTransferOrderFromNetsuite` (one batched flags lookup).
35
+ - `common_sync_togasupply.php` shared `GET /items` lookup requests `description`.
36
+
37
+ ### Rules
38
+ - Never blanks a stored description.
39
+ - Throws if the caller's item GET did not request `description` (fail loud, same pattern as itemClass) — otherwise every item would be re-written every run.
40
+ - Needs the API-role write grant on `Items.description` (RecordFields 109, roleId 3). Checked present in Elite, Adyen, Prudential, Growrk, Nychh.
41
+
42
+ ## Gotchas
43
+ - **⚠ `library` and `worker` MUST deploy together.** Library alone (2026-10-01 prod) made every client's SO sync throw `getCreateItem: the caller's item GET must request 'description'`. Tell-tale: trace `common_sync_togasupply.php:825` = old worker, `:827` = new — PHP reports the LAST argument line of a multi-line call.
44
+ - Items whose only NetSuite records will never resync keep the old text. Fix by hand (done for Elite items 228, 80, 89, 90). Elite item 125 is blank in NetSuite too — nothing to copy.
45
+
46
+ ## Change history
47
+ - 2026-10-01 — Built: description from NetSuite item record, refreshed on every touch, all clients; deploy-together incident; Elite one-time backfill. (rgirish)
@@ -23,7 +23,7 @@ related:
23
23
  - ../../worker/features/netsuite-togasupply-per-client-sync.md
24
24
  - ../architecture.md
25
25
  - ./error-capture-1-0.md
26
- - ../../../clients/wje/features/oneuptime-ticket-writes-monitor.md
26
+ - ../../../../clients/wje/features/oneuptime-monitors.md
27
27
  - ../../../clients/elite/features/togadesk-service-request-intake.md
28
28
  ---
29
29
 
@@ -147,7 +147,7 @@ A large bi-directional sync gated by **two checkpoint parameters** stored in 2.0
147
147
  Signature takes `$integrationType` (`TICKETS` or `REPAIR_ORDERS`) plus **seven independent enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
148
148
  - **2.0 → TOGaDesk** (`$isEnabledToga2ToTogadeskIntegration`): pages 2.0 `/tickets` and `/ticket-notes` changed since the checkpoint; routes each to `syncToga2TicketIntoTogadesk1{RepairOrder|Ticket}` by `$integrationType`. Per-ticket exceptions are caught and sent to Sentry so one bad ticket doesn't abort the batch.
149
149
  - **TOGaDesk → 2.0** (`$isEnabledTogadeskToToga2Integration`): queries the 1.0 `db_togadesk` DB directly (SQL over `repair_orders`/`repair_order_history` or `tickets`/`tickets_replies`/`comments`, using `GREATEST(...)` of reply/comment timestamps) for the client's department(s) — only the ones the cron passes (`$monitorTogadeskDepartmentIds`) — then calls `syncTogaDesk1{RepairOrder|Ticket}IntoToga2Ticket($id)`.
150
- - **Link = `referenceId`.** `tickets.referenceId` = 2.0 ticket uuid; `tickets_replies.referenceId` / `comments.referenceId` = 2.0 ticket-note uuid. Written back **only after a successful POST**. Rows created by the 2.0 → 1.0 direction already carry it. So NULL/`''` `referenceId` = not (yet) in 2.0 — the basis of the [WJE ticket-writes monitor](../../../clients/wje/features/oneuptime-ticket-writes-monitor.md).
150
+ - **Link = `referenceId`.** `tickets.referenceId` = 2.0 ticket uuid; `tickets_replies.referenceId` / `comments.referenceId` = 2.0 ticket-note uuid. Written back **only after a successful POST**. Rows created by the 2.0 → 1.0 direction already carry it. So NULL/`''` `referenceId` = not (yet) in 2.0 — the basis of the [WJE ticket-writes monitor](../../../../clients/wje/features/oneuptime-monitors.md).
151
151
  - A reply with a blank message falls back to `newstatus`; if still PHP-empty (NULL, `''`, `'0'`) it is skipped and never gets a `referenceId`. Comments are always sent.
152
152
  - Five further one-way 2.0 → TOGaDesk sub-syncs, each its own flag: Contacts→People, Items/Units→Assets, Predefined-Replies→TicketsPr, Ticket-Teams→Groups, Ticket-Categories→Category/Subcategory/Items.
153
153
  - Finishes by PUT-ing both checkpoint parameters back to 2.0.
@@ -34,6 +34,7 @@ related:
34
34
  - ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
35
35
  - ../../../clients/elite/features/netsuite-togasupply-sync.md
36
36
  - ../../library/features/netsuite-item-class-sync.md
37
+ - ../../library/features/netsuite-item-description-sync.md
37
38
  - ../../../clients/compass-usa/features/sales-order-line-renumbering.md
38
39
  - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
39
40
  - ../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
@@ -1973,6 +1974,18 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
1973
1974
  invoice against a duplicated SO it will false-throw the same way, and **their duplicates have a DIFFERENT
1974
1975
  cause than Compass's Office-Depot dual-source.** Also a data-quality question: why do six non-Compass
1975
1976
  clients have duplicate SalesOrders rows at all.
1977
+ - **⚠ "refusing to import invoice … billed sales order not imported" can mean the SO is OLDER than the
1978
+ SO cursor (Adyen, 2026-10-01, issue 910).** Invoices from 2026-07-02 on billed 10 SOs last-modified
1979
+ 2025-11-04 → 2026-06-15, but `NETSUITE_LAST_SYNC_CURSOR_SALES_ORDERS` was 2026-07-31, so the SO sync
1980
+ never pulls them. Fix: wait for SO mode `IDLE`, then set the SO cursor (Eastern) before the oldest
1981
+ billed SO's lastmod (here `2025-11-01 00:00:00|0`, ~117 SOs re-pull). To see which invoice/SO it fails
1982
+ on now, read `Logs_<Tenant>.Api` `queryString` — `Logs.Issue` keeps the first message only.
1983
+ - **⚠ The receipt sync never rebuilds lines on an EXISTING receipt** — `itemReceiptItems` is sent on POST
1984
+ only. A receipt whose PO lines were deleted and re-created keeps `ItemReceiptItems.purchaseOrderItemId
1985
+ NULL` (and can gain duplicate unlinked rows), which skews On Hand. Detect:
1986
+ `ItemReceiptItems WHERE purchaseOrderItemId IS NULL AND transferOrderItemId IS NULL`. Fix: delete
1987
+ those receipts and rewind the IR cursor so they re-import (Elite 2026-10-01,
1988
+ `Client_Elite/2026-10-01b`).
1976
1989
  - **⚠ ROOT CAUSE of the duplicate `Items`: the inventory-adjustment item-create OMITTED the catalog
1977
1990
  (fixed 2026-08-31).** `getCreateItem` (`toga2.php:6233`) and the fulfillment create (~4951) stamp
1978
1991
  `'catalog' => ['name' => 'Agilant']`; `syncInventoryAdjustmentFromNetsuite`'s item-create did **not** →
@@ -2065,6 +2078,7 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
2065
2078
  [NetSuite Sync Alert Monitor](../../library/features/netsuite-sync-alert-monitor.md).
2066
2079
 
2067
2080
  ## Change history
2081
+ - 2026-10-01 — `Items.description` now from the NetSuite item record, refreshed every touch ([doc](../../library/features/netsuite-item-description-sync.md); library + worker deploy together). Gotchas: invoice billing an SO older than the SO cursor (Adyen); receipt lines never rebuilt on an existing receipt (Elite). (rgirish)
2068
2082
  - 2026-10-01 — NYCHH diagnosis traps recorded: the wrapper is gated to **once a day** by
2069
2083
  `cronInitialization(86400)` despite its `*/5` schedule entry, and the live cursors/modes live in
2070
2084
  `Client_Nychh.Parameters` — the `NETSUITE_HH_*` keys in `Core` are stale (May 2026) leftovers.
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
10
- owners: [jcardinal, mhammontree]
9
+ updated: 2026-10-01
10
+ owners: [jcardinal, mhammontree, rgirish]
11
11
  files:
12
12
  - _underscore/Model/Client/User.php
13
13
  - worker2/Worker/Sync/ActiveDirectory.php
@@ -64,8 +64,10 @@ Northwell ServiceNow users arrive via api2 eBonding (`_Model_Northwell_Ticket` i
64
64
 
65
65
  - **⚠ A reconstructed `new _Model_Client_User($id)` does NOT carry `contactId`.** In `ActiveDirectory.php` the AD sync reads the existing `contactId` from the **bulk SELECT** and only creates a contact when it is truly missing. A naive `empty($user->contactId)` guard against a freshly reconstructed model reads null even when the row has a contact — and would **mint a duplicate contact every sync**. Trust the SELECT'd value, not the rehydrated model property.
66
66
  - **⚠ Set `primaryContactEmailAddressId` with a second server-side load+save, never a nested payload.** The nested singular-FK write re-parents (CREATE) or echoes null (UPDATE).
67
+ - **Adding a user by hand (dbchanges2 SQL): look up `ContactEmailAddresses` for an existing Contact first.** Sync-created Contacts often exist already (Elite: Contacts 29, 886, 1044 from the Elite sync, `Client_Elite/2026-10-01a`); link to them instead of minting a duplicate.
67
68
  - **Interceptor no-ops on a user with no email** — a no-email straggler gets no Contact from this path (the backfill migrations handle those name-only, per client). It is why the fan-out `NOT NULL` skips a client that still has a no-email residual (see [rerunnable-additive-inserts](../../dbchanges2/features/rerunnable-additive-inserts.md)).
68
69
 
69
70
  ## Change history
71
+ - 2026-10-01 — Hand-added users: link an existing Contact found via `ContactEmailAddresses` (Elite). (rgirish)
70
72
  - 2026-09-23 — Corrected the origin map: the Compass PEOPLE importer did **not** safely set `contactId` — it inserted `Users` first and linked afterwards, so the NOT NULL rollout broke it in prod (TRUE-82230). Added the write-order and `LAST_INSERT_ID()` gotchas for every raw-SQL creation path. (mhammontree)
71
73
  - 2026-09-21 — Initial. Decided `Client_*.Users.contactId` goes NOT NULL platform-wide (CTO Option B); built the shared `_Model_Client_User::postPost` interceptor (Core row: recordId 2, clientId+apiId NULL) to create+link a Contact for api2-created users; fixed the worker2 AD-sync and `cacheClickupUser` True paths; recorded the origin map and the reconstructed-model `contactId` gotcha. (jcardinal)
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [apeterson, ajean]
9
+ updated: 2026-10-01
10
+ owners: [apeterson, ajean, rgirish]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  - _underscore/String.php
@@ -26,16 +26,17 @@ How `/v2/auth/login` swaps to a cross-client (home) user's home client mid-reque
26
26
 
27
27
  ## How it works
28
28
 
29
- `_Component_Api_V2::execute()` (~L701-744):
30
- 1. The user is loaded **by email** from the current client's DB (~L705) — the client is resolved from `ClientEmailDomains`, **not** from any Bearer token.
29
+ `_Component_Api_V2::execute()` (~L762-812, re-checked 2026-10-01 — grep `homeClientUserId`, line numbers drift):
30
+ 1. The user is loaded **by email** from the current client's DB (~L762) — the client is resolved from `ClientEmailDomains`, **not** from any Bearer token.
31
31
  2. The submitted password is checked with `_String::passwordVerify()` → `password_verify()` (bcrypt) against the local user row (`_underscore/String.php` L563-576).
32
- 3. **If the password does NOT verify AND the loaded user row has a non-null `clientId` that differs from the current client**, the engine treats it as a **cross-client user**: it calls `registerClientDatabases($user->clientId)` and re-loads the *home* user via `homeClientUserId` (~L716-731). Authentication then completes against the home client.
32
+ 3. **If the password does NOT verify AND the loaded user row has a non-null `clientId` that differs from the current client**, the engine treats it as a **cross-client user**: it calls `registerClientDatabases($user->clientId)` and re-loads the *home* user via `homeClientUserId` (~L787-812). Authentication then completes against the home client.
33
33
 
34
34
  This is the same `homeClientUserId` bridge that [cross-client data retrieval](./cross-client-data-retrieval.md) uses to fan a home user across tenant DBs — here it governs which client a login actually authenticates against.
35
35
 
36
36
  ## Gotchas
37
37
 
38
38
  - **A login `EO-1 "Unknown database 'client_<x>'"` is the cross-client branch failing to open the home client's DB.** Observed: logging into **Elite** locally with `devteam@togatech.com` throws `EO-1 Unknown database 'client_true'`. That account is an internal TOGA (client `true` / `Client_True`) user shared into Elite via `homeClientUserId`; because its Elite-local password does not verify, the cross-client path tries to open `Client_True`, which does not exist in local dev. It works on the dev sandbox only because `Client_True` exists there.
39
+ - `devteam@togatech.com` in `Client_Elite`: `clientId` 1 (True), `homeClientUserId` 2 = `Client_True` devteam@goagilant.com (re-checked 2026-10-01).
39
40
  - **Local-dev workaround (never commit):** set the **local** `Client_<Client>` user's own bcrypt password hash to a known value so login succeeds on the **direct** path and never enters the cross-client branch. Generate with `_String::passwordHash()` / `password_hash($pw, PASSWORD_BCRYPT)`. A per-machine local fixup, not a code or data change to commit.
40
41
  - **The api2 dev server talks to `localhost` as `root` — sandbox DB edits are invisible to it.** Local stack traces show `mysqli_connect(["localhost","root",...])`. Any ACL/grant/password change must be applied to the **localhost** client DB, not a sandbox connection in a DB GUI, or the local API will never see it. (In DbGate: "Run" autocommits; a query-tab "unsaved" label refers to the unsaved `.sql` *script file*, not pending uncommitted data.) This exact confusion — a grant present in sandbox while the API read localhost — cost real debugging time.
41
42
  - **⚠ The domain lookup is `Core.ClientEmailDomains` at `V2.php:611-621`, and it only runs when the request host does not identify the client.** Confirmed 2026-09-04. Two follow-ons: an **editable `Users.email` is therefore a tenant-routing control**, and the session bind at `V2.php:762-765` attaches the session to whichever row carries the address (that path **does** filter `isActive = true`). Both matter to anything that writes an email — see [Users.email is a login identity](../../_underscore/features/user-email-as-identity-and-password-reset.md).
@@ -46,3 +47,6 @@ This is the same `homeClientUserId` bridge that [cross-client data retrieval](./
46
47
  - [Surface meta option](./surface-meta-option.md)
47
48
  - [Per-client database connections](../../_underscore/features/per-client-database-connections.md)
48
49
  - [Users.email as login identity & password reset](../../_underscore/features/user-email-as-identity-and-password-reset.md)
50
+
51
+ ## Change history
52
+ - 2026-10-01 — Re-checked V2.php line refs (~L762-812); worked example: devteam@togatech.com in Elite = clientId 1 (True), homeClientUserId 2. (rgirish)
@@ -33,7 +33,7 @@ related:
33
33
  - ../../../clients/staples/features/oneuptime-sftp-folder-monitors.md
34
34
  - ../../../clients/compass-usa/features/oneuptime-ma-refresh-order-monitor.md
35
35
  - ../../../clients/compass-usa/features/oneuptime-netsuite-duplicate-sales-orders-monitor.md
36
- - ../../../clients/wje/features/oneuptime-ticket-writes-monitor.md
36
+ - ../../../../clients/wje/features/oneuptime-monitors.md
37
37
  ---
38
38
 
39
39
  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.
@@ -309,7 +309,7 @@ This is about `Common` only — **worker2 does read other legacy DBs.** `worker2
309
309
  `[database1]` config group), already used in prod by `Worker/Sync/SalesOrderStatus.php` and
310
310
  `Worker/Client/Aig/ClosedClaims.php`. A 2.0 monitor over TOGaDesk data is possible and preferred (it
311
311
  runs outside the 1.0 worker it watches) — e.g. the
312
- [WJE ticket-writes monitor](../../../clients/wje/features/oneuptime-ticket-writes-monitor.md).
312
+ [WJE monitor suite](../../../../clients/wje/features/oneuptime-monitors.md).
313
313
 
314
314
  ### Match the lateness criterion to the flow's real time-scale
315
315
 
@@ -345,6 +345,11 @@ pattern (first instance: `EmailedAsnImport()`, which intermittently paged on a t
345
345
  - **Catch `\Throwable`, not `Exception`,** in the loop so a non-`Exception` `Error` (e.g. from the
346
346
  php-imap2 C-extension calls) can't escape the method, skip the socket-timeout restore, or break this
347
347
  worker action's "always return a string" invariant.
348
+ - **A delayed re-read must call `_Database::resetQueryCache()` first** — an identical second `_Query`
349
+ in the same run is served from the query cache, so the "retry" sees the first result. Use a
350
+ sleep-then-re-read when a row can legitimately arrive already "old" (e.g. WJE
351
+ `TicketSyncToTogadesk`: a note's `dtStamp` is its Freshservice time) — see
352
+ [WJE monitor suite](../../../../clients/wje/features/oneuptime-monitors.md).
348
353
 
349
354
  ### Two kinds of monitor — liveness probes and log-scrapers are complements, not duplicates
350
355
 
@@ -568,5 +573,6 @@ production deploy.**
568
573
  - [OneUptime 1.0 worker uptime monitoring](../../../1.0/apps/worker/features/oneuptime-worker-uptime-monitoring.md) — the 1.0 push-heartbeat predecessor
569
574
 
570
575
  ## Change history
576
+ - 2026-10-01 — Added the `resetQueryCache()` re-read trap to the in-run retry pattern; WJE link now points at the 3-monitor suite. (mhammontree)
571
577
  - 2026-10-01 — Clarified worker2 reads `TOGaDeskSupport` (`DB_TOGADESK1`) — only `Common` is unreachable; added `Monitor/Wje`. (mhammontree)
572
578
  - 2026-09-25 — Added `NetsuiteDuplicateSalesOrders`, the first monitor that queries NetSuite (SuiteQL) instead of a Toga DB. (rgirish)
@@ -28,7 +28,7 @@ files:
28
28
  - dbchanges2/Client_Wje/2024-10-04 - WjeOnboarding.sql
29
29
  related:
30
30
  - clients/wje/profile.md
31
- - clients/wje/features/oneuptime-ticket-writes-monitor.md
31
+ - clients/wje/features/oneuptime-monitors.md
32
32
  - 2.0/apps/worker2/features/elite-freshservice-sync.md
33
33
  - 1.0/apps/library/features/toga2-api-client-and-bridge.md
34
34
  - 2.0/apps/worker2/features/startech-webhook-handler.md
@@ -64,7 +64,7 @@ a **daily batch import** of supporting records. A separate 2-minute cron bridges
64
64
  `crons/toga2/wje/sync_togasupply_wje.php` (2-min Togadesk bridge),
65
65
  `crons/notifications/reports/wje/{wje_monthly_report,wje_quarterly_report,wje_common}.php`.
66
66
  - **Models:** `_underscore/Model/Wje/*` (8 classes, see Data model).
67
- - **Monitoring:** `library/app/systemmonitor/wje.php` (`App_SystemMonitor_WJE`, Freshservice/webhook errors); `worker2/Worker/Monitor/Wje.php` ([TOGaDesk → 2.0 write monitor](../../../clients/wje/features/oneuptime-ticket-writes-monitor.md)).
67
+ - **Monitoring:** `library/app/systemmonitor/wje.php` (`App_SystemMonitor_WJE`, Freshservice/webhook errors); `worker2/Worker/Monitor/Wje.php` ([WJE OneUptime monitor suite](../../../../clients/wje/features/oneuptime-monitors.md): webhook → 2.0, TOGaDesk → 2.0, 2.0 → TOGaDesk).
68
68
 
69
69
  ### Real-time webhook (`_Worker_Wje::Webhook`)
70
70
 
@@ -124,7 +124,7 @@ Hardcoded IDs: `CLIENT_UUID_WJE=c2f5180f-…`, `API_UUID_WJE=4750650f-…` (and
124
124
 
125
125
  - **W1** Payload is wrapped under `freshdesk_webhook`, not top-level. Decode with `json_decode($p, true)` then cast.
126
126
  - **W2** Webhook body is thin — `ticket_category`/`group_id` often null; the handler re-fetches `/tickets/{id_numeric}`. Don't trust the webhook body for category/group.
127
- - **W3** Two id forms: `ticket_id` (string "INC-46687", stored in `c_wjeTicketId`) vs `id_numeric` (46687, used in API calls). Mixing them → 404s / missed matches.
127
+ - **W3** Two id forms: `ticket_id` (string `"INC-66452"`) → stored in `Tickets.number`; `id_numeric` (`66452`) → stored in `c_wjeTicketId` and used in API calls (`Worker/Wje.php` ~L563-564). Mixing them → 404s / missed matches.
128
128
  - **W4** Webhook resolves category by **name** against TOGA `TicketCategories`; the **daily import** is what populates `c_wjeCategoryId`. A WJE rename before the import runs breaks name matching — check the import cron first.
129
129
  - **W5** Units keyed by asset tag `ASSET-{display_id}`, linked via `c_wjeAssetId`; asset webhook falls back to email matching when `c_wjeUserId` is absent.
130
130
  - **W6** Asset import catches per-asset errors and continues; if WJE revokes `/assets` API access it silently skips assets — check logs.
@@ -134,9 +134,10 @@ Hardcoded IDs: `CLIENT_UUID_WJE=c2f5180f-…`, `API_UUID_WJE=4750650f-…` (and
134
134
  - **W10** Lazy-transaction rule: write back `c_wje*` identifiers via direct model `->save()`, not an internal API round trip inside an open transaction (cf. Startech G4).
135
135
  - **W11** `json_decode($s, true)` + top-level `(object)` cast leaves nested fields as arrays — cast each level you traverse.
136
136
  - **W12** TicketStages use Freshservice status codes (Open=2 … Closed=5); status sync depends on those matching live Freshservice statuses.
137
- - Secrets (`API_SECRET_WJE`, the Freshservice password) are hardcoded in source — read them from the file when needed; never copy the values into docs or commits.
137
+ - **Open item — hardcoded credentials (parked 2026-10-01, separate ticket needed):** `API_SECRET_WJE` is a class const literal in `worker2/Worker/Wje.php`; the Freshservice API key is a literal in `_underscore/Component/Api/Wje/Wje.php` `initialize()` (plus the 1.0 copy, W9). Fix = move to config + rotate. Never copy the values into docs or commits.
138
138
 
139
139
  ## Change history
140
+ - 2026-10-01 — Corrected W3 (`c_wjeTicketId` = `id_numeric`, `INC-…` → `number`); recorded hardcoded-credential locations as an open item; linked the monitor suite. (mhammontree)
140
141
  - 2026-10-01 — Corrected dept 290 (not synced 1.0 → 2.0); linked the new ticket-writes monitor. (mhammontree)
141
142
 
142
143
  ## Related
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 30 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 31 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 41 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)
@@ -54,6 +54,9 @@ Adyen live TOGa Supply client (`Client_Adyen`) on the 1.0 NetSuite→TOGa Supply
54
54
 
55
55
  - **⚠ Transfer-order mode not independently verified against Adyen's NetSuite account.** The mode preserves the behavior Adyen had under the old implicit default; nobody has confirmed with the client that $0 + hold is the right signal *for Adyen*. Check before trusting the split.
56
56
 
57
+ - **⚠ Invoice sync stuck 2026-10-01 (issue 910, "billed sales order not imported")** — the billed SOs are older than the SO cursor. Fix handed to developer: SO cursor → `2025-11-01 00:00:00|0` once SO mode is `IDLE`. Details on the [engine doc](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
58
+ - **⚠ OPEN: `ShippingCarriers` has only FedEx + UPS.** Item-fulfillment tracking POST fails `EV-12` (issue 925) — likely a USPS/DHL carrier. Not fixed. See [shipping carriers](../../2.0/apps/_underscore/features/shipping-carriers-and-accounts.md).
59
+
57
60
  ### Environments — where `Client_Adyen` lives, and which one "beta" means
58
61
 
59
62
  - `toga_find_schema` returns the tenant DB in exactly **three** environments: **prod, dev-sandbox, client-sandbox**. There is **no `Client_Adyen` in `client-beta`**.
@@ -6,12 +6,13 @@ project: _Underscore
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-30
9
+ updated: 2026-10-01
10
10
  owners: [rgirish]
11
11
  files:
12
12
  - _underscore/Model/Elite/Item.php
13
13
  - _underscore/Model/Client/Item.php
14
14
  - worker/crons/toga2/netsuite/sync_togasupply_elite.php
15
+ - dbchanges2/Client_Elite/2026-10-01b - EliteReimportReceiptsWithUnlinkedLines.sql
15
16
  related:
16
17
  - ../profile.md
17
18
  - ./netsuite-togasupply-sync.md
@@ -43,7 +44,13 @@ How Elite's `_qtyOnHand` / `_qtyAvailable` are computed (`_Model_Elite_Item`) so
43
44
  - **Inventory page filter** is data, in `Client_Elite/2026-09-28b`: hides `SVC-`/`SVT-`/`LIC-`/`CON-` parts, AppleCare, service item classes and drop-ship-only items; shows only rows with nonzero on hand or available.
44
45
  - **Known gap: Docking 1 is below NetSuite** — RMA return receipts (created from a Return Authorization) are not imported by the sync, so returned stock is missing. Open.
45
46
  - Owned-warehouse scope is Elite-only today; other clients have fake "bay" warehouses (see [Elite sync](./netsuite-togasupply-sync.md)).
47
+ - **⚠ OPEN (developer to decide): Qty On Order is ALWAYS 0.** `Elite/Item.php` overrides `_qtyOrdered = _qtyReceived`, and `Client/Item.php` `_qtyOnOrder = Ordered − Received`. So "Qty Ordered" on the Inventory page means units received on POs. NetSuite has real on-order qty.
48
+ - **On Hand off by 1 = receipt lines with NULL `purchaseOrderItemId`** (PO lines deleted + re-created; sync never rebuilds lines on an existing receipt). Fix = delete the receipts + rewind the IR cursor — `Client_Elite/2026-10-01b` (receipts 6947587, 6957582, 7043074, 7068990; written, not yet run). Detection on the [engine doc](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
49
+ - **Part numbers match case-insensitively** — NS items 1505560 "iPhone 14" and 1505562 "IPhone 14" are ONE toga item. Sum both when comparing to NetSuite.
50
+ - **Compare to NetSuite** with SuiteQL `aggregateItemLocation` (location 196) for qty, `inventoryNumberLocation` for serials.
51
+ - **Client-facing guide** (Ordered, On Hand, Available, Fulfilled, Invoiced, On Order, worked example Docking Station 67 received + 100 adjusted − 15 SO − 69 TO = 83): https://claude.ai/artifact/2wrVUcTBesKEq2b2CsySKw. Artifact pages print badly — make the PDF with headless Chrome `--print-to-pdf` and `display:block` in `@media print`.
46
52
 
47
53
  ## Change history
54
+ - 2026-10-01 — Qty On Order always 0 (open); receipt-line On Hand fix `2026-10-01b`; case-insensitive part merge; NS compare queries; client guide link. (rgirish)
48
55
  - 2026-09-30 — New formula: owned-warehouse (`allowReceiving = 1`) STOCK SO lines + Transfer Order shipped/committed (`qtyCommittedTransferOrders`); verified vs NetSuite `aggregateitemlocation`. Inventory filter 2026-09-28b; RMA receipt gap recorded. (rgirish)
49
56
  - 2026-09-17 — Drop ship excluded from `_qtyOnHand` / `_qtyAvailable` only; no floor; NULL `fulfillmentType` not stock; base composites `self::` → `static::`. (rgirish)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-30
9
+ updated: 2026-10-01
10
10
  owners: ["snaredla", "jcardinal", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/sync_togasupply_elite.php
@@ -420,6 +420,14 @@ Set in `sync_togasupply_elite.php`; they feed the [inventory formula](./inventor
420
420
 
421
421
  ## Gotchas / known issues
422
422
 
423
+ - **Full NS↔toga recheck 2026-10-01: SO/IF/PO/Invoice/IA reconcile.** Expected differences (by design, not bugs):
424
+ - SOs closed in NS (all item lines closed, only Ship/Tax open, no related records) are deleted by the [closed-order rule](../../../1.0/apps/library/features/netsuite-closed-order-rule.md) (10 today).
425
+ - An invoice billed against a $0 hold SO (stored as a TransferOrder) is skipped — e.g. 6215693.
426
+ - Credit memos (`CustCred`) are stored in `Invoices`.
427
+ - IA 6326239 is a company-wide count at NS locations 4/5/6.
428
+ - Receipts at NS location 5 (New York) are excluded — `2026-09-28a` removed that Location (8 today).
429
+ - RMA return receipts are out of scope.
430
+ - **Users added by hand 2026-10-01** (`Client_Elite/2026-10-01a`): Base role, linked to the EXISTING sync-created Contacts (carry `c_eliteUserId`) — look up `ContactEmailAddresses` before creating a Contact. `devteam@togatech.com` here is a cross-client user (clientId 1 True, `homeClientUserId` 2) — see [cross-client login](../../../2.0/apps/api2/features/login-cross-client-user-resolution.md).
423
431
  - **Data fixes 2026-09-28/29 (`dbchanges2/Client_Elite/`):** `2026-09-28b` inventory filter, `28d`, `28e` (ACL + `allowReceiving = 1` on NS 196), `2026-09-29a - EliteReceiptLinkAndMissingSalesOrder.sql` (relinks receipt 58882 line to PO line 1765; inserts missing SO 289992). 7 closed-order shipments backfilled with `process_netsuite_records.php` after the [closed-order rule](../../../1.0/apps/library/features/netsuite-closed-order-rule.md) change.
424
432
  - **Open 2026-09-30:** Elite sync cron re-enabled locally in `worker/schedules/cron.worker.sync.json` — pending deploy. RMA return receipts are not imported (Docking 1 below NetSuite).
425
433
 
@@ -462,6 +470,7 @@ Set in `sync_togasupply_elite.php`; they feed the [inventory formula](./inventor
462
470
  rewinding `NETSUITE_LAST_SYNC_DATETIME_SALES_ORDERS`.
463
471
 
464
472
  ## Change history
473
+ - 2026-10-01 — Full NS↔toga recheck: list of by-design differences; hand-added users linked to existing Contacts; item descriptions now from NS item record ([doc](../../../1.0/apps/library/features/netsuite-item-description-sync.md)), items 228/80/89/90 backfilled by hand. (rgirish)
465
474
  - 2026-09-30 — Elite-only scope flags (owned-warehouse receipts, NS line location, skip TOs from unknown warehouse); NS 196 marked owned; receipt/SO data fixes; 7 closed-order shipments backfilled. (rgirish)
466
475
  - 2026-09-24 — TO↔PO links now via the two-direction reconcile; some upstream rows were vendor POs; SO downstream line links were being deleted on every PO re-sync (55 of 97 SO-linked vendor POs had none) — fixed. (jcardinal)
467
476
  - 2026-09-18 — **Stale-SalesOrder backfill is live: 218 → 187.** The retirement method needed
@@ -2,5 +2,5 @@
2
2
 
3
3
  | Doc | Framework | Summary |
4
4
  |-----|-----------|---------|
5
- | [WJE: TOGaDesk → TOGa 2 ticket-writes monitor (Monitor/Wje/TicketWritesToToga2)](features/oneuptime-ticket-writes-monitor.md) | 2.0 | worker2 OneUptime push monitor counting WJE TOGaDesk tickets/replies/comments never written to TOGa 2 (NULL referenceId); open when working on WJE bridge monito |
5
+ | [WJE: OneUptime monitor suite (Monitor/Wje — TicketWritesToToga2, TicketSyncToTogadesk, WebhookTicketSync)](features/oneuptime-monitors.md) | 2.0 | Three worker2 OneUptime push monitors on the WJE ticket flow (Freshservice webhook → TOGa 2, TOGaDesk → TOGa 2, TOGa 2 → TOGaDesk); open when working on WJE mon |
6
6
  | [Wiss, Janney, Elstner Associates, Inc.](profile.md) | 2.0 | WJE (Wiss, Janney, Elstner / "WJE IT") Freshservice help-desk client, single hardcoded tenant; open for the three sync paths, the TOGaDesk bridge ids/department |
@@ -0,0 +1,107 @@
1
+ ---
2
+ title: "WJE: OneUptime monitor suite (Monitor/Wje — TicketWritesToToga2, TicketSyncToTogadesk, WebhookTicketSync)"
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: wje
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-10-01
10
+ owners: ["mhammontree"]
11
+ files:
12
+ - worker2/Worker/Monitor/Wje.php
13
+ - worker2/Worker/Client/Wje.php
14
+ - dbchanges2/Core/2026-09-30b - Insert WJE Ticket Writes Monitor CronJob.sql
15
+ - dbchanges2/Core/2026-10-01a - Insert WJE Ticket Sync To TOGaDesk Monitor CronJob.sql
16
+ - dbchanges2/Core/2026-10-01b - Insert WJE Freshservice Webhook Sync Monitor CronJob.sql
17
+ - worker/crons/toga2/wje/sync_togasupply_wje.php
18
+ - library/app/api/toga2.php
19
+ related:
20
+ - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
21
+ - ../../../1.0/apps/library/features/toga2-api-client-and-bridge.md
22
+ - ../../../2.0/apps/worker2/features/wje-freshservice-sync.md
23
+ - ../profile.md
24
+ ---
25
+ Three worker2 OneUptime push monitors on the WJE ticket flow (Freshservice webhook → TOGa 2, TOGaDesk → TOGa 2, TOGa 2 → TOGaDesk); open when working on WJE monitoring, its parameters, activation, or clearing a stuck ticket.
26
+
27
+ ## Summary
28
+ `_Worker_Monitor_Wje` (`worker2/Worker/Monitor/Wje.php`) holds three actions, one per hop. All follow the shared [OneUptime push-metric pattern](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md) — payload contract, criteria, provisioning live there.
29
+
30
+ | Action | Ticket | Hop | OneUptime monitor name | Cron |
31
+ |---|---|---|---|---|
32
+ | `Monitor/Wje/WebhookTicketSync` | TRUE-80520 | Freshservice webhook → `Client_Wje.Tickets` | "WJE Freshservice Webhook Sync" | 5 min, max 120s |
33
+ | `Monitor/Wje/TicketWritesToToga2` | TRUE-80523 | TOGaDesk dept 269 → TOGa 2 | "WJE Ticket Writes to TOGa 2" | 5 min |
34
+ | `Monitor/Wje/TicketSyncToTogadesk` | TRUE-80521 | TOGa 2 → TOGaDesk | "WJE Ticket Sync to TOGaDesk" | 5 min, max 300s |
35
+
36
+ - **Status 2026-10-01:** all three committed on the one shared branch `TRUE-80523` (worker2, dbchanges2, worker comment), **not deployed**. Seeds ship `isActive = 0`. Push URLs are credentials — never record them.
37
+ - DBs: `TOGaDeskSupport` via `_underscore::DB_TOGADESK1` (legacy); `Client_Wje` via `_Worker_Client_Wje::DB_CLIENT_WJE` (`worker2/Worker/Client/Wje.php`, mirrors `Client/Prudential.php`, registered in `initialize()`); `Logs` via `_underscore::DB_LOGS` (registered per job in worker2 `Controller/Index.php`). Each query stays in one DB family; compare in PHP.
38
+ - DB failures go through `reportQueryFailure(array $params)` → `status: error` with **only** `errorClass` + `errorNumber` (the message can hold SQL, host, user).
39
+
40
+ ## How it works
41
+
42
+ ### Why backlogs, not checkpoint freshness
43
+ The TOGaDesk bridge catches a per-ticket exception → Sentry only, and **still advances its checkpoint** (`TOGA_LAST_TICKET_INTEGRATION_DATETIME` / `TOGADESK_LAST_TICKET_INTEGRATION_DATETIME`). A failed ticket is never retried unless touched again, so a checkpoint-age check reads healthy. All three monitors look for rows that should exist on the far side and do not (link model: [bridge doc](../../../1.0/apps/library/features/toga2-api-client-and-bridge.md)).
44
+
45
+ ### TicketWritesToToga2 (TOGaDesk → TOGa 2)
46
+ - Counts WJE rows (`tickets.clientid = 153`) in `tickets`, `tickets_replies`, `comments` with `referenceId` NULL or `''`, created on/after `BACKLOG_CUTOFF_DATE = '2025-05-01'`, in `departmentIds`, minus `excludedTicketNumbers`.
47
+ - Skip rules mirror `syncTogaDesk1TicketIntoToga2Ticket` (`library/app/api/toga2.php` ~L12280-12290): a reply with blank message falls back to `newstatus` and is skipped if still PHP-empty (NULL, `''`, `'0'`); it never gets a `referenceId`, so the monitor skips it too. Comments are always sent.
48
+ - `HIGH` when any counted row is `>= alertThresholdMinutes` old. Empty `departmentIds` → `status: error`, `error: no_departments_configured`.
49
+ - Body: `latePendingCount`, `pendingCount`, per-type counts, `oldestPendingMinutes`, `alertThresholdMinutes`, `lateTicketNumbers` (top 10), `departmentIds`, `excludedTicketCount`.
50
+ - Params: `{"alertThresholdMinutes":15,"departmentIds":[269],"excludedTicketNumbers":[]}`. PHP default for `departmentIds` is `[]` **on purpose** — the DB row is the only home of the list. The cron comment in `sync_togasupply_wje.php` points here; keep them in step.
51
+
52
+ ### TicketSyncToTogadesk (TOGa 2 → TOGaDesk)
53
+ - Mapping (from `syncToga2TicketIntoTogadesk1Ticket`): only tickets with `c_wjeTicketId` are synced; note `isExternal = 1` → `tickets_replies.referenceid`, else → `comments.referenceid`; 1.0 dept = `ticketTeam.c_togadeskDepartmentId`, fallback 269 → so **no dept filter**.
54
+ - Query A (`Client_Wje`): `Tickets` with `c_wjeTicketId` (age by `dtCreated`) + their `TicketNotes` (age by `dtStamp` — **`TicketNotes` has no `dtCreated`**), inside window `GREATEST(CAST('2026-09-24' AS DATETIME), NOW() - lookbackDays)` and older than the threshold.
55
+ - Query B (`TOGaDeskSupport`): `referenceId`s of clientid 153 tickets / replies / comments with `timestamp >=` window minus 1 day (the sync copies 2.0 `dtCreated`/`dtStamp` into 1.0 `timestamp`). Compare per type in PHP (trim + lowercase).
56
+ - **Missing → confirm before alarming:** sleep 150s, `_Database::resetQueryCache()`, re-read; `HIGH` only if still missing. A note's `dtStamp` is its Freshservice time, so it can arrive already "old".
57
+ - Params: `{"alertThresholdMinutes":15,"lookbackDays":30,"excludedTicketNumbers":[]}`. Rows stuck longer than `lookbackDays` drop off; use `excludedTicketNumbers` for abandoned or hand-deleted ones.
58
+ - Cutoff 2026-09-24: see Gotchas (21-ticket gap).
59
+
60
+ ### WebhookTicketSync (Freshservice webhook → TOGa 2)
61
+ - Reads `Logs.Webhook` rows with route `WJE_WEBHOOK_ROUTE = '/wje'` in the last `lookbackHours`, groups by payload `freshdesk_webhook.id_numeric` (`JSON_VALID`-guarded), age from the **first** webhook.
62
+ - For ids `>= alertThresholdMinutes` old, checks `Client_Wje.Tickets.c_wjeTicketId` (= `id_numeric`, set by `_Worker_Wje::Webhook`, which has no skip rule for ticket webhooks). Missing → `HIGH`.
63
+ - `id_numeric` comes from a public endpoint → validated as a positive int; others counted in `skippedPayloadCount`.
64
+ - **Scan bound:** `Logs.Webhook` has no index on `dtStamp`/`route` (PK `id` only; ~6k rows/day all clients, ~175k rows). The query bounds on PK: `id > (SELECT GREATEST(MAX(id), N) - N)` with `N = maxScannedWebhookRows` (`GREATEST` avoids a negative bound on small tables). Payload sets `scanWindowShort` when the scanned span < `lookbackHours`.
65
+ - Silence (`minutesSinceLastWebhook`) is reported, **never alarmed** — long quiet spells are normal (see Gotchas).
66
+ - Params: `{"alertThresholdMinutes":30,"lookbackHours":24,"maxScannedWebhookRows":20000,"excludedTicketNumbers":[]}`; PHP defaults are the `DEFAULT_WEBHOOK_*` / `DEFAULT_MAX_SCANNED_WEBHOOK_ROWS` constants.
67
+
68
+ ### Clear a stuck ticket (runbook)
69
+ 1. Fix the cause (Sentry has the exception).
70
+ 2. Touch the ticket (add a reply/note) so the sync picks it up again — or move the checkpoint back.
71
+ 3. If abandoning it, add its ticket number to that monitor's `excludedTicketNumbers`.
72
+
73
+ ### Activation (in order, all three)
74
+ 1. Push branch `TRUE-80523` (worker2, dbchanges2, worker).
75
+ 2. Deploy worker2.
76
+ 3. Run the Core seeds in order: `2026-09-30b`, `2026-10-01a`, `2026-10-01b`.
77
+ 4. Create the three OneUptime Incoming Request monitors with **body rules only**: Contains `"alarm":"HIGH"` → Offline; Contains `"status":"error"` → Offline; Contains `"alarm":"OK"` → Online. Put each push URL in its job config (never in a doc).
78
+ 5. Test `TicketWritesToToga2`: set `departmentIds` to `[290]` (has old unsent rows), hand-run → expect HIGH. Restore `[269]`, hand-run → expect recovery.
79
+ 6. Hand-run the other two → expect OK.
80
+ 7. Set `isActive = 1` on all three.
81
+ 8. Only then add the Not Received 10/15 min rules (see the activation-ordering trap in the shared doc).
82
+
83
+ ## Gotchas
84
+ - **Dept 269 is very low volume** (Sept 2026: 3 tickets, 7 replies, 2 comments). "No writes" is normal.
85
+ - **Do not add 290 casually** to `TicketWritesToToga2` — 290 is not synced 1.0 → 2.0 (see [profile](../profile.md)); it would alarm on the known backlog.
86
+ - **Pre-2025-05-01 history never drains:** 21 sendable unsent dept-269 rows 2024-12 → 2025-04-22. Hence `BACKLOG_CUTOFF_DATE`.
87
+ - **21 WJE tickets never reached TOGaDesk** (created in TOGa 2 2026-09-18 → 09-23: 66095–66100, 66137–66144, 66146, 66172–66176, 66207). Every ticket and note since 2026-09-24 did. Likely the 2.0 → 1.0 side failed ~5 days while its checkpoint moved — **cause not confirmed; not being investigated (Mark's call).** Hence the 2026-09-24 floor in `TicketSyncToTogadesk`.
88
+ - **WJE turns OFF new-ticket webhooks 8am–6pm on their workday** (TOGA covers after-hours calls). Updates on open tickets still arrive by day (peak 7–10am handoff). Volume ~20–30 webhooks/weekday, ~5/weekend day; 10h+ quiet nights are normal.
89
+ - **WJE sends only a subset of Freshservice tickets** (e.g. 66209–66257 never came); routing rule unknown. So a Freshservice-API "never sent" check was deliberately not built.
90
+ - **Webhook payload shape:** ticket webhooks carry `freshdesk_webhook.ticket_id` (`"INC-66452"`) + `id_numeric` (`66452`); asset webhooks carry `asset_display_id` instead.
91
+ - **Clock is US/Central everywhere** — `Logs`, `Client_Wje`, `TOGaDeskSupport` (verified: 66452 first webhook 06:59:41, `Tickets.dtCreated` 06:59:50). Spring DST can false-alarm once; autumn can delay an alarm up to 1h.
92
+ - **Re-read needs `_Database::resetQueryCache()`** — an identical second `_Query` in the same run is served from cache.
93
+ - **Query cost (prod, verified, all fine at 5 min):** `TOGaDeskSupport` has **no index on `referenceId`** (`tickets`: clientid etc.; `tickets_replies`: `ticketid`; `comments`: PRIMARY only, ~13k-row full scan). `Client_Wje.TicketNotes` ~7.3k-row full scan; `Tickets.c_wjeTicketId` not indexed. Legacy `wait_timeout` 900s (covers the 150s sleep).
94
+ - **Blind spots:**
95
+ - `TicketWritesToToga2`: field-only edits (priority/assignee) on a linked ticket create no row. Status changes are covered (they arrive as replies).
96
+ - `WebhookTicketSync`: webhooks WJE never sends; note sync inside the webhook; forged numeric posts to the public `/wje` route can raise a false HIGH (whether the ingestion Lambda verifies the sender is unchecked).
97
+ - **The ClickUp TRUE-80523 description is fabricated** (Talos auto-generated) — see [MCP tool usage gotcha](../../../standalone/apps/claude/workflows/mcp-tool-usage.md). Trust this doc and the code.
98
+
99
+ ## Related
100
+ - [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md) — shared pattern
101
+ - [App_Api_Toga2 bridge](../../../1.0/apps/library/features/toga2-api-client-and-bridge.md) — `syncWithTogadesk`, `referenceId` write-back
102
+ - [WJE Freshservice sync](../../../2.0/apps/worker2/features/wje-freshservice-sync.md) — webhook handler, `c_wje*` fields
103
+ - [WJE profile](../profile.md)
104
+
105
+ ## Change history
106
+ - 2026-10-01 — Renamed from `oneuptime-ticket-writes-monitor.md` to the suite doc; added `TicketSyncToTogadesk` (TRUE-80521) and `WebhookTicketSync` (TRUE-80520). Not deployed. (mhammontree)
107
+ - 2026-10-01 — Created: `TicketWritesToToga2` built on branch TRUE-80523, not deployed, cron `isActive = 0`. (mhammontree)
@@ -17,10 +17,11 @@ files:
17
17
  - worker2/Worker/Wje.php
18
18
  - worker/crons/toga2/wje/import_supporting_records.php
19
19
  - worker2/Worker/Monitor/Wje.php
20
+ - worker2/Worker/Client/Wje.php
20
21
  related:
21
22
  - ../../1.0/apps/library/features/toga2-api-client-and-bridge.md
22
23
  - ../../2.0/apps/worker2/features/wje-freshservice-sync.md
23
- - ./features/oneuptime-ticket-writes-monitor.md
24
+ - ./features/oneuptime-monitors.md
24
25
  ---
25
26
 
26
27
  WJE (Wiss, Janney, Elstner / "WJE IT") Freshservice help-desk client, single hardcoded tenant; open for the three sync paths, the TOGaDesk bridge ids/departments, and its monitor.
@@ -37,7 +38,9 @@ Data moves between WJE and TOGA on three paths:
37
38
  2. **Daily batch import** (`worker/crons/toga2/wje/import_supporting_records.php`) — imports canned responses, requesters, assets, groups, and the 3-level category hierarchy.
38
39
  3. **2-minute TOGaDesk bridge** (`App_Api_Toga2::syncWithTogadesk`) — the cron `worker/crons/toga2/wje/sync_togasupply_wje.php` enables the **full** set of bridge sub-syncs (tickets both directions, ticket-notes, contacts→people, items/units→assets, predefined-replies, ticket-teams→groups, ticket-categories) for TOGaDesk client id 153. **TOGaDesk → 2.0 syncs department 269 only.**
39
40
 
40
- Monitoring: [ticket-writes monitor](./features/oneuptime-ticket-writes-monitor.md) alarms when a dept-269 TOGaDesk row is not written to 2.0.
41
+ Monitoring: [OneUptime monitor suite](./features/oneuptime-monitors.md) — three backlog monitors, one per hop (webhook → 2.0, TOGaDesk dept 269 → 2.0, 2.0 → TOGaDesk).
42
+
43
+ Webhook behavior: WJE turns OFF new-ticket webhooks 8am–6pm on their workday (TOGA covers after-hours); updates still arrive by day. WJE sends only a subset of its Freshservice tickets (rule unknown).
41
44
 
42
45
  ## TOGaDesk departments
43
46
 
@@ -50,4 +53,4 @@ Monitoring: [ticket-writes monitor](./features/oneuptime-ticket-writes-monitor.m
50
53
 
51
54
  - **[wje-freshservice-sync](../../2.0/apps/worker2/features/wje-freshservice-sync.md)** — full integration: webhook handler, import cron, `c_wje*` models, API clients, schema, reports, gotchas.
52
55
  - **[toga2-api-client-and-bridge](../../1.0/apps/library/features/toga2-api-client-and-bridge.md)** — the `syncWithTogadesk` bridge mechanism.
53
- - **[oneuptime-ticket-writes-monitor](./features/oneuptime-ticket-writes-monitor.md)** — the 1.0 → 2.0 write monitor.
56
+ - **[oneuptime-monitors](./features/oneuptime-monitors.md)** — the three WJE OneUptime monitors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.932",
3
+ "version": "1.0.934",
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",
@@ -1,80 +0,0 @@
1
- ---
2
- title: "WJE: TOGaDesk → TOGa 2 ticket-writes monitor (Monitor/Wje/TicketWritesToToga2)"
3
- framework: "2.0"
4
- repo: worker2
5
- project: Worker
6
- client: wje
7
- type: client-feature
8
- status: active
9
- updated: 2026-10-01
10
- owners: ["mhammontree"]
11
- files:
12
- - worker2/Worker/Monitor/Wje.php
13
- - dbchanges2/Core/2026-09-30b - Insert WJE Ticket Writes Monitor CronJob.sql
14
- - worker/crons/toga2/wje/sync_togasupply_wje.php
15
- - library/app/api/toga2.php
16
- related:
17
- - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
18
- - ../../../1.0/apps/library/features/toga2-api-client-and-bridge.md
19
- - ../../../2.0/apps/worker2/features/wje-freshservice-sync.md
20
- - ../profile.md
21
- ---
22
- worker2 OneUptime push monitor counting WJE TOGaDesk tickets/replies/comments never written to TOGa 2 (NULL referenceId); open when working on WJE bridge monitoring, its departments, or clearing a stuck ticket.
23
-
24
- ## Summary
25
- `Monitor/Wje/TicketWritesToToga2` (`_Worker_Monitor_Wje`, TRUE-80523) watches the 1.0 → 2.0 half of the WJE TOGaDesk bridge. It follows the shared [OneUptime push-metric pattern](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md) — read that for payload contract, criteria, and provisioning.
26
-
27
- - **Status 2026-10-01:** committed on branch `TRUE-80523` (worker2 + dbchanges2 + worker comment), **not deployed**. Cron seed ships `isActive = 0`; `$ONEUPTIME_URL` is still empty (push credential — never record it).
28
- - Runs in worker2, outside the 1.0 worker it watches. Reads `TOGaDeskSupport` via `_underscore::DB_TOGADESK1` (see [worker2 legacy DB access](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md)).
29
- - OneUptime monitor name: **"WJE Ticket Writes to TOGa 2"**.
30
-
31
- ## How it works
32
-
33
- ### Why a backlog, not a checkpoint-freshness check
34
- The bridge catches a per-ticket exception → Sentry only, and **still advances `TOGA_LAST_TICKET_INTEGRATION_DATETIME` past it**. The failed ticket is never retried unless touched again. A checkpoint-age monitor would read healthy. A row with NULL `referenceId` catches both a dead cron and silent per-ticket failures (link model: [bridge doc](../../../1.0/apps/library/features/toga2-api-client-and-bridge.md)).
35
-
36
- ### What it counts
37
- WJE rows (`tickets.clientid = 153`) in `tickets`, `tickets_replies`, `comments` where `referenceId` is NULL or `''`, created on/after `BACKLOG_CUTOFF_DATE = '2025-05-01'`, in the configured departments, minus `excludedTicketNumbers`.
38
-
39
- Skip rules mirror `syncTogaDesk1TicketIntoToga2Ticket` in `library/app/api/toga2.php` (~L12280-12290): a reply with a blank message falls back to `newstatus` and is skipped if still PHP-empty (NULL, `''`, `'0'`) — it never gets a `referenceId`, so the monitor skips it too. Comments are always sent.
40
-
41
- ### Decision + payload
42
- - `alarm: HIGH` when any counted row is `>= alertThresholdMinutes` old; else `alarm: OK`.
43
- - `status: error` on DB failure (payload carries **only** `errorClass` + `errorNumber` — the exception message can hold SQL, DB host, DB user) and on empty `departmentIds` (`error: no_departments_configured`).
44
- - Body: `latePendingCount`, `pendingCount`, per-type counts, `oldestPendingMinutes`, `alertThresholdMinutes`, `lateTicketNumbers` (top 10), `departmentIds`, `excludedTicketCount`.
45
-
46
- ### Parameters (`Core.CronJobs.parameters`, no deploy to change)
47
- `{"alertThresholdMinutes":15,"departmentIds":[269],"excludedTicketNumbers":[]}`
48
- - `departmentIds` PHP default is `[]` **on purpose** — the DB row is the only home of the list. Missing list → `status: error`, never a silent hardcoded dept. The cron comment in `sync_togasupply_wje.php` points here; keep the two in step if the sync's departments change.
49
- - `excludedTicketNumbers` — TOGaDesk ticket numbers the team gives up on, so one permanently failing ticket does not hold the alarm red.
50
-
51
- ### Clear a stuck ticket (runbook)
52
- 1. Fix the cause (Sentry has the exception).
53
- 2. Add a reply to the ticket in TOGaDesk so the sync picks it up again — or move the checkpoint back.
54
- 3. If abandoning it, add its ticket number to `excludedTicketNumbers`.
55
-
56
- ### Activation (in order)
57
- 1. Create the OneUptime Incoming Request monitor with **body rules only**: Contains `"alarm":"HIGH"` → Offline + incident; Contains `"status":"error"` → Offline + incident; Contains `"alarm":"OK"` → Online.
58
- 2. Paste its URL into `$ONEUPTIME_URL`.
59
- 3. Push and deploy worker2.
60
- 4. Run the Core seed `2026-09-30b - Insert WJE Ticket Writes Monitor CronJob.sql`.
61
- 5. Test HIGH: set `departmentIds` to `[290]` (has old unsent rows), hand-run. Restore `[269]`, hand-run to see recovery.
62
- 6. Set `isActive = 1`.
63
- 7. Only then add the not-received 10/15 min rules (see the activation-ordering trap in the shared doc).
64
-
65
- ## Gotchas
66
- - **Dept 269 is very low volume** (Sept 2026: 3 tickets, 7 replies, 2 comments). "No writes" is normal — this is why it is a backlog monitor, not a "writes stopped" monitor.
67
- - **Blind spot: field-only edits.** Priority/assignee change on an already-linked ticket creates no row, so a failed push is invisible. Status changes are covered (they arrive as replies).
68
- - **Clock is US/Central.** `TOGaDeskSupport` `NOW()` and row timestamps are both server US/Central (the `_underscore` driver sets no session `time_zone`). Spring DST can false-alarm once; autumn can delay an alarm up to 1h.
69
- - **Pre-cutoff history never drains:** 21 sendable unsent dept-269 rows 2024-12 → 2025-04-22 (early integration). That is why the cutoff is 2025-05-01.
70
- - **Query cost (prod, verified):** `tickets` via index_merge(departmentid, clientid); `tickets_replies` via `ticketid` index; `comments` has **no** `ticketid` index → full scan of ~13k rows (acceptable). Timestamp columns have no `ON UPDATE`.
71
- - **Do not add 290 casually.** Dept 290 is not synced 1.0 → 2.0 today (see [profile](../profile.md)); pointing the monitor at it would alarm on the known backlog, not a new failure.
72
- - **The ClickUp TRUE-80523 description is fabricated** (Talos auto-generated) — see [MCP tool usage gotcha](../../../standalone/apps/claude/workflows/mcp-tool-usage.md). Trust this doc and the code.
73
-
74
- ## Related
75
- - [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md) — shared pattern
76
- - [App_Api_Toga2 bridge](../../../1.0/apps/library/features/toga2-api-client-and-bridge.md) — `syncWithTogadesk`, `referenceId` write-back
77
- - [WJE profile](../profile.md)
78
-
79
- ## Change history
80
- - 2026-10-01 — Created: monitor built on branch TRUE-80523, not deployed, cron `isActive = 0`. (mhammontree)