toga-ai 1.0.159 → 1.0.161

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,3 +7,4 @@
7
7
  | [Branded HTML Email Templates (App_Email_Template)](features/email-templates.md) | `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in the 1.0 (`App_`) framework. | library/app/email/template.php, library/app/email/agilant.php |
8
8
  | [NetSuite SuiteQL/REST API Reference](features/netsuite-suiteql-api-reference.md) | General working reference for the Agilant NetSuite integration: how to authenticate, how SuiteQL behaves, and the confirmed schema of the tables/columns/codes w | library/app/api/netsuite/rest.php, library/ssl/netsuite_ec_key.pem |
9
9
  | [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php |
10
+ | [Startech PC Matic B2B Sync (library)](features/startech-pcmaticb2b-sync.md) | `library/app/api/toga2.php` handles bidirectional ticket sync for PC Matic B2B between TOGaDesk 1.0 and TOGA 2.0. | library/app/api/toga2.php |
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: Startech PC Matic B2B Sync (library)
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: pcmaticb2b
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: [snaredla]
11
+ files:
12
+ - library/app/api/toga2.php
13
+ related:
14
+ - clients/pcmaticb2b/features/startech-ticket-sync.md
15
+ - clients/pcmaticb2b/profile.md
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ `library/app/api/toga2.php` handles bidirectional ticket sync for PC Matic B2B between TOGaDesk 1.0 and TOGA 2.0. All tickets sync in both directions regardless of `c_escalateToStartech` value — the Startech exclusion is enforced by `_Trait_Startech_Ticket`, not here.
21
+
22
+ ## Key Function
23
+
24
+ `App_Api_Toga2::syncToga2TicketIntoTogadesk1Ticket()` (~line 5095)
25
+
26
+ PC Matic B2B path detected via:
27
+ ```php
28
+ $isPcmaticb2bUuid = (self::$togaClientUuid == App_Api_Toga2::CLIENT_UUID_PCMATICB2B);
29
+ ```
30
+
31
+ ## Ticket Type Map: TOGA 2.0 to TOGaDesk
32
+
33
+ ```php
34
+ $ticketTypeMap = [
35
+ TOGA2_TICKET_TYPE_CODE__SUPPORT => TOGADESK_TICKET_TYPE__INCIDENT,
36
+ TOGA2_TICKET_TYPE_CODE__API => TOGADESK_TICKET_TYPE__API,
37
+ TOGA2_TICKET_TYPE_CODE__PCS => TOGADESK_TICKET_TYPE__PCS,
38
+ TOGA2_TICKET_TYPE_CODE__TOGADESK_PCS => TOGADESK_TICKET_TYPE__TOGADESK_PCS,
39
+ ];
40
+ ```
41
+
42
+ ## Ticket Type Map: TOGaDesk to TOGA 2.0
43
+
44
+ Applied at two callsites (~lines 6845 and 7417):
45
+
46
+ ```php
47
+ if ($togadeskTicketTypeCustomFieldValue === TOGADESK_TICKET_TYPE__API) {
48
+ $toga2TicketTypeCode = TOGA2_TICKET_TYPE_CODE__API;
49
+ } elseif ($togadeskTicketTypeCustomFieldValue === TOGADESK_TICKET_TYPE__PCS
50
+ || $togadeskTicketTypeCustomFieldValue === TOGADESK_TICKET_TYPE__TOGADESK_PCS) {
51
+ $toga2TicketTypeCode = TOGA2_TICKET_TYPE_CODE__PCS;
52
+ } else {
53
+ $toga2TicketTypeCode = TOGA2_TICKET_TYPE_CODE__SUPPORT;
54
+ }
55
+ $payload['ticketType'] = ['code' => $toga2TicketTypeCode];
56
+ ```
57
+
58
+ Both `PCS Ticket` and `TOGaDesk-PCS` map to `PCS` in TOGA 2.0.
59
+
60
+ ## Gotchas
61
+
62
+ - **Do not add a NULL early-return guard in this function.** NULL `c_escalateToStartech` means the ticket was created in TOGaDesk — it must still sync to TOGA 2.0. The Startech gate lives in `_Trait_Startech_Ticket::postPost`, not here.
63
+ - **`SUPPORT REQUEST` with a space** — `TOGA2_TICKET_TYPE_CODE__SUPPORT = 'SUPPORT REQUEST'`. Using `'SUPPORT'` silently breaks type matching.
64
+ - **`$isPcmaticb2bUuid` flag** is used for PC Matic B2B-specific branching throughout `syncToga2TicketIntoTogadesk1Ticket()`.
65
+
66
+ ## Change history
67
+
68
+ - 2026-06-23: Initial doc — ticket type maps (both directions), NULL sync behavior, TOGADESK_PCS type added
@@ -9,4 +9,5 @@
9
9
  | [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Notification/Email.php, dbchanges2/Core/2026-05-21 - Monitors.sql |
10
10
  | [NetSuite → TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, test/@dave/probe_clickup_desc_match.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
11
11
  | [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
12
+ | [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 |
12
13
  | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for Teams meeting transcripts produced by a set of organizers, classifies ea | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Startech Webhook Handler (worker2)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: pcmaticb2b
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-23
10
+ owners: [snaredla]
11
+ files:
12
+ - worker2/Worker/Startech.php
13
+ related:
14
+ - clients/pcmaticb2b/features/startech-ticket-sync.md
15
+ - clients/pcmaticb2b/profile.md
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. Sets `c_escalateToStartech = 0` on all webhook-originated tickets so the sync system recognizes them as Startech-sourced without re-posting them back to Startech.
21
+
22
+ ## Key File
23
+
24
+ `worker2/Worker/Startech.php` — `_Worker_Startech::Webhook()`
25
+
26
+ ## How It Works
27
+
28
+ 1. Startech POSTs a webhook event to the TOGA 2.0 worker endpoint
29
+ 2. `Webhook()` extracts `ticket_id` from the payload as `$startechTicketId`
30
+ 3. Searches TOGA 2.0 for an existing ticket with `c_startechTicketId = $startechTicketId`
31
+ 4. If not found: `POST /tickets` (create); if found: `PUT /tickets/{uuid}` (update)
32
+ 5. Builds payload with ticket fields + `c_escalateToStartech: 0`
33
+ 6. `_Trait_Startech_Ticket::postPost` gate: `!($payload->c_escalateToStartech ?? null)` evaluates `!0 = true` — skips re-posting to Startech since ticket already exists there
34
+
35
+ ## Payload Fields
36
+
37
+ ```php
38
+ $payload = [
39
+ 'c_startechTicketId' => $startechTicketId,
40
+ 'c_escalateToStartech' => 0,
41
+ 'number' => (string) $startechTicketId,
42
+ 'ticketType' => ['c_startechTicketTypeId' => $startechPayload->ticket_type],
43
+ 'urgency' => ['c_startechPriorityId' => $startechPayload->ticket_priority],
44
+ 'shortDescription' => $startechPayload->subject,
45
+ 'longDescription' => $startechPayload->description,
46
+ ];
47
+ ```
48
+
49
+ Conditional additions: `ticketStage` (from `ticket_status`), `contact` (from `creator`).
50
+
51
+ ## Gotchas
52
+
53
+ - `c_escalateToStartech: 0` must be in the payload. If omitted, the interceptor still skips Startech (null is also falsy), but the field stays NULL in TOGA 2.0 — making webhook tickets indistinguishable from TOGaDesk-created ones.
54
+ - `ticketType` is resolved via `c_startechTicketTypeId` on `TicketTypes` — not a hardcoded string. Startech type IDs: SR=6, PCS=54, API=201.
55
+
56
+ ## Change history
57
+
58
+ - 2026-06-23: Initial doc — webhook handler, c_escalateToStartech=0 payload field, POST vs PUT logic
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 5 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 6 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 10 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **togadesk** (TOGa Desk) — 7 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
10
10
  - **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
@@ -15,7 +15,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
15
15
  ## 2.0 framework
16
16
 
17
17
  - **_underscore** (_Underscore) _(framework core)_ — 10 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
18
- - **worker2** (Worker) — 8 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
18
+ - **worker2** (Worker) — 9 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
19
19
  - **api2** (API) — 4 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
20
20
  - **dbchanges2** (Database Changes) _(framework core)_ — 1 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
21
21
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -42,6 +42,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
42
42
  - **New York City Department of Education** (`nycdoe`) → [clients/nycdoe/INDEX.md](clients/nycdoe/INDEX.md)
43
43
  - **NYC Health & Hospitals** (`nychh`) → [clients/nychh/INDEX.md](clients/nychh/INDEX.md)
44
44
  - **Office Depot** (`office-depot`) → [clients/office-depot/INDEX.md](clients/office-depot/INDEX.md)
45
+ - **PC Matic B2B Client Profile** (`pcmaticb2b`) → [clients/pcmaticb2b/INDEX.md](clients/pcmaticb2b/INDEX.md)
45
46
  - **Prudential Financial** (`prudential`) → [clients/prudential/INDEX.md](clients/prudential/INDEX.md)
46
47
  - **Quad Graphics** (`quad`) → [clients/quad/INDEX.md](clients/quad/INDEX.md)
47
48
  - **Rate** (`rate`) → [clients/rate/INDEX.md](clients/rate/INDEX.md)
@@ -0,0 +1,6 @@
1
+ # Client: PC Matic B2B Client Profile `pcmaticb2b`
2
+
3
+ | Doc | Framework | Summary | Files |
4
+ |-----|-----------|---------|-------|
5
+ | [PC Matic B2B — Startech Ticket Sync](features/startech-ticket-sync.md) | 1.0 | Bidirectional ticket sync between TOGaDesk 1.0 (client 177), TOGA 2.0 (client 21), and Startech (Easeedesk). | worker/crons/toga2/startech/sync_togadesk_startech_pcmaticb2b.php, library/app/api/toga2.php, togadesk/desk/includes/controllers/quickactions.php, togadesk/desk/template/pages/tickets/manage.php, togadesk/desk/includes/functions.php, worker2/Worker/Startech.php, _underscore/Trait/Startech/Ticket.php, dbchanges2/Client_Pcmaticb2b/2026-06-22-pcmaticb2b-enhancements.sql |
6
+ | [PC Matic B2B Client Profile](profile.md) | 1.0 | PC Matic B2B is a client using TOGaDesk 1.0 for ticket management, with TOGA 2.0 as the data layer and Startech (Easeedesk) as an external ticketing system for | worker/crons/toga2/startech/sync_togadesk_startech_pcmaticb2b.php, togadesk/desk/includes/functions.php |
@@ -0,0 +1,159 @@
1
+ ---
2
+ title: PC Matic B2B — Startech Ticket Sync
3
+ framework: "1.0"
4
+ project: TOGa
5
+ client: pcmaticb2b
6
+ type: client-feature
7
+ status: active
8
+ updated: 2026-06-23
9
+ owners: [snaredla]
10
+ files:
11
+ - worker/crons/toga2/startech/sync_togadesk_startech_pcmaticb2b.php
12
+ - library/app/api/toga2.php
13
+ - togadesk/desk/includes/controllers/quickactions.php
14
+ - togadesk/desk/template/pages/tickets/manage.php
15
+ - togadesk/desk/includes/functions.php
16
+ - worker2/Worker/Startech.php
17
+ - _underscore/Trait/Startech/Ticket.php
18
+ - dbchanges2/Client_Pcmaticb2b/2026-06-22-pcmaticb2b-enhancements.sql
19
+ related:
20
+ - clients/pcmaticb2b/profile.md
21
+ - 2.0/apps/worker2/features/startech-webhook-handler.md
22
+ - 1.0/apps/library/features/startech-pcmaticb2b-sync.md
23
+ ---
24
+
25
+ ## Summary
26
+
27
+ Bidirectional ticket sync between TOGaDesk 1.0 (client 177), TOGA 2.0 (client 21), and Startech (Easeedesk). The goal is full data parity between 1.0 and 2.0 (long-term 1.0 retirement path), while Startech only receives tickets that originated there or were explicitly escalated by an analyst.
28
+
29
+ ## c_escalateToStartech Field
30
+
31
+ The `Client_Pcmaticb2b.Tickets.c_escalateToStartech` TINYINT(1) field controls sync behavior:
32
+
33
+ | Value | Meaning | Syncs 1.0↔2.0 | Posts to Startech |
34
+ |-------|---------|----------------|-------------------|
35
+ | `NULL` | Created in TOGaDesk manually | Yes | No |
36
+ | `0` | Originated from Startech (webhook-created) | Yes | No (already there) |
37
+ | `1` | Escalated via "Escalate to Startech" button | Yes | Yes |
38
+
39
+ Column: `TINYINT(1) UNSIGNED NULL DEFAULT NULL`
40
+
41
+ ## Ticket Type Map
42
+
43
+ ### TOGA 2.0 to TOGaDesk 1.0
44
+
45
+ | TOGA 2.0 Code | TOGaDesk Name | TOGaDesk Dept |
46
+ |---------------|---------------|---------------|
47
+ | `SUPPORT REQUEST` | Startech Incident | 336 |
48
+ | `PCS` | PCS Ticket | 337 |
49
+ | `API` | API Ticket | 338 |
50
+ | `TOGADESK_PCS` | TOGaDesk-PCS | 337 |
51
+
52
+ ### TOGaDesk 1.0 to TOGA 2.0
53
+
54
+ | TOGaDesk Name | TOGA 2.0 Code |
55
+ |---------------|---------------|
56
+ | `API Ticket` | `API` |
57
+ | `PCS Ticket` | `PCS` |
58
+ | `TOGaDesk-PCS` | `PCS` |
59
+ | anything else | `SUPPORT REQUEST` |
60
+
61
+ `TOGADESK_PCS` maps to `PCS` in TOGA 2.0 — it is the pre-escalation placeholder type for manually created tickets destined for PCS.
62
+
63
+ ## Startech Ticket Type IDs (c_startechTicketTypeId)
64
+
65
+ | Code | Startech ID |
66
+ |------|-------------|
67
+ | `SUPPORT REQUEST` | 6 |
68
+ | `PCS` | 54 |
69
+ | `API` | 201 |
70
+
71
+ The `ticketType` on Startech-originated tickets is resolved dynamically via `c_startechTicketTypeId` on `TicketTypes` — not hardcoded.
72
+
73
+ ## Sync Direction 1: Startech to TOGA 2.0 (Webhook)
74
+
75
+ `worker2/Worker/Startech.php` — `_Worker_Startech::Webhook()`
76
+
77
+ 1. Startech POSTs a webhook event on ticket create/update
78
+ 2. Worker looks up TOGA 2.0 ticket by `c_startechTicketId`
79
+ 3. Creates (POST) or updates (PUT) the ticket in TOGA 2.0
80
+ 4. Payload includes `c_escalateToStartech: 0` — marks it as Startech-originated
81
+ 5. `_Trait_Startech_Ticket::postPost` gate evaluates `!0 = true` — skips re-posting to Startech (already there)
82
+
83
+ ## Sync Direction 2: TOGA 2.0 to TOGaDesk 1.0 (CRON)
84
+
85
+ `library/app/api/toga2.php` — `App_Api_Toga2::syncToga2TicketIntoTogadesk1Ticket()`
86
+
87
+ CRON calls `syncWithToga()` every 2 minutes. All PC Matic B2B tickets sync regardless of `c_escalateToStartech` value — NULL, 0, and 1 all sync to TOGaDesk. Ticket type is mapped using the TOGA 2.0 to TOGaDesk map above.
88
+
89
+ ## Sync Direction 3: TOGaDesk 1.0 to TOGA 2.0 (CRON)
90
+
91
+ `library/app/api/toga2.php` — called by `syncWithTogadesk()` in the same CRON
92
+
93
+ Pushes TOGaDesk ticket changes back to TOGA 2.0. Ticket type mapped using the TOGaDesk to TOGA 2.0 map above.
94
+
95
+ ## Sync Direction 4: TOGA 2.0 to Startech (Interceptors)
96
+
97
+ `_underscore/Trait/Startech/Ticket.php`
98
+
99
+ - `postPost`: fires on new TOGA 2.0 ticket creation. Gate: `if (!($payload->c_escalateToStartech ?? null)) { return; }` — only proceeds when value is `1`. NULL and `0` both evaluate to falsy and skip.
100
+ - `postPut`: fires on update. Creates in Startech when `c_escalateToStartech` flips to `1` and no `c_startechTicketId` yet.
101
+
102
+ ## Escalate to Startech Button
103
+
104
+ Shown on the TOGaDesk manage ticket page only for client 177. Handler: `quickactions.php` case `"escalateToStartech"`.
105
+
106
+ Flow:
107
+ 1. Confirm dialog: "Escalate this ticket to Startech? The ticket type will be changed to PCS."
108
+ 2. Loads ticket, gets TOGA 2.0 UUID from `referenceId`
109
+ 3. GETs PCS ticket type UUID from TOGA 2.0 (`code = 'PCS'`)
110
+ 4. PUTs `/tickets/{uuid}` with `c_escalateToStartech: 1` + `ticketType: {uuid: $pcsTypeUuid}`
111
+ 5. Sets TOGaDesk `departmentid = 337` (PCS dept) and saves
112
+ 6. `postPut` interceptor fires and creates the Startech ticket
113
+ 7. History comment added, page redirects
114
+
115
+ ## DB Migration
116
+
117
+ `dbchanges2/Client_Pcmaticb2b/2026-06-22-pcmaticb2b-enhancements.sql`
118
+
119
+ - `Contacts.c_togaCustomerId` — TOGaDesk 1.0 customer ID cross-reference
120
+ - `Tickets.c_escalateToStartech` — sync state flag (see above)
121
+ - `TicketTypes.c_togadeskTicketDepartmentId` — maps TOGA 2.0 ticket type to TOGaDesk dept
122
+ - Department UPDATE: SR=336, PCS=337, API=338
123
+ - INSERT `TOGADESK_PCS` ticket type (code=`TOGADESK_PCS`, dept=337)
124
+ - All new fields registered in `CustomRecordFields` + `AclCustomFieldPermissions`
125
+
126
+ ## Constants
127
+
128
+ ### togadesk/desk/includes/functions.php
129
+ ```php
130
+ const CLIENT_PCMATICB2B = 177;
131
+ const CLIENT_DEPARTMENT_PCMATICB2B_PCS = 337;
132
+ ```
133
+
134
+ ### sync_togadesk_startech_pcmaticb2b.php
135
+ ```php
136
+ const TOGADESK_PCMATICB2B_CLIENT_ID = 177;
137
+ const TOGADESK_PCMATICB2B_STARTECH_DEPARTMENT_ID = 326;
138
+ const TOGA_PCMATICB2B_CLIENT_ID = 21;
139
+ const TOGA2_TICKET_TYPE_CODE__SUPPORT = 'SUPPORT REQUEST';
140
+ const TOGA2_TICKET_TYPE_CODE__PCS = 'PCS';
141
+ const TOGA2_TICKET_TYPE_CODE__API = 'API';
142
+ const TOGA2_TICKET_TYPE_CODE__TOGADESK_PCS = 'TOGADESK_PCS';
143
+ const TOGADESK_TICKET_TYPE__INCIDENT = 'Startech Incident';
144
+ const TOGADESK_TICKET_TYPE__PCS = 'PCS Ticket';
145
+ const TOGADESK_TICKET_TYPE__API = 'API Ticket';
146
+ const TOGADESK_TICKET_TYPE__TOGADESK_PCS = 'TOGaDesk-PCS';
147
+ ```
148
+
149
+ ## Gotchas / Known Issues
150
+
151
+ - **`SUPPORT REQUEST` not `SUPPORT`** — the DB value has a space. Using `'SUPPORT'` breaks type matching silently.
152
+ - **TOGADESK_PCS maps to PCS in TOGA 2.0** — both `PCS Ticket` and `TOGaDesk-PCS` TOGaDesk types map to the `PCS` code in TOGA 2.0. Intentional.
153
+ - **NULL tickets sync 1.0 and 2.0** — Startech exclusion for NULL is enforced by `_Trait_Startech_Ticket`, not the CRON. Do not add a NULL early-return guard in `syncToga2TicketIntoTogadesk1Ticket()`.
154
+ - **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.
155
+ - **ticketType uses c_startechTicketTypeId dynamically** — stored on `TicketTypes`, not hardcoded in the worker.
156
+
157
+ ## Change history
158
+
159
+ - 2026-06-23: Initial doc — full bidirectional sync, c_escalateToStartech semantics, ticket type maps, escalate button, DB migration, constants, gotchas
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: PC Matic B2B Client Profile
3
+ framework: "1.0"
4
+ project: TOGa
5
+ client: pcmaticb2b
6
+ type: client-feature
7
+ status: active
8
+ updated: 2026-06-23
9
+ owners: [snaredla]
10
+ files:
11
+ - worker/crons/toga2/startech/sync_togadesk_startech_pcmaticb2b.php
12
+ - togadesk/desk/includes/functions.php
13
+ related:
14
+ - clients/pcmaticb2b/features/startech-ticket-sync.md
15
+ ---
16
+
17
+ ## Summary
18
+
19
+ PC Matic B2B is a client using TOGaDesk 1.0 for ticket management, with TOGA 2.0 as the data layer and Startech (Easeedesk) as an external ticketing system for escalated/inbound tickets. All three systems sync bidirectionally.
20
+
21
+ ## Key IDs
22
+
23
+ | Identifier | Value |
24
+ |------------|-------|
25
+ | TOGaDesk 1.0 client ID | 177 |
26
+ | TOGA 2.0 client ID | 21 |
27
+ | TOGA 2.0 client UUID constant | `App_Api_Toga2::CLIENT_UUID_PCMATICB2B` |
28
+ | TOGA 2.0 API UUID constant | `App_Api_Toga2::API_UUID_PCMATICB2B` |
29
+ | TOGA 2.0 API secret constant | `App_Api_Toga2::API_SECRET_PCMATICB2ID` |
30
+ | Startech department in TOGaDesk | 326 |
31
+
32
+ ## Departments (TOGaDesk 1.0)
33
+
34
+ | Constant | ID | Ticket Type |
35
+ |----------|----|-------------|
36
+ | `TOGADESK_PCMATICB2B_DEPARTMENT_ID__SUPPORT_REQUEST` | 336 | Support Request |
37
+ | `TOGADESK_PCMATICB2B_DEPARTMENT_ID__PCS` | 337 | PCS / TOGaDesk-PCS |
38
+ | `TOGADESK_PCMATICB2B_DEPARTMENT_ID__API` | 338 | API |
39
+
40
+ ## Ticket Type IDs in Startech (Easeedesk)
41
+
42
+ | TOGA 2.0 Code | Startech Type ID |
43
+ |---------------|-----------------|
44
+ | `SUPPORT REQUEST` | 6 |
45
+ | `PCS` | 54 |
46
+ | `API` | 201 |
47
+
48
+ ## CRON Entry Point
49
+
50
+ `worker/crons/toga2/startech/sync_togadesk_startech_pcmaticb2b.php` — runs every 2 minutes.
51
+
52
+ Calls:
53
+ 1. `App_Api_Toga2::syncWithToga()` — pulls TOGA 2.0 tickets into TOGaDesk
54
+ 2. `App_Api_Toga2::syncWithTogadesk()` — pushes TOGaDesk tickets to TOGA 2.0
55
+
56
+ ## Change history
57
+
58
+ - 2026-06-23: Initial profile — client IDs, departments, Startech ticket type IDs, CRON entry point
@@ -2,5 +2,5 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
- | [Credit Card Receipt Processing](features/receipt-processing.md) | 2.0 | Automated processing of credit card receipts uploaded to SharePoint. | worker2/Worker/Client/TowFoundation.php, worker2/Worker/Client/TowFoundation/ProcessReceipts.php, worker2/Worker/Client/TowFoundation/TowFoundationVendors.php |
5
+ | [Credit Card Receipt Processing](features/receipt-processing.md) | 2.0 | Automated processing of credit card receipts uploaded to SharePoint. | worker2/Worker/Client/TowFoundation.php, worker2/Worker/Client/TowFoundation/ProcessReceipts.php, worker2/Worker/Client/TowFoundation/TowFoundationVendors.php, worker2/Worker/Client/TowFoundation/TowFoundationCategories.php |
6
6
  | [Tow Foundation](profile.md) | 2.0 | Tow Foundation is a nonprofit client. | |
@@ -5,12 +5,13 @@ project: Worker
5
5
  client: tow-foundation
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-06-18
8
+ updated: 2026-06-23
9
9
  owners: ["rgirish"]
10
10
  files:
11
11
  - worker2/Worker/Client/TowFoundation.php
12
12
  - worker2/Worker/Client/TowFoundation/ProcessReceipts.php
13
13
  - worker2/Worker/Client/TowFoundation/TowFoundationVendors.php
14
+ - worker2/Worker/Client/TowFoundation/TowFoundationCategories.php
14
15
  related:
15
16
  - clients/tow-foundation/profile.md
16
17
  ---
@@ -86,7 +87,7 @@ Amex billing cycle runs 3rd-to-3rd. `computeRefNo()` returns the cycle-end date
86
87
  `TowFoundationVendors.php` holds 2,994 vendor names exported verbatim from Tow Foundation's
87
88
  QuickBooks vendor list (June 2026). `matchQbVendor()` runs three passes against this list:
88
89
  1. Exact match (case-insensitive)
89
- 2. Substring match — QB name contains extracted name or vice versa (min 4 chars); picks shortest match on multiple hits
90
+ 2. Substring match — QB name contains extracted name or vice versa; minimum 3 chars. For needles shorter than 4 chars, a **whole-word boundary check** is used (regex `(?<![a-z])<needle>(?![a-z])`) to prevent short acronyms like `"UPS"` from matching inside `"USPS"`. Picks shortest QB name on multiple hits.
90
91
  3. `similar_text()` similarity ≥ 80% — picks highest scorer
91
92
 
92
93
  Returns the exact QB vendor string or `''` if no confident match. The AI only extracts
@@ -95,11 +96,35 @@ Returns the exact QB vendor string or `''` if no confident match. The AI only ex
95
96
  To update the vendor list: re-export from QuickBooks → run the extraction script →
96
97
  replace `TowFoundationVendors.php`. The matching logic is in `matchQbVendor()` and needs no changes.
97
98
 
98
- ### AI category values
99
- The AI is prompted to return **specific expense types** — e.g. `"Dining"`, `"Travel"`,
100
- `"Accommodation"`, `"Transportation"`, `"Office Supplies"`, `"Entertainment"`,
101
- `"Professional Services"`, `"Subscriptions"`. It must not use broad accounting labels
102
- like `"Administrative Expenses"`.
99
+ ### AI category values and QB mapping
100
+ The AI is prompted to return **specific expense types**. These are then mapped to the exact
101
+ QB account string via `mapCategory()`, which loads `TowFoundationCategories.php` and does
102
+ a case-insensitive lookup. Unmapped values are passed through as-is and logged as warnings.
103
+
104
+ | AI category value | QB account string |
105
+ |---|---|
106
+ | `Dining` | `6640 Dining & Entertainment` |
107
+ | `Entertainment` | `6640 Dining & Entertainment` |
108
+ | `Travel` | `6609 Travel, conferences, meetings` |
109
+ | `Transportation` | `6610 Travel expense` |
110
+ | `Accommodation` / `Lodging` | `6620 Lodging` |
111
+ | `Office Supplies` | `6751 Office supplies` |
112
+ | `Professional Services` | `6400 Professional Fees & Consultants` |
113
+ | `Subscriptions` | `67271 Subscriptions` |
114
+ | `Technology` | `6710 Technology Systems Expense` |
115
+ | `Software` | `6714 Software License Expense` |
116
+ | `Printing` | `6722 Printing expense` |
117
+ | `Postage` | `6721 Postage` |
118
+ | `Membership` | `6730 Membership Dues` |
119
+ | `Insurance` | `6740 Insurance` |
120
+ | `Telephone` | `6755 Telephone & internet expense` |
121
+ | `Photography` | `67241 Photography` |
122
+ | `Board` | `6650 Board Expenses and Board Development` |
123
+ | `Professional Development` | `6735 Staff Professional Development` |
124
+
125
+ To update the mapping: edit `TowFoundationCategories.php` — keys are case-insensitive,
126
+ values must be the exact QB account string. The AI prompt in `extractReceiptData()` lists
127
+ the valid category names; keep them in sync with the map keys.
103
128
 
104
129
  ### Per-person constants
105
130
  `CLASS_MAP` and `PAYMENT_ACCOUNT_MAP` in `ProcessReceipts.php` map each cardholder's name
@@ -158,9 +183,13 @@ Fatal errors send only to `NOTIFY_EMAIL_DEV` (no CC/BCC).
158
183
  - **`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.
