toga-ai 1.0.325 → 1.0.327

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.
@@ -9,6 +9,7 @@
9
9
  | [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
10
10
  | [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
11
11
  | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
12
+ | [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
12
13
  | [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP | _underscore/Loader.php, worker2/_.php, api2/_.php, worker2/Component/Forecast/Db/Db.php, worker2/Component/Forecast/SaleImport/SaleImport.php, api2/Component/Api/Netsuite/Netsuite.php |
13
14
  | [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
14
15
  | [Error Reporting — Issue/Event Aggregation (agreed POST-to-receiver design)](features/error-reporting-issue-event.md) | Platform-wide error-reporting infrastructure for TOGA 2.0, built around a two-table **Issue / Event** aggregation model in the shared **Core Logs DB**. | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: _Cloud S3 helpers (copy / get / delete / list)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-13
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - _underscore/Cloud.php
13
+ related:
14
+ - ../../worker2/features/oneuptime-worker2-monitoring.md
15
+ ---
16
+
17
+ ## Summary
18
+
19
+ `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into
20
+ workers or app code. Alongside the existing copy / get / delete methods it now has a
21
+ **LIST** method, `_Cloud::getS3Objects()`.
22
+
23
+ ## Key files / entry points
24
+
25
+ - `_underscore/Cloud.php` — `_Cloud::getS3Objects($s3BucketName, $s3BucketPath = '')`.
26
+
27
+ ## How it works
28
+
29
+ `getS3Objects()` uses the AWS SDK `S3Client->listObjectsV2` and **transparently paginates**
30
+ (`listObjectsV2` caps at 1000 keys per page), returning entries shaped:
31
+
32
+ - `Key` — object key
33
+ - `LastModified` — ISO-8601 string
34
+ - `Size`
35
+
36
+ The method is **purely additive** — no existing `_Cloud` method signature changed. Keeping
37
+ S3 listing here (rather than instantiating `S3Client` in a worker) is the reason to prefer
38
+ this helper over ad-hoc SDK calls.
39
+
40
+ First consumer: the worker2 Office Depot EDI backlog monitor — see
41
+ [OneUptime push-metric monitors](../../worker2/features/oneuptime-worker2-monitoring.md).
42
+
43
+ ## Client variations
44
+
45
+ None — shared core helper.
46
+
47
+ ## Gotchas / known issues
48
+
49
+ - Callers get every matching key regardless of count; pagination is handled internally, so
50
+ do not add your own `ContinuationToken` loop on top.
51
+
52
+ ## Change history
53
+ - 2026-07-13 — Added `_Cloud::getS3Objects()` S3 LIST helper (paginated `listObjectsV2`,
54
+ returns `{Key, LastModified, Size}`); additive, no existing signatures changed. (jcardinal)
@@ -20,6 +20,7 @@
20
20
  | [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, worker/crons/toga2/forecast2/import_supporting_records.php |
21
21
  | [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, _underscore/Model/Client/EmailTemplate.php |
22
22
  | [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
23
+ | [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. | worker2/Worker/Monitor/Compass.php, _underscore/Cloud.php |
23
24
  | [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
24
25
  | [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
25
26
  | [Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)](features/talos-pricing-automation.md) | The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and tools `talos-pricing-ui` docs for the other halves). | worker2/Worker/Talos/Pricing.php, worker2/Database/TalosPricingCrons.sql |
@@ -0,0 +1,128 @@
1
+ ---
2
+ title: OneUptime push-metric monitors for 2.0 workers
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-13
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - worker2/Worker/Monitor/Compass.php
13
+ - _underscore/Cloud.php
14
+ related:
15
+ - ./monitoring-framework.md
16
+ - ../../_underscore/features/cloud-s3-helpers.md
17
+ - ../../../1.0/apps/worker/features/oneuptime-worker-uptime-monitoring.md
18
+ ---
19
+
20
+ ## Summary
21
+
22
+ A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from
23
+ the 1.0 `App_SystemMonitor_Compass` monitors. A worker2 cron action runs a self-contained
24
+ check, decides pass/fail itself, and POSTs a JSON metric body to a OneUptime "Incoming
25
+ Request" monitor — replacing the 1.0 tier's email alerts. First monitor:
26
+ `_Worker_Monitor_Compass::OfficeDepotEdiImportQueue()`, which watches the Office Depot
27
+ inbound EDI import backlog. Designed to be **client-agnostic** — a reusable template for
28
+ future monitors and clients.
29
+
30
+ This is distinct from the DB-driven email-orchestrator
31
+ [Monitoring Framework](./monitoring-framework.md) (`_Worker_Monitor` orchestrator +
32
+ `_Worker_Monitors_<Name>` children + `Core.Monitors` table, sends email). This pattern has
33
+ **no orchestrator and no `Core.Monitors` table** — each monitor is a plain worker2 action
34
+ that reports straight to OneUptime. Note the folder difference: these live under
35
+ `Worker/Monitor/` (singular) vs. the framework's `Worker/Monitors/` (plural).
36
+
37
+ ## Key files / entry points
38
+
39
+ - `worker2/Worker/Monitor/Compass.php` — `abstract class _Worker_Monitor_Compass`; each
40
+ monitor is a `public static` action method returning a summary string recorded in
41
+ `WorkerJobs` (standard worker2 action conventions). First method:
42
+ `OfficeDepotEdiImportQueue()`.
43
+ - `_underscore/Cloud.php` — `_Cloud::getS3Objects()` list helper this monitor relies on
44
+ (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
45
+
46
+ ## How it works
47
+
48
+ Registered as a `Core.CronJobs` entry: schedule `*/5 6-20 * * *`, action
49
+ `Monitor/Compass/OfficeDepotEdiImportQueue`. Each tick, `OfficeDepotEdiImportQueue()`:
50
+
51
+ 1. Lists `s3://agilant-as2/OfficeDepot/` via `_Cloud::getS3Objects()`, then counts files
52
+ older than `AGED_FILE_MINUTES` (10), **excluding** the `OUTBOX/` and `SENT/` sub-prefixes
53
+ and directory-placeholder keys.
54
+ 2. Decides the alarm state itself and POSTs a JSON metric body to the OneUptime "Incoming
55
+ Request" monitor via `_ApiRequest` — with **logging disabled** and
56
+ **`throwExceptionsOnFailure` disabled**, so a failed ping never fails the worker job.
57
+ 3. On S3 failure it still POSTs `{"status":"error"}` so a blind/dead checker is
58
+ distinguishable from a real backlog.
59
+
60
+ Body tokens (see the alarm contract below): `"alarm":"HIGH"` when the aged-file count is
61
+ over threshold, else `"alarm":"OK"`; `"status":"error"` on S3 failure.
62
+
63
+ ### The token-based alarm contract (critical — OneUptime cannot compare numbers)
64
+
65
+ OneUptime **Incoming Request** monitors **cannot** do numeric threshold comparison on a
66
+ pushed body. Verified in OneUptime source (`Common/Server/.../IncomingRequestCriteria.ts`):
67
+ a `checkOn: "Request Body"` filter supports only `Contains` / `NotContains` **string**
68
+ matching — it does **not** JSON-parse the body, does **not** target a nested key, and does
69
+ **not** support Greater Than / Less Than (those filter types apply only to
70
+ outgoing/synthetic checks like Response Time / Status Code / Metric Value).
71
+
72
+ Consequence: the "dumb reporter, smart monitor" ideal (push a raw number, let OneUptime
73
+ compare `> threshold`) is **not achievable for push monitors**. Instead the **worker makes
74
+ the threshold decision** and emits a string token that OneUptime matches with `Contains`.
75
+ OneUptime criteria for this monitor:
76
+
77
+ - Request Body **Contains** `"alarm":"HIGH"` → Offline + incident.
78
+ - Request Body **Contains** `"status":"error"` → Offline + incident.
79
+ - **Online** requires BOTH "received in 1 min" AND Request Body **Contains** `"alarm":"OK"`
80
+ — so status does not flap back to Operational while the alarm is still HIGH.
81
+
82
+ ### Heartbeat / cron cadence timing
83
+
84
+ Heartbeat missed-ping thresholds must match the cron cadence. With a **5-minute** push
85
+ cadence: **Degraded at 10 min / Offline at 15 min**. An initial 3/5-min setting
86
+ false-alarmed on a single missed ping. The Office Depot backlog alarm threshold is
87
+ **10 aged files**.
88
+
89
+ ## Provisioning a monitor (runbook)
90
+
91
+ 1. Write the monitor method on `_Worker_Monitor_Compass` (or a new `_Worker_Monitor_<X>`
92
+ class). Keep it **fully self-contained**: no private helpers, no class constants — all
93
+ per-monitor config lives as `ALL_CAPS` local variables inside the method (PHP disallows
94
+ `const` at function scope), so the class stays clean as monitors accumulate.
95
+ 2. Provision the OneUptime monitor by cloning the reusable import/export template at
96
+ `monitor-import-OfficeDepot-EDI-1.0.json` (a local dev artifact, not a repo file) and
97
+ importing it into OneUptime; wire the Contains criteria above.
98
+ 3. Register the OneUptime push URL as a local/constant in the monitor method — it is a
99
+ **push credential**; never log it and never record its value in a doc.
100
+ 4. Add the `Core.CronJobs` row (`action = 'Monitor/<Class>/<Method>'`) with the schedule,
101
+ and set OneUptime's Degraded/Offline thresholds to match the cadence.
102
+
103
+ ## Client variations
104
+
105
+ None — the pattern is shared infrastructure. The first monitor targets Compass USA's
106
+ Office Depot inbound EDI (vendor ODP), but the class and template are client-agnostic;
107
+ clone the template to provision the same check for another client.
108
+
109
+ ## Gotchas / known issues
110
+
111
+ - OneUptime Incoming Request bodies are matched **as strings only** (Contains/NotContains) —
112
+ no numeric comparison, no JSON key targeting. Always emit a decided token, never a raw
113
+ number, for push monitors.
114
+ - The OneUptime push URL is a credential — keep it as a local/constant in the method; never
115
+ log it or write its value into knowledge docs.
116
+ - Keep the metric POST non-fatal (`throwExceptionsOnFailure` off) so a monitoring outage
117
+ never breaks the worker job it rides in.
118
+
119
+ ## Change history
120
+ - 2026-07-13 — Ported Compass monitoring from the 1.0 worker tier into worker2 reporting to
121
+ OneUptime; built `_Worker_Monitor_Compass::OfficeDepotEdiImportQueue()` (Office Depot EDI
122
+ backlog), the token-based alarm contract (OneUptime Incoming Request can only string-match
123
+ Contains, not compare numbers), and the 5-min cadence → 10/15-min heartbeat timing. (jcardinal)
124
+
125
+ ## Related docs
126
+ - [Monitoring Framework](./monitoring-framework.md) — the parallel DB-driven, email-alert monitoring pattern
127
+ - [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md) — `_Cloud::getS3Objects()` used to list the EDI bucket
128
+ - [OneUptime 1.0 worker uptime monitoring](../../../1.0/apps/worker/features/oneuptime-worker-uptime-monitoring.md) — the 1.0 push-heartbeat predecessor
@@ -17,8 +17,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
17
17
 
18
18
  ## 2.0 framework
19
19
 
20
- - **_underscore** (_Underscore) _(framework core)_ — 29 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
- - **worker2** (Worker) — 27 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
20
+ - **_underscore** (_Underscore) _(framework core)_ — 31 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
+ - **worker2** (Worker) — 28 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
22
  - **api2** (API) — 10 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
24
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -9,6 +9,7 @@ apps:
9
9
  - toga2-commerce
10
10
  - worker
11
11
  - worker1.5
12
+ - worker2
12
13
  - dbchanges2
13
14
  project: _Underscore
14
15
  client: compass-usa
@@ -29,6 +29,8 @@ Optional filter parameters: `limit` (int — caps count processed, **not a dry-r
29
29
 
30
30
  The monthly production procedure is **two explicit per-card runs** (Amex + Ryan Farrell's Mastercard), never a single month sweep — see *Monthly production run procedure* below.
31
31
 
32
+ **Before a real run, use `Preview` to see who actually has receipts for a cycle** (read-only dry-run — downloads nothing, moves nothing, sends no email). See *Preview action (read-only dry-run)* below.
33
+
32
34
  ## Key files / entry points
33
35
 
34
36
  - `TowFoundation.php` — base class; holds all email constants and `initialize()` (empty — no client DB)
@@ -195,6 +197,36 @@ the valid category names; keep them in sync with the map keys.
195
197
  to their QuickBooks Class and Payment Account strings. Ryan Farrell uses Bank of America
196
198
  Mastercard; all others use AMEX Open Credit Card.
197
199
 
200
+ ### Preview action (read-only dry-run)
201
+ `Preview(?string $year = null, ?string $person = null, ?string $billingCycle = null): string` is
202
+ the read-only counterpart to `Run()`. It reuses `walkReceiptsFolder()` and the **same**
203
+ `year`/`person`/`billingCycle` filters, but **only tallies** — it downloads nothing, extracts
204
+ nothing, moves nothing, and sends no email. This is the supported way to answer "who actually has
205
+ receipts for this cycle / is this month mostly empty?" **before** a real, side-effecting `Run()`
206
+ (there is otherwise no dry-run — see *Production-safety facts*).
207
+
208
+ ```json
209
+ {
210
+ "action": "Client/TowFoundation/ProcessReceipts/Preview",
211
+ "parameters": { "year": "2026", "billingCycle": "07-03-2026" }
212
+ }
213
+ ```
214
+
215
+ Returns pretty JSON:
216
+ ```
217
+ { filters, totalReceipts, personsWithReceipts,
218
+ persons: [ { person, receipts (count), cycles: [folder...], fileTypes: {ext => count},
219
+ sampleFiles: [up to 3 names], hasStatement (bool) } ],
220
+ personsWithNoReceipts: [...] }
221
+ ```
222
+
223
+ `sampleFiles` + `fileTypes` were added so a human can confirm the counted files are genuine
224
+ receipts (`pdf`/`jpg`/`png`/`docx`) rather than statements. **Read `persons[]` as the truth:** a
225
+ cardholder is empty for the cycle iff they are **absent from `persons[]`**. `personsWithNoReceipts`
226
+ is a softer signal — it is derived from people who have a **statement** file but no receipts this
227
+ cycle, and it also surfaces stray top-level folders (e.g. `"Processed"`) as pseudo-"persons"
228
+ (cosmetic noise). Do not treat presence in `personsWithNoReceipts` as authoritative.
229
+
198
230
  ### MoveBack action (restore archived receipts for reprocessing)
199
231
  `MoveBack(string $person, string $billingCycle, ?string $year = null): string` is a public
200
232
  static action that walks the `Archive/` folder under a person's year directory and moves all
@@ -259,7 +291,9 @@ Ryan Farrell's Mastercard cycle:
259
291
  list comes entirely from the `NOTIFY_*` constants in `Worker/Client/TowFoundation.php` (see
260
292
  *Email routing*) — the request cannot change who is emailed. Secret/config values (SharePoint
261
293
  credentials, drive/site IDs) live in `TowFoundation.php` constants and `_Config`, never in the
262
- request or this doc.
294
+ request or this doc. **SharePoint credentials** specifically live in the
295
+ `[sharepoint_towfoundation]` section of the per-environment Config ini (e.g. `dev-rohan-mac.ini`)
296
+ and are read via `_Config::sharepoint_towfoundation()` — document only *where*, never the values.
263
297
 
264
298
  ## Email routing
265
299
 
@@ -318,7 +352,8 @@ Fatal errors send only to `NOTIFY_EMAIL_DEV` (no CC/BCC).
318
352
  recorded, a fallback `error_log()`s and defaults to `{personName}/{currentYear}`. (Before
319
353
  2026-07-13 all persons' Excels went to one shared root `Credit Card Receipts/3. QB Excel/`.)
320
354
 
321
- - **Person folder name normalization** — SharePoint folders are named `"Brent Peterkin CC receipts"` but `CLASS_MAP` / `PAYMENT_ACCOUNT_MAP` keys are just `"Brent Peterkin"`. The ` CC receipts` suffix is stripped via regex in `walkReceiptsFolder()`. Without this, Class and Payment Account columns are blank for those persons.
355
+ - **Person folder name normalization — map keys MUST equal the folder-derived name** — SharePoint folders are named `"Brent Peterkin CC receipts"` but `CLASS_MAP` / `PAYMENT_ACCOUNT_MAP` keys are the **normalized folder name** (folder name minus the ` CC receipts` suffix, stripped via regex in `walkReceiptsFolder()`). The person filter and both map lookups all key on this same folder-derived name, so **a map key that does not exactly equal `folder − " CC receipts"` is never hit**. Both maps fall back to `''` on a miss **with no warning**, so a mismatched cardholder silently ships **blank Class + Payment Account** columns. Confirmed real: the folder `"Ligia Marroquin Soto CC receipts"` normalizes to `"Ligia Marroquin Soto"`, but both maps were keyed `"Ligia Marroquin"` (no "Soto") — her rows blanked out silently until the keys were renamed to `"Ligia Marroquin Soto"` (`CLASS_MAP` ⇒ `Administration:Operations`, `PAYMENT_ACCOUNT_MAP` ⇒ `AMEX Open Credit Card:Ligia Marroquin-Soto Amex CC`). When adding a cardholder, copy the exact normalized folder name as the key.
356
+ - **Unmapped cardholder → silent blank columns** — a cardholder with a receipts folder but **no** entry in `CLASS_MAP`/`PAYMENT_ACCOUNT_MAP` gets blank Class + Payment Account (same silent `''` fallback as above). As of 2026-07-13, **Michael Zuber Zander** has a receipts folder but is in **neither** map (and currently has zero receipts in any cycle), so he is left unmapped/uncommented pending his QB Class + Payment Account values from the client. Before running a cardholder for the first time, confirm they exist in both maps.
322
357
  - **Archive paths must use the actual SharePoint folder name, not the normalized name** — the normalized person name (` CC receipts` stripped, spaces replaced) is for **map lookups only**. When building the SharePoint archive path, use the *actual* folder name (e.g. `"Emily Tow CC receipts"`), not the normalized `"Emily Tow"` — otherwise the PATCH move 404s because the path segment does not exist. Keep the normalized name and the real folder name as separate values.
323
358
  - **`billingCycle` filter is suffix-match, not exact** — always pass just the date portion (`"06-03-2026"`), not the full folder name. Passing the full name (`"Amex ending in 06-03-2026"`) also works but would miss Mastercard folders.
324
359
  - **Missing per-cycle statement → no Notes memos (data dependency, not a bug)** — statements
@@ -371,9 +406,21 @@ Fatal errors send only to `NOTIFY_EMAIL_DEV` (no CC/BCC).
371
406
  - **Subway docx line-item memos come from the docx, by design** — the per-ride memos on a subway
372
407
  `.docx` are sourced from the docx line-item description, not the statement `Notes` column. This is
373
408
  intentional; do not "fix" it by routing them through `matchStatementNotes()`.
409
+ - **July 2026 cycle reality (`Preview`, as of 2026-07-13)** — Amex `"07-03-2026"`: **72** real
410
+ receipts across **9** people (Nadia Alia 43, Emily Tow 11, Meghan Lowney 8, Johany Bedon 3, Angela
411
+ Leis 2, Magdalena Minta 2, Eileen Wiseman 1, Kathryn Lockhart 1, Ligia Marroquin Soto 1). Empty for
412
+ July (only earlier cycles): Diane Sierpina, Brent Peterkin, Susan Ransden. **Two cardholders with
413
+ July receipts but NO statement file** → their memos will be AI-inferred (see the missing-statement
414
+ gotcha above): **Ryan Farrell** (Mastercard, expected) and **Magdalena Minta**. Confirms per-person
415
+ runs are correct: people span multiple cycles and the `billingCycle` filter narrows to the target
416
+ cycle.
417
+ - **Michael Zuber Zander unmapped** — see the *Unmapped cardholder* gotcha above; he is in neither
418
+ map and has zero receipts currently. Resolve his QB Class + Payment Account with the client before
419
+ he first appears in a cycle.
374
420
 
375
421
  ## Change history
376
422
 
423
+ - 2026-07-13 — **Read-only Preview action + Ligia map-key fix + July cycle findings.** (1) BUILT: `Preview(?year, ?person, ?billingCycle): string` — a true dry-run that reuses `walkReceiptsFolder()` and the same filters as `Run()` but only tallies (downloads/extracts/moves nothing, sends no email), returning pretty JSON with per-person receipt counts, cycles, fileTypes, up-to-3 sampleFiles, and hasStatement, plus a softer `personsWithNoReceipts` list. Read `persons[]` as the authoritative empty signal; `personsWithNoReceipts` is derived from statement-only folders and also surfaces stray top-level folders (e.g. "Processed") as cosmetic noise. (2) FIXED: `CLASS_MAP`/`PAYMENT_ACCOUNT_MAP` keys for Ligia Marroquin Soto were `"Ligia Marroquin"` (no "Soto") — never hit against the folder-derived `"Ligia Marroquin Soto"`, so her Class + Payment Account silently blanked (both maps fall back to `''` with no warning). Renamed keys to `"Ligia Marroquin Soto"` (Class ⇒ `Administration:Operations`, Payment Account ⇒ `AMEX Open Credit Card:Ligia Marroquin-Soto Amex CC`); broadened the person-folder-normalization gotcha — map keys MUST equal `folder − " CC receipts"`. (3) DISCOVERED: July Amex `"07-03-2026"` has 72 receipts across 9 people (Nadia Alia 43 …); Diane Sierpina, Brent Peterkin, Susan Ransden empty for July; Ryan Farrell + Magdalena Minta have July receipts but no statement → AI-inferred memos; **Michael Zuber Zander** has a receipts folder but is in neither map (and zero receipts) → left unmapped pending client-provided QB Class + Payment Account. Also recorded that SharePoint creds live in the `[sharepoint_towfoundation]` Config ini section, read via `_Config::sharepoint_towfoundation()`. (rgirish)
377
424
  - 2026-07-13 — **Per-person QB Excel location fix + two production findings.** (1) FIXED: `uploadExcelToSharePoint()` now writes each person's generated QB Excel to that person's own `Credit Card Receipts/{Person} CC receipts/{Year}/3. QB Excel/` subfolder (matching the docblock and client expectation) instead of a single shared root `3. QB Excel/`; signature is now `(accessToken, driveId, personFolderName, year, excelName, tmpFile)`, fed by a `$personFolders` map populated first-write-wins in Pass 1 with an `error_log` fallback to `{personName}/{currentYear}`. (2) DISCOVERED (production-critical gotcha): the statement-Notes lookup is EXACT string equality between the statement filename (`cycleKey`) and the receipt's billing-cycle FOLDER name — no date/fuzzy fallback — so a card whose folder has no identically-named statement silently skips the authoritative Notes memo and falls back to AI inference with zero signal (confirmed for Ryan Farrell's Mastercard folder vs. an Amex-named statement); also noted `computeRefNo()` hard-codes cycle-end day `03`, giving Mastercard rows a `…03…` Ref No. (pre-existing, unchanged). (3) DECIDED: monthly production procedure is two explicit per-card `Run` invocations (Amex + Ryan Farrell's Mastercard), NOT a "MM-YYYY month sweep" — the sweep prototype was deliberately reverted because `PAYMENT_ACCOUNT_MAP` is keyed per-person not per-card and would misattribute the QB Payment Account for anyone holding two cards in a swept month; recorded production-safety facts (no dry-run — `limit` still archives + emails; file moves reversible via `MoveBack` but email is not recallable; recipients are compile-time `NOTIFY_*` constants). (rgirish)
378
425
  - 2026-07-09 — **Local reprocess-all-cardholders verification pass — three more fixes + two data/ops findings (code-only, uncommitted).** These fixes were found *after* the six-fix pass earlier the same day, while re-running every cardholder locally and inspecting the uploaded QB files. (1) **Statements-folder detection matched plural `reports` only** — `walkReceiptsFolder()` now matches `stripos(name,'report')` (singular), so `"{Name} report"` folders (Angela Leis, Kathryn Lockhart, Meghan Lowney, Eileen Wiseman) actually load their statement; without this the earlier Notes-memo fix silently did nothing for them in a real `Run()` (folder-name matching is the gate — verifying `matchStatementNotes()` in isolation is not enough). (2) **docx run-boundary word-splitting** — `</w:r>` was replaced with a space, injecting spaces mid-word (`"Innoc ence"`, `"Armstron g"`, `"202 6 . 0 6 .0 3"`) in Emily Tow's subway memos; now replaced with `''` at both sites (`extractDocxData`, `parseDocxRideLines`) — only `</w:p>` is a real line break. (3) **docx "Closing Date" header mis-parsed as first ride** — the ride regex read `"AMEX Closing Date 2026.06.03"` as a ride and swallowed the real first ride; now stripped upfront via `preg_replace('/(?:AMEX|Visa)\s+Closing\s+Date\s+\d{4}[.\/-]\d{1,2}[.\/-]\d{1,2}/i','',$text)` (kept 4-digit dot dates for Jheanelle rather than narrowing ride dates). Data/ops findings (no code change): **Nadia Alia's** June statement is absent from SharePoint → all her memos AI-inferred until the client uploads `"Amex ending in 06-03-2026.xlsx"` with a Notes column; local worker HTTP endpoint 500s on direct `{action,parameters}` calls (post 2026-07-07 `Core.WorkerJobs` dependency absent in dev) → reprocess a cycle via CLI + `MoveBack` (debug_mode redirects mail). Verified correct: Angela, Emily (incl. 14 subway line items), Kathryn, Meghan (Kellari $548.11 flagged), Michael (all 5 charges). Nadia pending client statement; Jheanelle + Diane not yet reprocessed. (rgirish)
379
426
  - 2026-07-09 — **Client QA pass on a production run — six fixes (code-only, not deployed).** (1) Statement `Notes` column is now the **authoritative** payment memo, used verbatim; `loadStatementExcel()` detects `Notes` (exact-match first, guarded substring fallback) — replaces the `isActivityHeadingMemo()` allow-list that dropped hand-written memos (fixed Angela, Emily, Katy, Meghan, Nadia). (2) `matchStatementCandidate($strict)` returns `null` on ambiguous same-amount matches for the Notes memo instead of guessing; `matchStatementNotes()` is strict, description fallback stays best-effort. (3) Dedup now keys on SharePoint `fileId` only — the old person|date|vendor|amount key dropped distinct same-amount charges (Michael lost 2 of 5). (4) docx ride-line regex now accepts dot dates (`[\/.]`) with optional `[:\-]?` delimiter — Jheanelle's dot-format subway docx previously matched 0 lines and produced no output. (5) `buildExcelFileName()` now derives the reporting month from the billing-cycle end (`parseCycleEndDate()`/`resolvePersonCycle()`) not the charge date, so the filename agrees with the Ref No. (Diane). (6) Systemic discovery: non-docx `payment_amount` is AI-OCR only, never reconciled against the statement `Amount` (Meghan $548.11 vs $538.11); added flag-only `statementHasAmount()` + `$warnings` "Amounts to verify" email section — auto-override deferred. (rgirish)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.325",
3
+ "version": "1.0.327",
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",