toga-ai 1.0.941 → 1.0.943

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.
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
10
- owners: ["jcardinal", "ajean", "rgirish", "kyalamarthi"]
9
+ updated: 2026-10-05
10
+ owners: ["jcardinal", "ajean", "rgirish", "kyalamarthi", "mhammontree"]
11
11
  files:
12
12
  - _underscore/ApiRequest.php
13
13
  - _underscore/Model/Client/Logs/Api.php
@@ -17,6 +17,7 @@ related:
17
17
  - ../../worker2/features/creating-worker-actions.md
18
18
  - ../../worker2/features/talos-transcript-ingestion.md
19
19
  - togaiq-gateway-client.md
20
+ - ../../worker2/features/8x8-post-call-survey-import.md
20
21
  ---
21
22
 
22
23
  `_ApiRequest`, the 2.0 outbound HTTP client — JSON encode/decode, retry, and api-logging traps; open before calling it or reading `Logs.Api`.
@@ -94,6 +95,14 @@ case-insensitive scan of already-set headers, so a caller-supplied `Content-Type
94
95
  **hardcodes** its token in committed source — flagged for a rotate-and-move-to-config ticket; 11
95
96
  worker2 files use it). When adding a new component, decide deliberately: log and accept the header
96
97
  exposure, strip/redact the auth header before the call is logged, or `setLogging(false)`.
98
+ Basic auth via `CURLOPT_USERPWD` is **not** logged (it is not a header); bearer headers and
99
+ request/response bodies **are**. Vendor calls carrying secrets or caller PII → `setLogging(false)`
100
+ (e.g. [8x8 survey import](../../worker2/features/8x8-post-call-survey-import.md)).
101
+ - **⚠ OPEN, not fixed: SSL peer verification is OFF by default.** `ApiRequest.php:238` sets
102
+ `CURLOPT_SSL_VERIFYPEER` to `0` whenever `sslVerifyPeer` is null — despite the "AUTO based on
103
+ https" comment, `!null` is true, so an unset value never verifies, https included. New components
104
+ must call `setSslVerifyPeer(true)`. Changing the default touches every caller — needs a `cto`
105
+ opinion. 1.0 twin: [library architecture](../../../1.0/apps/library/architecture.md).
97
106
  - **Multipart/file upload ALREADY WORKS — do not add an `ENCODE__MULTIPART` case (2026-09-04).**
98
107
  The request-encoding switch (`ApiRequest.php:205-228`) has **no `default:` case**, so
99
108
  `payloadEncoding: null` passes the payload through **untouched** and sets no `Content-Type`; `:236`
@@ -153,6 +162,7 @@ case-insensitive scan of already-set headers, so a caller-supplied `Content-Type
153
162
  `dtStamp` (indexed) or it times out — `WHERE source = '…'` alone will not return.
154
163
 
155
164
  ## Change history
165
+ - 2026-10-05 — Added SSL-verify-off default (OPEN) and basic-auth-not-logged note, from TRUE-82185 (mhammontree)
156
166
  - 2026-09-25 — renamed product reference from "TogaIQ" to "Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
157
167
 
158
168
  ## Related
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
9
+ updated: 2026-10-05
10
10
  owners: ["bala", "mhammontree"]
11
11
  files:
12
12
  - _underscore/Database.php
@@ -66,6 +66,15 @@ For DB-**free** unit tests of interceptor logic, use the PHPUnit scaffold instea
66
66
  anything touches `App_Framework`. `__APPROOT__/app/framework.php` shadows the library file for
67
67
  *both* class names, so resolving `App_Framework` first loads the app file and the library
68
68
  definition never arrives.
69
+ - **Locally, `registerClientDatabases()` can fail outright.** It reads `Clients` through the local
70
+ `Core` alias, which on a laptop may point at the 1.0 `Core` DB (no `Clients` table). Register the
71
+ client schema directly instead: `_Database::register(...)` with `database` = the real schema
72
+ (e.g. `Client_True`) and `alias` = `DB_CLIENT`. Windows MySQL reports schema names lower-case
73
+ (`client_true`) — compare case-insensitively.
74
+ - **⚠ The 2.0 debug error page prints `print_r($GLOBALS)`, including `$_SERVER` / env.** A script
75
+ that reads a secret from env vars must scrub them (`putenv`, `unset($_SERVER[...], $_ENV[...])`)
76
+ and keep the value out of global scope (inside a function) **before** the framework loads, or the
77
+ first error dumps the secret to the screen.
69
78
  - Do not drive a local harness through `_Worker::runTask` — the dev config's
70
79
  `[cloud] aws_worker_queue_url` points at the **production** SQS queue. Call the target method
71
80
  in-process. (Also noted on [email-template-sending.md](./email-template-sending.md).)
@@ -79,4 +88,5 @@ For DB-**free** unit tests of interceptor logic, use the PHPUnit scaffold instea
79
88
  — the work these traps surfaced in.
80
89
 
81
90
  ## Change history
91
+ - 2026-10-05 — Added local `registerClientDatabases` failure + debug-page `$GLOBALS` secret leak gotchas (TRUE-82185) (mhammontree)
82
92
  - 2026-09-25 — Added lazy-CWD autoloader gotcha (found building the worker2 Codeception harness) (mhammontree)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-10-01