159
184
  - **Ole Mole not in QB vendor list** — as of June 2026 this vendor is not in Tow Foundation's QuickBooks, so `qb_vendor` will be blank for Ole Mole charges until they add it to QB and the vendor list is refreshed.
160
185
  - **Duplicate detection is within-run only** — if the same receipt was processed in a prior run and archived, it won't be caught. The dedup only covers receipts present in the current run's `$pendingRenames`.
186
+ - **Unmapped AI categories pass through** — if the AI returns a category string not in `TowFoundationCategories.php`, `mapCategory()` logs a warning and writes the raw AI string to the Excel. Check error logs after a run if the Category column looks odd; add the new value to the map file to fix it.
187
+ - **Short vendor acronyms (< 4 chars) require whole-word match** — the substring pass uses a word-boundary regex for needles under 4 chars to avoid false positives (e.g. `"UPS"` inside `"USPS"`). If a short vendor name is not matching, check that the QB vendor list entry starts or ends with the acronym as a whole word.
188
+ - **Archive restore script** — a one-shot CLI script `/tmp/tow_restore_archives.php` was written 2026-06-23 to move archived files back to their billing-cycle folders for reprocessing. It is not committed; re-create from the session transcript if needed. It uses the production credentials from `Config/production.ini`.
161
189
 
