toga-ai 1.0.317 → 1.0.319

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.
@@ -7,5 +7,6 @@
7
7
  | [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php |
8
8
  | [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, _underscore/Component/Forecast/SaleImport/SaleImport.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
9
9
  | [NetSuite → TOGa Supply Per-Client Sync (thin wrappers)](features/netsuite-togasupply-per-client-sync.md) | Syncs NetSuite transactions (sales orders, purchase orders, invoices, item receipts, item fulfillments, inventory adjustments) into each TOGa Supply (2.0) clien | worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_canon.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql, library/app/api/netsuite/rest.php, library/app/systemmonitor/netsuiteintegration.php |
10
+ | [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL | library/app/worker.php, worker/crons/worker/worker_heartbeat.php |
10
11
  | [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
11
12
  | [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer (`worker/crons/toga2/netsuite/`). | worker/crons/toga2/netsuite/sync_togasupply.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: OneUptime external uptime monitoring for 1.0 workers
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-13
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - library/app/worker.php
13
+ - worker/crons/worker/worker_heartbeat.php
14
+ related: []
15
+ ---
16
+
17
+ ## Summary
18
+
19
+ Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once
20
+ per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL. This is an
21
+ **external** observability layer that sits on top of the pre-existing **internal**
22
+ DB-heartbeat logic in `worker_heartbeat.php` (the `Workers` table / `dtHeartbeat`
23
+ self-recovery/terminate mechanism). If a box dies, OneUptime stops receiving pings and
24
+ fires **Degraded at 3 min** / **Offline at 5 min**, alerting the team independently of the
25
+ internal self-recovery path.
26
+
27
+ ## Key files / entry points
28
+
29
+ - `library/app/worker.php` — `App_Worker::$oneUptimeEndpoints`: a `public static` map keyed
30
+ by `workerName` (`notification`, `database`, `infrastructure`, `toga`, `togadesk`, `sync`,
31
+ `catalog`) → that worker's OneUptime heartbeat push URL. This is where the endpoint URLs
32
+ (with their per-monitor UUIDs) live; treat them as a credential/secret location, not values
33
+ to reproduce elsewhere.
34
+ - `worker/crons/worker/worker_heartbeat.php` — the per-minute heartbeat cron; the OneUptime
35
+ push happens in the block that handles this instance's own `Workers` row.
36
+
37
+ ## How it works
38
+
39
+ 1. The heartbeat cron processes this instance's own `Workers` row and sets
40
+ `dtHeartbeat = NOW()` (existing internal-heartbeat behavior).
41
+ 2. Immediately after, it curl-POSTs to
42
+ `App_Worker::$oneUptimeEndpoints[$row['workerName']]` with a 10s timeout.
43
+ 3. On `curl_errno`, it `error_log()`s the failure (per coding-style rules — no silent
44
+ swallow) but does not abort the cron.
45
+ 4. OneUptime, receiving a ping every minute, keeps the monitor "Online". Missing pings for
46
+ 3 min → **Degraded**; 5 min → **Offline**, which triggers the team alert.
47
+
48
+ Net effect: liveness is now observed from outside the box. The internal `Workers`/`dtHeartbeat`
49
+ stale-cutoff (270s) self-terminate/replace logic is unchanged and still runs in the same cron.
50
+
51
+ ## Provisioning the monitors
52
+
53
+ The OneUptime monitors are **Incoming Request heartbeat** monitors configured
54
+ 1 min online / 3 min degraded / 5 min offline. They were provisioned by exporting the JSON
55
+ of the existing "Sync 1.0" monitor and cloning it into import files for Notification,
56
+ Database, Infrastructure, TOGa, TOGa Desk, and Catalog, then importing each into OneUptime
57
+ manually. To add a new worker's monitor, clone an existing monitor export the same way and
58
+ register the resulting heartbeat URL in `App_Worker::$oneUptimeEndpoints`.
59
+
60
+ ## Gotchas
61
+
62
+ - Do **not** hardcode the heartbeat URLs/UUIDs anywhere but `App_Worker::$oneUptimeEndpoints`
63
+ — they are push credentials. Same for the AWS keys / EIP allocation IDs already present in
64
+ these files: do not copy them into docs or logs.
65
+ - A stale leftover `const ONE_UPTIME_ENDPOINTS = [];` remains near the top of
66
+ `worker_heartbeat.php` (dead code superseded by the `App_Worker` map). It is a known
67
+ pending cleanup — the live source of truth is the `App_Worker::$oneUptimeEndpoints` map.
68
+
69
+ ## Change history
70
+ - 2026-07-13 — Added external OneUptime heartbeat push for all 1.0 workers on top of the
71
+ existing internal DB-heartbeat mechanism; endpoints keyed by workerName in
72
+ `App_Worker::$oneUptimeEndpoints`. (jcardinal)
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 12 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 14 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 15 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
10
10
  - **togadesk** (TOGa Desk) — 9 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
11
11
  - **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
@@ -5,7 +5,7 @@ project: Worker
5
5
  client: tow-foundation
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-07-09
8
+ updated: 2026-07-13
9
9
  owners: ["rgirish"]
10
10
  files:
11
11
  - worker2/Worker/Client/TowFoundation.php
@@ -25,7 +25,9 @@ Excel file per person, uploads it to SharePoint, and sends a summary email.
25
25
 
26
26
  Invocation: `{"action": "Client/TowFoundation/ProcessReceipts/Run", "parameters": {}}`
27
27
 
28
- Optional filter parameters: `limit` (int), `year` (string e.g. `"2026"`), `person` (string e.g. `"Brent Peterkin"`), `billingCycle` (string — matches the **end** of the billing-cycle folder name, e.g. `"06-03-2026"` matches `"Amex ending in 06-03-2026"`, `"Mastercard ending in 06-03-2026"`, etc.).
28
+ Optional filter parameters: `limit` (int — caps count processed, **not a dry-run**; each processed receipt still archives and the email still sends), `year` (string e.g. `"2026"`), `person` (string e.g. `"Brent Peterkin"`), `billingCycle` (string — matches the **end** of the billing-cycle folder name, e.g. `"06-03-2026"` matches `"Amex ending in 06-03-2026"`, `"Mastercard ending in 06-03-2026"`, etc.).
29
+
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.
29
31
 
30
32
  ## Key files / entry points
31
33
 
@@ -37,11 +39,11 @@ Optional filter parameters: `limit` (int), `year` (string e.g. `"2026"`), `perso
37
39
  ### SharePoint folder structure
38
40
  ```
39
41
  Credit Card Receipts/
40
- 3. QB Excel/ ← generated Excel files for ALL persons uploaded here after each run
41
- {Person}/
42
+ {Person} CC receipts/ ← each cardholder's own folder ("Brent Peterkin CC receipts")
42
43
  {Year}/
43
44
  {BillingCycleFolder}/ ← receipt files live here (e.g. "Amex ending in 06-03-2026")
44
45
  {Person} report[s]/ ← .xlsx billing statements (folder name matched on singular "report")
46
+ 3. QB Excel/ ← this person's generated QB import Excel uploaded here (per-person)
45
47
  Archive/ ← successfully processed receipts land here (renamed, flat)
46
48
  Archive/exception/ ← failed receipts land here (original name preserved, flat)
47
49
  ```
@@ -82,7 +84,8 @@ Credit Card Receipts/
82
84
  - PATCH SharePoint to rename + move to `Archive/` (flat — no person subfolder)
83
85
  - **Multi-entry docx expansion** — a `.docx` whose `extractDocxData()` produced multiple `line_items[]` (e.g. a subway-rides document with 14 dated trip entries) is expanded into one Excel row per entry
84
86
  8. **Excel generation** — one `.xlsx` per person (PhpSpreadsheet); columns: Row #, Account Name, QB Vendor, Payment Amount, Date, Payment Method, Payment Memo, QB Description, Class, Category, Payment Account, Ref No.
85
- 9. **SharePoint upload** — each Excel uploaded to root-level `Credit Card Receipts/3. QB Excel/` via Graph API PUT (shared folder for all persons)
87
+ 9. **SharePoint upload** — each person's Excel is uploaded to **that person's own**
88
+ `Credit Card Receipts/{Person} CC receipts/{Year}/3. QB Excel/` folder via Graph API PUT (see *Per-person QB Excel upload location* below)
86
89
  10. **Summary email** — sent with Excel files attached; To: Jheanelle, CC: Magdalena, BCC: devteam@togatech.com. `sendSummaryEmail()` takes a `$warnings` param and renders an **"Amounts to verify"** section listing rows whose amount could not be reconciled against the statement (see below).
87
90
 
88
91
  ### Amount reconciliation (flag-only)
@@ -227,6 +230,37 @@ To re-run one cardholder + cycle after a code fix, without deploying:
227
230
  3. **Email is safe locally** — each `Run()` sends the summary email, but with `debug_mode=1`
228
231
  locally all mail is redirected to `send_debug_emails_to`, so no client contact is reached.
229
232
 
233
+ ### Monthly production run procedure (per-card, NOT a month sweep)
234
+ The agreed monthly ops procedure is **two explicit invocations** of
235
+ `Client/TowFoundation/ProcessReceipts/Run`, one per card — one for the Amex cycle and one for
236
+ Ryan Farrell's Mastercard cycle:
237
+ ```json
238
+ {"year":"2026","billingCycle":"Amex ending in 07-03-2026"}
239
+ {"year":"2026","person":"Ryan Farrell","billingCycle":"Mastercard ending in 07-06-2026"}
240
+ ```
241
+ - `billingCycle` is a **case-insensitive `str_ends_with` suffix match** on the folder name.
242
+ - **Business invariant: one card per cardholder** — everyone is Amex except **Ryan Farrell**,
243
+ who is Mastercard-only.
244
+ - **Why not a single "MM-YYYY month sweep"?** A month-sweep feature (one call processing both
245
+ the Amex and Mastercard cycles together) was prototyped and then **deliberately reverted before
246
+ release**: `PAYMENT_ACCOUNT_MAP` is keyed **per-person, not per-card**, so a sweep would
247
+ misattribute a cardholder's QB Payment Account if a person ever held two cards in one swept
248
+ month. Per-card explicit runs avoid that ambiguity. Do not reintroduce a month sweep without
249
+ first making `PAYMENT_ACCOUNT_MAP` (and Class) card-aware.
250
+
251
+ ### Production-safety facts (there is no dry-run mode)
252
+ - **No dry-run.** `limit` only caps *how many* receipts are processed — each processed receipt
253
+ **still moves to SharePoint `Archive/`** and the **summary email still sends**. `limit` is not
254
+ a preview.
255
+ - **File moves are reversible; the email is not.** Archive moves can be undone with the `MoveBack`
256
+ action (see above), but once the summary email sends it **cannot be recalled**. Treat every
257
+ `Run()` on production data as a real send.
258
+ - **Recipients are compile-time constants, not request-derived.** The completion-email recipient
259
+ list comes entirely from the `NOTIFY_*` constants in `Worker/Client/TowFoundation.php` (see
260
+ *Email routing*) — the request cannot change who is emailed. Secret/config values (SharePoint
261
+ credentials, drive/site IDs) live in `TowFoundation.php` constants and `_Config`, never in the
262
+ request or this doc.
263
+
230
264
  ## Email routing
231
265
 
232
266
  Defined as constants in `TowFoundation.php`:
@@ -273,9 +307,16 @@ Fatal errors send only to `NOTIFY_EMAIL_DEV` (no CC/BCC).
273
307
  - **Archive folders are flat** — processed files land directly in `Archive/` and failed
274
308
  files in `Archive/exception/` with no extra person subfolder. The Archive is already
275
309
  scoped inside `{Person}/{Year}/` so the extra nesting is redundant.
276
- - **QB Excel goes to the root `3. QB Excel/` folder** — all persons' Excels land in
277
- `Credit Card Receipts/3. QB Excel/` (not inside each person's subfolder). This folder
278
- exists at the root of `Credit Card Receipts/` alongside the person folders.
310
+ - **QB Excel goes to each person's own `3. QB Excel/` subfolder** — each cardholder's
311
+ Excel is uploaded to `Credit Card Receipts/{Person} CC receipts/{Year}/3. QB Excel/`,
312
+ **not** a single shared root folder. `uploadExcelToSharePoint(accessToken, driveId,
313
+ personFolderName, year, excelName, tmpFile)` builds the path with the same per-segment
314
+ `rawurlencode` + `ensureFolderPath()` pattern as `renameAndMoveToArchive`/`moveFileToArchive`.
315
+ The `personFolderName`/`year` come from a `$personFolders` map (personName ⇒ {folderName,
316
+ year}) populated **first-write-wins in Pass 1**, before extraction, so the destination is
317
+ recorded even if that person's receipt later fails. If a person's folder/year was never
318
+ recorded, a fallback `error_log()`s and defaults to `{personName}/{currentYear}`. (Before
319
+ 2026-07-13 all persons' Excels went to one shared root `Credit Card Receipts/3. QB Excel/`.)
279
320
 
280
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.
281
322
  - **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.
@@ -289,6 +330,21 @@ Fatal errors send only to `NOTIFY_EMAIL_DEV` (no CC/BCC).
289
330
  folder held Jan–May statements but no `"Amex ending in 06-03-2026.xlsx"` (and even her 05-03
290
331
  file has no `Notes` column). Resolution is a **client action**: upload Nadia's 06-03-2026
291
332
  statement with a `Notes` column, then re-run her. Not a code fix.
333
+ **The match is EXACT string equality between the statement filename and the receipt's
334
+ billing-cycle FOLDER name — there is no date-based or fuzzy fallback, and the miss is
335
+ entirely silent.** `collectStatementFiles` keys each statement by its filename-without-extension
336
+ (`cycleKey`); the later lookup is `parsedStatements[$personName][$billingCycleFolder]`. So a
337
+ cardholder whose receipts live in a differently-named folder than any statement file gets **no
338
+ Notes memo, no exception, and no warning row** — the memo silently falls back to AI inference,
339
+ producing plausible-but-unverified output with zero signal. Confirmed real for **Ryan Farrell**:
340
+ his receipts are in a `"Mastercard ending in 07-06-2026"` folder while his only statement is named
341
+ `"Amex ending in ..."`, so his Notes never load. When adding/extending this worker, treat a missing
342
+ statement as a real defect risk, not a benign case — cross-check that `parsedStatements[$person]`
343
+ is populated for each cardholder in an actual run.
344
+ - **`computeRefNo()` hard-codes the Amex cycle-end day `03`** — so Mastercard rows (Ryan Farrell,
345
+ whose cycle closes on the **6th**) get a Ref No. ending in `…03…` rather than `…06…`. Pre-existing
346
+ data-accuracy quirk (not changed as of 2026-07-13); relevant if Mastercard Ref No. accuracy ever
347
+ matters for the QuickBooks import.
292
348
  - **Vendors not in QB vendor list** — as of June 2026 these vendors are not in Tow Foundation's QuickBooks, so `qb_vendor` is blank for their charges until the client adds them to QB and the vendor list is refreshed: **Ole Mole**, **AMORE PIZZA CAFE**, **Green & Tonic (New Canaan)**, **NCFP (National Center for Philanthropy)**, **Rippling**, **Langan's**. This is a client action, not a code fix.
293
349
  - **Duplicate detection is within-run and `fileId`-keyed only** — dedup keys on the SharePoint
294
350
  drive-item `fileId`, so it catches only a literal re-upload of the same item present in the
@@ -318,6 +374,7 @@ Fatal errors send only to `NOTIFY_EMAIL_DEV` (no CC/BCC).
318
374
 
319
375
  ## Change history
320
376
 
377
+ - 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)
321
378
  - 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)
322
379
  - 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)
323
380
  - 2026-06-29 — **Reversed the over-aggressive memo override.** Replaced `isBareTranportationMemo()` (added earlier same day) with `isActivityHeadingMemo()`: the statement memo now overrides the AI memo **only** when it is a proper activity-purpose heading (e.g. `Grantee meeting:`, `Peer meeting:`), not whenever the AI memo started with `"Transportation:"`. The previous approach replaced semi-useful AI memos with raw bank strings (`"AplPay LYFT …"`, `"UBER"`) on ride-share/taxi rows where the statement has no activity heading — degraded 8 rows on Emily Tow's statement (1, 17, 18, 21, 26, 31, 32, 34). New priority: statement heading > AI memo, with AI memo kept for any non-heading statement description. (rgirish)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.317",
3
+ "version": "1.0.319",
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",