9
+ updated: 2026-10-05
10
10
  owners: [jcardinal, mhammontree, bala, ajean]
11
11
  files:
12
12
  - Core/
@@ -189,6 +189,7 @@ When the executor processes a client, it applies that client's own folder **plus
189
189
  |---|---|---|
190
190
  | `netsuite` | `_modules/netsuite/` | Compass, Canon, Ncci, Sbasite, Spglobal, … (most clients) |
191
191
  | `startech` | `_modules/startech/` | Pcmatic |
192
+ | `8x8` | `_modules/8x8/` | True (8x8 post-call survey tables, TRUE-82185) |
192
193
 
193
194
  Module folders use the **same `YYYY-MM-DD` naming + ordering rules** as client folders.
194
195
 
@@ -3,6 +3,7 @@
3
3
  | Doc | Summary |
4
4
  |-----|---------|
5
5
  | [Worker (worker2) Architecture](architecture.md) | How worker2 (Elastic Beanstalk Worker Tier background-job app) works overall — job pipeline, MySQL/SQS roles, Lambdas, routing, transactions; open for any worke |
6
+ | [8x8 Post-Call Survey (CSAT) Import (8x8/Surveys → _modules/8x8 tables)](features/8x8-post-call-survey-import.md) | worker2 imports 8x8 Contact Center post-call surveys (CSAT) from the 8x8 Analytics API into `Client_True` tables, hourly, with an import-run watermark; open bef |
6
7
  | [Deploy-Time Auto-Registration to the Shared ALB Target Group (non-production)](features/alb-target-group-auto-registration.md) | Deploy-time postdeploy hook that self-registers a non-prod EB instance into the shared-ALB target group; open when a non-prod worker2/saml/api2 env is unreachab |
7
8
  | [All-Client Email Queue Monitor (Monitor/Operations/EmailQueue)](features/all-client-email-queue-monitor.md) | Cross-client health check for the 2.0 outbound email queue (`Logs_<Client>.Email`); open when working on email-queue alerting or an all-client DB fan-out. |