162
190
  ## Change history
163
191
 
192
+ - 2026-06-23 — `TowFoundationCategories.php` added: AI category values now mapped to exact QB account strings via `mapCategory()`; `"Transportation"` → `"6610 Travel expense"` etc. (19 mappings). QB vendor substring match minimum lowered from 4 to 3 chars with whole-word boundary guard for short needles — fixes `"CVS"` → `"CVS Pharmacy"`, `"MTA"` → `"MTA Metro card"`, prevents `"UPS"` → `"USPS"` false positive. (rgirish)
164
193
  - 2026-06-18 — QB vendor matching via local fuzzy match against `TowFoundationVendors.php` (2,994 vendors); AI category prompt updated to return specific types (Dining, Travel, etc.); duplicate receipt detection added (person+date+vendor+amount dedup); `billingCycle` filter parameter added (suffix-match — card-type agnostic); person folder ` CC receipts` suffix stripped so CLASS_MAP/PAYMENT_ACCOUNT_MAP resolve correctly (rgirish)
165
194
  - 2026-06-12 — QB Excel upload moved to root-level `3. QB Excel/`; Archive/exception folders flattened (no person subfolder); unsupported mime types (`application/octet-stream`) now throw ReceiptProcessingException instead of being silently skipped (rgirish)
166
195
  - 2026-06-11 — Added CC (Magdalena), BCC (devteam@togatech.com), per-cardholder Archive subfolders, Excel upload to "3. QB Excel" SharePoint folder, two-pass rename with alphabetical duplicate suffix, rename format changed to `YYYY.MM.DD Full Name_Vendor_Amount` (rgirish)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.159",
3
+ "version": "1.0.161",
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",