8
9
  | [API 2.0 Stability Monitor (Monitor/Operations/ApiStability)](features/api-stability-monitor.md) | End-to-end authenticated round-trip probe of the production 2.0 API; open when working on API liveness monitoring or a server-side call to the public-auth endpo |
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: "8x8 Post-Call Survey (CSAT) Import (8x8/Surveys → _modules/8x8 tables)"
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: draft
9
+ updated: 2026-10-05
10
+ owners: ["mhammontree"]
11
+ files:
12
+ - worker2/Worker/8x8/Surveys.php
13
+ - _underscore/Component/Api/8x8/8x8.php
14
+ - dbchanges2/Core/2026-10-02a - 8x8 partner and survey import cron.sql
15
+ - dbchanges2/_modules/8x8/2026-10-02a - PostCallSurveys.sql
16
+ related:
17
+ - ../../_underscore/features/apirequest-json-content-type.md
18
+ - ../../_underscore/features/cloud-secrets-manager.md
19
+ - ../../_underscore/features/cli-script-bootstrap.md
20
+ - ./creating-worker-actions.md
21
+ - ../../../clients/true/profile.md
22
+ ---
23
+
24
+ worker2 imports 8x8 Contact Center post-call surveys (CSAT) from the 8x8 Analytics API into `Client_True` tables, hourly, with an import-run watermark; open before touching the 8x8 component, the survey tables, or 8x8 credentials.
25
+
26
+ ## Summary
27
+
28
+ **Status (2026-10-05): built on branch `TRUE-82185` in `_underscore`, `worker2`, `dbchanges2` — NOT committed, NOT deployed.** Ticket TRUE-82185; design from the 2026-09-29 "8x8 Integration" meeting.
29
+
30
+ Replaces the manual 8x8 CSV export + Excel pivots. Phase 1 = store only. Later phases link surveys to Desk tickets, Chat, call records. Power BI / Talos read the tables directly (no `Core.Records` registration yet).
31
+
32
+ One 8x8 tenant (`agilantsolutionsi01`, TOGA's call center) serves many end clients (Office Depot, Craftex, Walmart, PC Matic). **All surveys land in `Client_True`**; the end client is identified by `surveyName` / `queueName`. Routing to per-end-client DBs is deferred.
33
+
34
+ ## How it works
35
+
36
+ **Credentials.** AWS Secrets Manager `partners/8x8/<clientIdentifier>` (us-east-1), JSON `{clientId, secret, region}`. Not in `Core.PartnerApiIdentities.partnerApiPassword` — that column is a STRING RecordField on record 305, readable through the records API.
37
+
38
+ **Component `_Component_Api_8x8`** (`_underscore/Component/Api/8x8/8x8.php`):
39
+ 1. Token: `POST https://api.8x8.com/oauth/v2/token`, client_credentials, Basic `clientId:secret`, lives ~1799s.
40
+ 2. Create report: `POST /analytics/cc/v8/historical-metrics/detailed-reports`, type `detailed-reports-survey`; returns `DONE` immediately.
41
+ 3. Page data: `GET /detailed-reports/{id}/data?size=1000`; next-page cursor is the `Last-Document-Id` **response header**.
42
+ 4. Region path prefix: us `''`, `eu`, `au`, `ca`.
43
+ 5. `Logs.Api` logging OFF on every call (bearer token + caller PII); `setSslVerifyPeer(true)`. A 204 / empty body = empty page. Exception messages carry response shape only, never values.
44
+
45
+ **Worker `_Worker_8x8_Surveys`** (`worker2/Worker/8x8/Surveys.php`):
46
+ 1. `8x8/Surveys/Dispatch` (cron `15 * * * *`, 300s) queues one `8x8/Surveys/Import` per client with an 8x8 row in `Core.PartnerApiIdentities`.
47
+ 2. `Import(int $clientId, ?string $windowStart, ?string $backfillStart)` — one client per job, 1-day windows, 180s budget, then re-queues itself via `runTask` with `windowStart`.
48
+ 3. Start = last SUCCESS `dtWindowEnd` − 48h overlap; with no SUCCESS row, `backfillStart` is required (else throws).
49
+ 4. Upsert on `(source, externalResponseKey)` via raw escaped `INSERT … ON DUPLICATE KEY UPDATE` — `_Model` has no upsert and `save()` cascades to parents. Agents/answers are delete+reinsert **only** when the survey row changed (affected rows 0 = unchanged → skip).
50
+ 5. Fail loud: a bad record throws, the window is marked FAILED, the watermark does not advance.
51
+ 6. RUNNING guard also treats a SUCCESS completed within 300s as running (closes the chain hand-off gap).
52
+ 7. Read host disabled for the job (`_Database::setIsReadHostEnabled(false)`) — see Gotchas.
53
+
54
+ **Schema.**
55
+ - `Core`: `PartnerApiIdentities.clientApiId` now NULL-able; `Partners` row `8x8`; identity row clientIdentifier `True` → partnerIdentifier `agilantsolutionsi01`; `CronJobs` `8x8/Surveys/Dispatch` inserted **INACTIVE**.
56
+ - Module `8x8` (`_modules/8x8/`, listed in `Client_True/_modules.txt`): `PostCallSurveys`, `PostCallSurveyAgents`, `PostCallSurveyAnswers`, `PostCallSurveyImports` (the import-run table is the watermark, not `MAX(dtSurveyStarted)`).
57
+ - Survey times stored **UTC** (8x8 returns local time + offset). Vendor values (`surveyType`, `questionType`, `answerType`) are VARCHAR, not ENUM; our `status` is ENUM.
58
+
59
+ **8x8 data shape (probed 2026-10-02).**
60
+ - One row = one survey. `surveyId` is the survey **template** (1201 OD Customer Survey, 2164 Craftex CSAT, 1221 Walmart Customers, 1161 PC Matic Survey); there is no response id. Unique key = `surveyId` + highest `interactionId` (ids start with a hex timestamp; 1–2 per row on transfer).
61
+ - Answers nested in `items[key=results].value[]`; values like `StayOnCall`, `Scale`, `VoiceComment`, `Valid`, `Recorded`. `surveyIsDeleted` is the string `"false"`. Durations in ms. `voiceRecordingUuid` is per answer (`callerVoiceInput-<uuid>`).
62
+ - Volume ~110 surveys/day.
63
+
64
+ **Deploy order** (human steps):
65
+ 1. Run the Core SQL (cron stays inactive).
66
+ 2. Run the module SQL on `Client_True`.
67
+ 3. Verify the `PartnerApiIdentities` row for `True`.
68
+ 4. devops grants the worker2 EB role `secretsmanager:GetSecretValue` on `partners/8x8/*`.
69
+ 5. Deploy `_underscore` (Rohan's weekly window), then worker2.
70
+ 6. Run `8x8/Surveys/Import` by hand with `backfillStart`.
71
+ 7. Verify SUCCESS rows in `PostCallSurveyImports`, including one empty window.
72
+ 8. Set `CronJobs.isActive = 1`.
73
+
74
+ **Local verification.** `test/@Mark/TRUE-82185/upsert_local_test.php` feeds a saved real page through private `upsertSurvey` (ReflectionMethod) into local `Client_True`: 25 inserted; re-run 25 unchanged, 0 answers rewritten. Full end-to-end through the worker not yet run (needs AWS secret access).
75
+
76
+ ## Gotchas
77
+
78
+ - **Omit the report `timezone` param.** The tenant rejects `"UTC"` ("Timezone UTC is not valid or configured for this tenant"); convert to UTC in PHP.
79
+ - **The 1.0 VCC API has no survey data.** `vcc-na20.8x8.com/api` (basic auth tenant+token; `library/app/api/8x8.php`, `worker/crons/togadesk/send_uma_8x8_daily_report.php`) gives queue stats only. Surveys need the Analytics API (CC API v7+).
80
+ - **8x8 developer reference pages render in JavaScript** — WebFetch gets no schemas. Confirm fields with a real call.
81
+ - **Read-after-write in the job:** SELECTs go to the read host (a separate connection) and cannot see the job's uncommitted insert. Disable the read host for the job — same fix as `_Email` (TRUE-80448). See [api-payload-interceptors](../../api2/features/api-payload-interceptors.md).
82
+ - **DB errors: store/rethrow only `errno` + error text.** The raw `_Query` message repeats the whole INSERT, with caller PII, into `WorkerJobs.output` / `Logs.Issue`.
83
+ - **8x8 report storage:** each run creates reports on 8x8's side (~72/day). Not cleaned up unless 8x8 complains (decided 2026-10-02).
84
+
85
+ ## Change history
86
+ - 2026-10-05 — Created: Phase 1 survey import built on `TRUE-82185` (uncommitted, not deployed); single-tenant → `Client_True` decision, Secrets Manager creds, import-run watermark (mhammontree)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-17
9
+ updated: 2026-10-05
10
10
  owners: [jcardinal, dfranks, mhammontree, tcox, bala, ajean, rgirish]
11
11
  files:
12
12
  - worker2/Worker/
@@ -513,10 +513,15 @@ Rules: rename **both sides in one PR and one deploy**; grep every repo for the o
513
513
  failure indefinitely. The dry run must exercise the **same helpers** as the real path — a separate
514
514
  test harness that reimplements them proves nothing. See
515
515
  [netsuite-opportunity-sync](./netsuite-opportunity-sync.md#gotchas--known-issues) for what it caught.
516
+ - **Read-after-write inside an action needs the read host OFF.** `_Query` sends SELECTs to the read
517
+ host — a separate connection that cannot see the job's own uncommitted insert (not replication
518
+ lag). Call `_Database::setIsReadHostEnabled(false)` for the job, as `_Email` (TRUE-80448) and
519
+ [8x8 survey import](./8x8-post-call-survey-import.md) do.
516
520
  - See [architecture.md](../architecture.md) for the always-HTTP-200 rule and the
517
521
  commit-before-SQS transaction pattern that the worker relies on.
518
522
 
519
523
  ## Change history
524
+ - 2026-10-05 — Added read-host read-after-write gotcha for worker actions (TRUE-82185) (mhammontree)
520
525
  - 2026-09-17 — Added the **half-written `WorkerJobs` row** tell: a row with `dtStarted` but no
521
526
  `dtCompleted`/`isSuccess`/`output` means the **action class is not on the box**, not a hung job.
522
527
  `Controller/Index.php` (~L322) calls `method_exists($className, 'initialize')` one line **above**
@@ -6,11 +6,12 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-01
9
+ updated: 2026-10-05
10
10
  owners: ["jcardinal", "mhammontree", "bala", "rgirish"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Compass.php
13
13
  - worker2/Worker/Client/Compass.php
14
+ - worker2/Worker/Client/CompassCanada.php
14
15
  - worker2/composer.json
15
16
  - _underscore/Cloud.php
16
17
  - worker2/Worker/Monitor/Operations.php
@@ -34,6 +35,7 @@ related:
34
35
  - ../../../clients/compass-usa/features/oneuptime-ma-refresh-order-monitor.md
35
36
  - ../../../clients/compass-usa/features/oneuptime-netsuite-duplicate-sales-orders-monitor.md
36
37
  - ../../../../clients/wje/features/oneuptime-monitors.md
38
+ - ../../../clients/compass-canada/features/oneuptime-po-transmissions-to-vendors-monitor.md
37
39
  ---
38
40
 
39
41
  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.
@@ -44,7 +46,7 @@ A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, p
44
46
  `App_SystemMonitor_Compass` monitors. A worker2 cron action runs a self-contained check, decides
45
47
  pass/fail itself, and POSTs a JSON metric body to a OneUptime "Incoming Request" monitor — replacing
46
48
  the 1.0 tier's email alerts. The class is `abstract class _Worker_Monitor_Compass` and now carries a
47
- **suite of 12 monitors** covering the Compass USA / Office Depot integration (S3, DB, mailbox, and
49
+ **suite of 13 monitors** covering the Compass USA / Office Depot integration (plus one Compass Canada monitor) (S3, DB, mailbox, and
48
50
  NetSuite SuiteQL checks). Designed to be **client-agnostic** — a reusable template for future monitors and clients.
49
51
 
50
52
  Distinct from the DB-driven email-orchestrator [Monitoring Framework](./monitoring-framework.md)
@@ -72,9 +74,9 @@ disallows `const` at function scope), plus a `$push` closure using `_ApiRequest`
72
74
  method, so the class scales cleanly as monitors accumulate. The metric POST runs with **logging
73
75
  disabled** and **`throwExceptionsOnFailure` disabled**, so a failed ping never fails the worker job.
74
76
 
75
- ### The 12 monitors
77
+ ### The 13 monitors
76
78
 
77
- Each measures one Compass/Office Depot integration metric and pushes a decided alarm token.
79
+ Each measures one Compass/Office Depot (or Compass Canada) integration metric and pushes a decided alarm token.
78
80
 
79
81
  - **OfficeDepotEdiImportQueue** (S3) — counts files >10 min old in `s3://agilant-as2/OfficeDepot/`,
80
82
  excluding `OUTBOX/`, `SENT/` and directory placeholders.
@@ -134,6 +136,11 @@ Each measures one Compass/Office Depot integration metric and pushes a decided a
134
136
  NetSuite instead of a Toga DB** (via `_Component_Api_Netsuite::send()`, `QUERY_SUITEQL`), because the
135
137
  duplicates exist only in NetSuite. Details:
136
138
  [Compass: NetSuite duplicate SO monitor](../../../clients/compass-usa/features/oneuptime-netsuite-duplicate-sales-orders-monitor.md).
139
+ - **CompassCanadaPurchaseOrderTransmissionsToVendors** (DB, added 2026-10-05) — due, unsubmitted
140
+ `Client_CompassCanada` POs (customer 5) the 1.0 Canada transmit cron has not sent to Grand & Toy.
141
+ `Core.CronJobs` id 87, `*/5`. First monitor in this class on a **second client DB** (registers
142
+ `Client_CompassCanada` in-method). DEGRADED >10 waiting / >15 min oldest, HIGH >20 / >30 min. Details:
143
+ [Compass Canada: PO-to-vendor monitor](../../../clients/compass-canada/features/oneuptime-po-transmissions-to-vendors-monitor.md).
137
144
 
138
145
  ### Payload contract & OneUptime criteria (critical — OneUptime cannot compare numbers)
139
146
 
@@ -573,6 +580,7 @@ production deploy.**
573
580
  - [OneUptime 1.0 worker uptime monitoring](../../../1.0/apps/worker/features/oneuptime-worker-uptime-monitoring.md) — the 1.0 push-heartbeat predecessor
574
581
 
575
582
  ## Change history
583
+ - 2026-10-05 — Added `CompassCanadaPurchaseOrderTransmissionsToVendors` (cron 87), the first monitor reading `Client_CompassCanada`. (bala)
576
584
  - 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)
577
585
  - 2026-10-01 — Clarified worker2 reads `TOGaDeskSupport` (`DB_TOGADESK1`) — only `Common` is unreachable; added `Monitor/Wje`. (mhammontree)
578
586
  - 2026-09-25 — Added `NetsuiteDuplicateSalesOrders`, the first monitor that queries NetSuite (SuiteQL) instead of a Toga DB. (rgirish)
@@ -20,7 +20,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
20
20
  ## 2.0 framework
21
21
 
22
22
  - **_underscore** (_Underscore) _(framework core)_ — 94 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
23
- - **worker2** (Worker) — 81 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
+ - **worker2** (Worker) — 83 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
24
24
  - **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
25
25
  - **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
26
26
  - **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
  | [French (fr-CA) Order Email Localization (Compass Canada)](features/french-order-email-localization.md) | 2.0 | How Compass Canada order emails render per-recipient EN/FR (item block, labels, denial-reason machine translation) under an iso-8859-1 send; open for any locali |
7
7
  | [Grand & Toy ASN Import (Compass Canada)](features/grand-and-toy-asn-import.md) | 2.0 | Imports Grand & Toy ASNs for Compass Canada (CSV cron + cXML gateway) and auto-builds the ItemFulfillment chain + bilingual in-transit email; open for G&T ASN f |
8
8
  | [Compass Canada — Item-Fulfillment TableViews (dead-Core-id 500, rebuilt onto the USA shape)](features/item-fulfillment-tableviews.md) | 2.0 | Why Compass Canada's item-fulfillment TableViews 500'd on dead Core ids and how they were rebuilt onto the USA shape; open when a Canada TableView 500s on /meta |
9
+ | [Compass Canada: PO-to-vendor transmission monitor (Monitor/Compass/CompassCanadaPurchaseOrderTransmissionsToVendors)](features/oneuptime-po-transmissions-to-vendors-monitor.md) | 2.0 | worker2 OneUptime push monitor for Compass Canada POs the 1.0 cron has not yet sent to the vendor (Grand & Toy), plus how the Canada PO transmit cron differs fr |
9
10
  | [Compass Canada — Fulfilled vs Partially Fulfilled (per-line, ASN bridge only)](features/order-fulfillment-status-per-line.md) | 2.0 | Compass Canada's pure per-line fulfilled/partiallyFulfilled rule (ASN bridge only) and why it diverges from USA; open before touching Canada `_fulfillmentStatus |
10
11
  | [Compass Canada](profile.md) | 2.0 | Compass Canada is the Canadian arm of the Compass account — a separate TOGA tenant, related to but distinct from Compass USA. |
11
12
  | [Copying a Compass USA user into the Canada tenant (cross-tenant hand-add)](workflows/copying-a-us-user-into-the-canada-tenant.md) | 2.0 | Giving a Compass **USA** person a login on the Canada TechHub portal: the guarded insert chain, the uuid role/persona mapping, and the worker2 guard that stops |
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: "Compass Canada: PO-to-vendor transmission monitor (Monitor/Compass/CompassCanadaPurchaseOrderTransmissionsToVendors)"
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: compass-canada
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-10-05
10
+ owners: ["bala"]
11
+ files:
12
+ - worker2/Worker/Monitor/Compass.php
13
+ - worker2/Worker/Client/CompassCanada.php
14
+ - worker/crons/toga2/compasscanada/workflow/2_transmit_mits_purchase_orders_to_vendors.php
15
+ - worker/schedules/cron.worker.sync.json
16
+ - library/app/client/compasscanada.php
17
+ related:
18
+ - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
19
+ - ../../compass-usa/features/mits-po-transmission-to-vendors.md
20
+ - ./grand-and-toy-asn-import.md
21
+ - ../profile.md
22
+ ---
23
+ worker2 OneUptime push monitor for Compass Canada POs the 1.0 cron has not yet sent to the vendor (Grand & Toy), plus how the Canada PO transmit cron differs from USA; open when Canada POs are not reaching G&T or when tuning this monitor.
24
+
25
+ ## Summary
26
+ `_Worker_Monitor_Compass::CompassCanadaPurchaseOrderTransmissionsToVendors()` counts due, unsubmitted Canada POs and how long the oldest has waited, and pushes an alarm token to OneUptime. Follows the shared [OneUptime push-metric pattern](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md) (payload contract, provisioning, criteria).
27
+
28
+ - Prod `Core.CronJobs` **id 87**, `*/5 * * * *`, action `Monitor/Compass/CompassCanadaPurchaseOrderTransmissionsToVendors`. Live 2026-10-05 (first runs: 0 waiting, alarm=OK, push ok).
29
+ - Push URL = in-method `$ONEUPTIME_URL` local (credential; never record the value). Import JSON is a local artifact, not in the repo.
30
+ - Canada had no PO-to-vendor monitor before this. The US sibling `CompassPurchaseOrderTransmissionsToVendors` only checks count > 10.
31
+
32
+ ## How it works
33
+
34
+ ### Producer being watched (1.0)
35
+ `worker/crons/toga2/compasscanada/workflow/2_transmit_mits_purchase_orders_to_vendors.php`, `*/2` in `worker/schedules/cron.worker.sync.json`. Picks `Client_CompassCanada.PurchaseOrders` with `customerId = 5`, `dtSubmitted IS NULL`, `dateRefresh` NULL or `<= CURDATE()`, vendor id IN (1, 2, 3), and an INNER JOIN `Vendors.defaultPoSubmissionIntegrationId -> Integrations`. Uses LEFT joins elsewhere (no SalesOrders INNER JOIN, unlike USA).
36
+
37
+ ### Query (mirrors the cron)
38
+ - Same WHERE as the cron. `Integrations` is LEFT joined so non-routable POs can be counted.
39
+ - **Routable** = `Integrations.id IS NOT NULL AND Vendors.id IN (1,2,3)`. Only routable POs drive the alarm.
40
+ - **Age** = minutes since `GREATEST(dtCreated, dateRefresh)`, so a future-dated PO is not "late" the moment it becomes due.
41
+ - `unroutableCount` = due POs the cron will never pick up. Reported, never alarmed.
42
+ - Registers `Client_CompassCanada` in-method via `_Worker_Client_CompassCanada::DB_CLIENT_COMPASSCANADA` (the class `initialize()` only registers Compass USA).
43
+
44
+ ### Thresholds (calibrated on prod, last 60 days: every PO sent within 7 min, avg < 1 min, busiest day 17 POs)
45
+ | Signal | DEGRADED | HIGH |
46
+ |---|---|---|
47
+ | `pendingCount` | > 10 | > 20 |
48
+ | `oldestPendingMinutes` | > 15 | > 30 |
49
+ | heartbeat not received | 10 min | 15 min |
50
+
51
+ DB read failure pushes `status:"error"`. Body also carries `lateCount` and all four thresholds.
52
+
53
+ ## Gotchas
54
+ - **Only one real vendor.** All 1,041 Canada POs (2026-04-09 to 2026-10-01) went to Grand & Toy (`Vendors.id 1`, integration 1 = CXML). Prod `Vendors`: 1 GRAND & TOY, 4 COMPASS CANADA, 5 AGILANT SOLUTIONS INC (4 and 5 have no integration).
55
+ - **⚠ 1.0 constants do not match prod.** `App_Client_CompassCanada::VENDOR_ID__COMPASS_CANADA = '2'` and `VENDOR_ID__AGILANT_SOLUTIONS = '3'`, but prod ids are 4 and 5. Cron and monitor both use (1,2,3). Harmless while only G&T gets POs; fix both together if Canada ever routes to a second vendor.
56
+ - **The cXML endpoint row has `isProduction = 0` but IS the live G&T URL** (grandandtoy.com `B2BIntegration/SubmitPO_CXML.aspx`). Not a test endpoint; do not "fix" the flag or skip it.
57
+ - **A non-200 cXML response throws and stops the whole cron run.** Every PO behind it waits. The age threshold catches this.
58
+ - **Non-Ground shipping forces the EMAIL integration (id 2)**, and `dtSubmitted` is stamped after the email is queued with no delivery check. Delivery of an emailed PO cannot be monitored.
59
+ - worker2 has no Canada vendor/customer constants (`_Worker_Client_CompassCanada` only has `DB_CLIENT_COMPASSCANADA`); customer id 5 and vendor list are method locals.
60
+
61
+ ## Change history
62
+ - 2026-10-05 — Created and deployed to prod (cron 87, `*/5`); first runs 0 waiting, alarm=OK. (bala)
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: compass-canada
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-09-28
21
+ updated: 2026-10-05
22
22
  owners: [jcardinal, bala, tcox, apeterson, ajean, mhammontree, rgirish]
23
23
  files: []
24
24
  related:
@@ -37,6 +37,7 @@ related:
37
37
  - features/french-order-email-localization.md
38
38
  - features/order-fulfillment-status-per-line.md
39
39
  - features/item-fulfillment-tableviews.md
40
+ - features/oneuptime-po-transmissions-to-vendors-monitor.md
40
41
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
41
42
  - ../../2.0/apps/toga2-commerce/features/sso-redirect-and-session-gating.md
42
43
  - ../compass-usa/features/saml-sso.md
@@ -75,6 +76,11 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
75
76
  (6 rows here); the fix seeds the catalog as a Surface create-default per tenant.
76
77
 
77
78
  ## Vendors & integrations
79
+ - **One real vendor, customer id 5 (verified 2026-10-05).** Canada `PurchaseOrders.customerId = 5`.
80
+ Every Canada PO (1,041, 2026-04-09 to 2026-10-01) went to Grand & Toy `Vendors.id 1` (integration 1 =
81
+ CXML). Prod `Vendors` 4 COMPASS CANADA and 5 AGILANT SOLUTIONS INC have no integration, and the 1.0
82
+ constants for them (`'2'`, `'3'`) do not match prod. PO transmit cron quirks and the OneUptime monitor
83
+ (cron 87): [PO-to-vendor monitor](features/oneuptime-po-transmissions-to-vendors-monitor.md).
78
84
  - **Grand & Toy (G&T)** — primary hardware vendor. SOs flow toga → MITS → PO to G&T; G&T sends
79
85
  back ASNs. ASN ingestion (email CSV + the auto-created ItemFulfillment chain + bilingual
80
86
  in-transit email) is documented in [Grand & Toy ASN Import](features/grand-and-toy-asn-import.md).
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: compass-usa
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-09-16
8
+ updated: 2026-10-05
9
9
  owners: ["rgirish", "bala", "jcardinal"]
10
10
  files:
11
11
  - worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php
@@ -49,7 +49,7 @@ Renders the PO with **ezpdf** (`Cezpdf`). The "MAIN ITEM LISTING" draws one row
49
49
  - `Logs_Compass.Record` — `recordId` 17 (PurchaseOrder) / 14 (SalesOrder), `primaryKeyId` = the PO/SO `id`. The "transmitted to Vendor" note is the success marker.
50
50
 
51
51
  ### Client variations
52
- Compass Canada has a parallel `compasscanada/workflow/2_transmit_...` keyed on `CUSTOMER_ID__COMPASS_CANADA`.
52
+ Compass Canada has a parallel `compasscanada/workflow/2_transmit_...` keyed on `CUSTOMER_ID__COMPASS_CANADA` (5), sending only to Grand & Toy; it uses LEFT joins (no SalesOrders INNER JOIN). Canada cron differences + its monitor: [Compass Canada PO-to-vendor monitor](../../compass-canada/features/oneuptime-po-transmissions-to-vendors-monitor.md).
53
53
 
54
54
  ## Gotchas
55
55
  - **⚠ Everything transmitted is sourced from the `VendorItem` the PO line points at — a wrong-catalog `vendorItemId` sends the wrong SKU, wrong cost, AND the wrong channel.** The vendor SKU and the cXML-vs-EMAIL routing come entirely from the `VendorItem` that `PurchaseOrderItems.vendorItemId` references (with `PurchaseOrderItems.cost` for price). So a PO line pointing at an **Agilant** VendorItem instead of the Office-Depot one transmits Agilant's **internal SKU** and **wrong cost**, and — because that VendorItem carries **no `overridePoSubmissionIntegrationId`** — falls back to the vendor's default **EMAIL** integration instead of **cXML**. Verified in the legacy `Logs.API`: good order **SA136666** went **cXML** to `b2b.odpbusiness.com` with `SupplierPartID 5775600` / `UnitPrice 945.17`; bad order **SA136846** went **email** with `C40QYUC#ABA` / `$61`. Downstream symptom of the 2026-08-31 catalog fold (a folded PO line resolved to the Agilant twin) — root cause and reversal: [catalog-fold reversal](../workflows/catalog-fold-reversal.md); [two-catalog item model](item-catalogs-and-duplicate-items.md).
@@ -5,8 +5,8 @@ project: TOGa
5
5
  client: pcmaticb2b
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-09-16
9
- owners: [snaredla, jcardinal]
8
+ updated: 2026-10-05
9
+ owners: [snaredla, jcardinal, mhammontree]
10
10
  files:
11
11
  - worker/crons/toga2/startech/sync_togadesk_startech_pcmaticb2b.php
12
12
  - worker/crons/toga2/startech/common_import_supporting_records.php
@@ -177,6 +177,8 @@ const TOGADESK_TICKET_TYPE__TOGADESK_PCS = 'TOGaDesk-PCS';
177
177
  - **Webhook must set `c_escalateToStartech=0`** — without this, the `postPost` gate evaluates `!null = true` and would skip Startech for webhook-originated tickets on any subsequent PUT.
178
178
  - **ticketType uses `c_startechTicketTypeId` dynamically** — stored on `TicketTypes`, not hardcoded in the worker.
179
179
 
180
+ - **⚠ OPEN (found 2026-10-05, not fixed): Startech credentials are exposed in two places.** (1) `_underscore/Component/Api/Startech/Startech.php` hardcodes `STARTECH_API_USERNAME` / `STARTECH_API_PASSWORD` class constants in committed source — rotate, then move to Secrets Manager. (2) `Core.PartnerApiIdentities.partnerApiPassword` holds the Startech password in plaintext and is a STRING RecordField on record 305, so the records API can read it. New partners use Secrets Manager instead (see [8x8 survey import](../../../2.0/apps/worker2/features/8x8-post-call-survey-import.md)).
181
+
180
182
  ### Sync prerequisites & gotchas (2026-07-06)
181
183
  - **The entitlement gate blocks the 2.0 create.** A TOGaDesk ticket only syncs up to TOGA 2.0 (and can then reach Startech) if its email maps to an existing 2.0 contact **with an active entitlement**. `_Model_Pcmaticb2b_Ticket::prePost` rejects the create otherwise, so the desk ticket never gets a `referenceId` — and the escalate handler shows "Ticket is not linked to TOGa 2.0".
182
184
  - **These prePost rejections are now HTTP 400, not 500 (fixed 2026-08-25).** `_Model_Pcmaticb2b_Ticket::prePost()` (the 2.0 pre-interceptor for `POST /v2/tickets`) previously threw a **plain `Exception`** for its four client-actionable rejections — *"Ticket requires a contact…"*, *"Contact not found for uuid=…"*, *"Contact not found for email=…"*, *"No active entitlement found for contact"* — each producing **HTTP 500 (EO-1)** plus a `Logs.Issue` and a Sentry event (the "requires a contact" one was prod `Logs.Issue` ref `FG`, 215 occurrences). All four now throw **`_Exception_Validation`**, which `Controller/Index.php` maps to **HTTP 400** — the client sees it in the response envelope with no Issue/Sentry/5xx-health impact. A client-input / business rejection belongs at 400, not 500. (See the 2.0 backend-php standard, *Choosing an exception class is a ROUTING decision*.)
@@ -195,3 +197,6 @@ const TOGADESK_TICKET_TYPE__TOGADESK_PCS = 'TOGaDesk-PCS';
195
197
  - [Startech webhook handler (worker2)](../../../2.0/apps/worker2/features/startech-webhook-handler.md)
196
198
  - [Library Startech pcmaticb2b sync](../../../1.0/apps/library/features/startech-pcmaticb2b-sync.md)
197
199
  - [Startech entitlement provisioning](startech-entitlement-provisioning.md)
200
+
201
+ ## Change history
202
+ - 2026-10-05 — Recorded open Startech credential exposure (hardcoded constants + plaintext `partnerApiPassword` RecordField) (mhammontree)
@@ -16,7 +16,7 @@ project: _Underscore
16
16
  client: true
17
17
  type: profile
18
18
  status: active
19
- updated: 2026-10-02
19
+ updated: 2026-10-05
20
20
  owners: [jcardinal, kyalamarthi, tcox, rgirish, mhammontree]
21
21
  files: []
22
22
  related:
@@ -24,6 +24,7 @@ related:
24
24
  - ../../2.0/apps/saml/features/gateway-environments-and-redirect-safety.md
25
25
  - ../../2.0/apps/_underscore/features/core-domains-app-host-registry.md
26
26
  - ../../1.0/apps/togadesk/features/staff-sso-login.md
27
+ - ../../2.0/apps/worker2/features/8x8-post-call-survey-import.md
27
28
  ---
28
29
 
29
30
  "True" — TOGa's internal/staff tenant (`Client_True`, identifier `True`, default email SSO mapper); open for its internal apps (Tools, sprint dashboard, togatech site) and its Talos-pricing internal-tenant role.
@@ -44,6 +45,8 @@ The **Tools** app (1.0; repo `tools`) authenticates True users via **SSO** throu
44
45
 
45
46
  **TOGa Desk** (1.0; repo `togadesk`) staff sign in via the same True SSO (TRUE-78064, prod only, not deployed as of 2026-10-02): True user email → one AD People row → existing `TOGaDeskSupport.people` row; no auto-create. Domain row id 261 (repointed from an unused devteam seed row) `uuid d1e526b9-e2fb-4462-935f-ed449e8f5986`, `clientId 1`, `appId 18`, env 1, `https://togadesk.togatech.com/desk/` — live in prod Core 2026-10-02. See [TOGa Desk staff SSO](../../1.0/apps/togadesk/features/staff-sso-login.md).
46
47
 
48
+ **8x8 post-call surveys (CSAT)** for every end client served by TOGA's one 8x8 call-center tenant (Office Depot, Craftex, Walmart, PC Matic) are stored in `Client_True` (module `8x8` in `Client_True/_modules.txt`); the end client is read from `surveyName` / `queueName`. Built on TRUE-82185, not deployed as of 2026-10-05. See [8x8 survey import](../../2.0/apps/worker2/features/8x8-post-call-survey-import.md).
49
+
47
50
  ## ⚠ "Base" maps to Super User in Client_True
48
51
 
49
52
  In `Client_True` the role **named `Base`** maps to **`coreRoleId` 3 = Super User**. Every other
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.941",
3
+ "version": "1.0.943",
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",