toga-ai 1.0.36 → 1.0.37

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.
@@ -3,4 +3,4 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Library (1.0 Framework) Architecture](architecture.md) | `library` is the shared library repository for **all 1.0 (legacy) applications** — the `App_` framework. | library/_.php, library/app/, library/browser/ |
6
- | [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` contains three private helpers that handle bidirectional file-attachment sync between TOGA 2 and other systems fo | library/app/api/toga2.php |
6
+ | [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
@@ -17,77 +17,150 @@ related:
17
17
 
18
18
  ## Summary
19
19
 
20
- `App_Api_Toga2` in `library/app/api/toga2.php` contains three private helpers that handle
21
- bidirectional file-attachment sync between TOGA 2 and other systems for Elite:
20
+ `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between
21
+ TOGA 2 and TOGaDesk. The top-level entry point is `syncWithTogadesk()`. Three private
22
+ helper methods handle file-attachment sync within that orchestration.
22
23
 
23
- | Method | Direction | Purpose |
24
- |---|---|---|
25
- | `syncFreshserviceConversationAttachmentsIntoToga2` | Freshservice → TOGA 2 | Download attachments from Freshservice presigned S3 URLs and store in TOGA 2 |
26
- | `syncToga2NoteFilesIntoTogadesk1` | TOGA 2 → TOGaDesk | Copy TOGA 2 note files to TOGaDesk reply attachments |
27
- | `syncTogadesk1NoteFilesIntoToga2` | TOGaDesk → TOGA 2 | Copy TOGaDesk reply attachments to TOGA 2 note files |
28
-
29
- ## Key files / entry points
30
-
31
- - `library/app/api/toga2.php` — all three methods live here, called from larger
32
- sync orchestration methods in the same class
33
- - `App_Api_Toga2::send()` — the HTTP client used by all three
24
+ ## Top-level entry point `syncWithTogadesk()`
34
25
 
35
- ## How dedup works (all three methods)
26
+ 14-parameter static method. Called from a cron script for each client.
36
27
 
37
- All three methods avoid re-uploading files by checking what is already linked before
38
- uploading. The pattern uses `depth=5` on the parent ticket-note:
28
+ ```php
29
+ App_Api_Toga2::syncWithTogadesk(
30
+ int $togadeskClientId,
31
+ int $togadeskTicketDepartmentId,
32
+ string $togaClientUuid,
33
+ string $togaApiKey,
34
+ string $togaApiSecret,
35
+ string $integrationType, // 'REPAIR_ORDERS' or 'TICKETS'
36
+ bool $isEnabledToga2ToTogadeskIntegration,
37
+ bool $isEnabledTogadeskToToga2Integration,
38
+ bool $isEnabledToga2ContactsToTogadeskPeopleIntegration,
39
+ bool $isEnabledToga2ItemsUnitsToTogadeskAssetsIntegration,
40
+ bool $isEnabledToga2PredefinedRepliesToTogadeskTicketsPrIntegration,
41
+ bool $isEnabledToga2TicketTeamsToTogadeskGroupsIntegration,
42
+ bool $isEnabledToga2TicketCategoriesToTogadeskCategorySubcategoryItemsIntegration,
43
+ array $monitorTogadeskDepartmentIds = []
44
+ );
45
+ ```
39
46
 
47
+ Constants for `$integrationType`:
40
48
  ```php
41
- $togaNote = App_Api_Toga2::send($clientUuid, $apiKey, $apiSecret,
42
- 'GET', '/ticket-notes/' . $ticketNoteUuid, null, ['depth' => 5], true, $endpoint);
43
-
44
- $existingFileNames = [];
45
- foreach (($togaNote->data->ticketNotes->ticketNoteFiles ?? []) as $link) {
46
- if (!empty($link->file->name)) {
47
- $existingFileNames[] = $link->file->name;
48
- }
49
- }
49
+ App_Api_Toga2::SYNC_WITH_TOGADESK_INTEGRATION_TYPE__REPAIR_ORDERS // = 'REPAIR_ORDERS'
50
+ App_Api_Toga2::SYNC_WITH_TOGADESK_INTEGRATION_TYPE__TICKETS // = 'TICKETS'
50
51
  ```
51
52
 
52
- Dedup is by **filename** — if `$file->name` is already in `$existingFileNames`, skip.
53
+ ## Watermark timestamps
53
54
 
54
- ## TOGA 2 API critical gotchas
55
+ The sync uses two watermarks stored as TOGA 2 `/parameters` records. These are fetched
56
+ at the start of each run and updated at the end:
55
57
 
56
- **Response key is `ticketNoteFiles`, not `ticketNotesFiles`.**
57
- The extra `s` before `Files` causes a silent empty array — dedup never fires and files
58
- get re-uploaded on every sync run. Always use `ticketNoteFiles`.
58
+ | TOGA 2 parameter key | Meaning | Used for |
59
+ |---|---|---|
60
+ | `TOGADESK_LAST_TICKET_INTEGRATION_DATETIME` | Newest `_updated` timestamp of tickets synced from TOGA 2 | Drives TOGA 2 → TOGaDesk query |
61
+ | `TOGA_LAST_TICKET_INTEGRATION_DATETIME` | Newest `timestamp` of tickets/replies synced from TOGaDesk | Drives TOGaDesk → TOGA 2 query |
62
+
63
+ Both default to `2000-01-01 00:00:00` if not yet set (full initial sync).
64
+
65
+ ## TOGA 2 → TOGaDesk sync (`isEnabledToga2ToTogadeskIntegration`)
66
+
67
+ Runs when this flag is `true`. Processes 200 records per page.
68
+
69
+ **Step 1 — Changed tickets:**
70
+ - `GET /tickets?where[_updated > TOGADESK_LAST_TICKET_INTEGRATION_DATETIME]` (paged)
71
+ - Each ticket re-fetched at `depth=5` for full details
72
+ - Calls `syncToga2TicketIntoTogadesk1Ticket($togaTicket)`
73
+
74
+ **Step 2 — Changed ticket-notes (catches notes on already-processed tickets):**
75
+ - `GET /ticket-notes?where[_updated > TOGADESK_LAST_TICKET_INTEGRATION_DATETIME]` (paged)
76
+ - Skips tickets already processed in Step 1 (tracked in `$alreadyProcessedToga2TicketUuids`)
77
+ - Re-fetches the parent ticket at `depth=5`, calls `syncToga2TicketIntoTogadesk1Ticket`
78
+
79
+ **Inside `syncToga2TicketIntoTogadesk1Ticket`:**
80
+ - `isExternal = true` note → creates `tickets_replies` record with `referenceid = ticketNote->uuid`
81
+ - `isExternal = false` note → creates `comments` record with `referenceid = ticketNote->uuid`
82
+ - After creating a new reply: calls `syncToga2NoteFilesIntoTogadesk1($ticketNote, $togadeskReplyId)`
83
+ - Also writes a `tickets_history` row for each new reply
84
+
85
+ ## TOGaDesk → TOGA 2 sync (`isEnabledTogadeskToToga2Integration`)
86
+
87
+ Runs when this flag is `true`.
88
+
89
+ **Ticket selection:**
90
+ ```sql
91
+ SELECT tickets.id,
92
+ GREATEST(
93
+ COALESCE(latestReply.timestamp, tickets.timestamp),
94
+ COALESCE(latestComment.timestamp, tickets.timestamp)
95
+ ) AS ticketIntegrationTimestamp
96
+ FROM tickets
97
+ LEFT JOIN (SELECT ticketid, MAX(timestamp) FROM tickets_replies GROUP BY ticketid) AS latestReply ...
98
+ LEFT JOIN (SELECT ticketid, MAX(timestamp) FROM comments GROUP BY ticketid) AS latestComment ...
99
+ WHERE tickets.clientid = ? AND tickets.departmentId IN (?)
100
+ AND GREATEST(...) > '$dtTogaLastTicketIntegration'
101
+ ```
59
102
 
60
- **`GET /ticket-notes/{uuid}/ticket-note-files` is NOT a list endpoint.**
61
- This nested route uses `childPolicy = MATCH`:
62
- - 0 bridge records → 404 EV-6
63
- - 1 record → 200 single object
64
- - 2+ records → 404 EV-14 ("multiple found, expected one")
103
+ **Per ticket:** calls `syncTogaDesk1TicketIntoToga2Ticket($togadeskTicketId)`
65
104
 
66
- Always use `depth=5` on the parent note for listing — never the nested route.
105
+ **Inside that method (replies loop):**
106
+ - For each `tickets_replies` row without a `referenceId`: POST to `/ticket-notes` in TOGA 2
107
+ with `c_eliteConversationId` (for Elite) or `c_wjeConversationId` (for WJE)
108
+ - Stores returned `ticketNotes.uuid` back into `tickets_replies.referenceId`
109
+ - Calls `syncTogadesk1NoteFilesIntoToga2($togadeskReplyId, $ticketNoteUuid)`
67
110
 
68
- **`throwExceptionOnApiError` (8th param of `App_Api_Toga2::send()`).**
69
- Pass `true` for the dedup GETa valid note UUID should always return 200. Passing
70
- `false` silently swallows errors and the dedup array stays empty.
111
+ **`referenceid` field** in TOGaDesk (`tickets_replies.referenceId`, `comments.referenceId`)
112
+ is the foreign key to TOGA 2 it holds the TOGA 2 `ticketNotes.uuid`. When `referenceId`
113
+ is already populated the row was previously synced and is skipped.
71
114
 
72
- ## Bridge route for linking files to notes
115
+ ## Other sync operations within `syncWithTogadesk`
73
116
 
74
- ```php
75
- // POST /ticket-notes/{uuid}/ticket-note-files
76
- App_Api_Toga2::send($clientUuid, $apiKey, $apiSecret, 'POST',
77
- '/ticket-notes/' . $ticketNoteUuid . '/ticket-note-files',
78
- ['file' => ['uuid' => $fileUuid]],
79
- null, true, $endpoint);
80
- ```
117
+ | Flag | What it syncs | TOGA 2 source → TOGaDesk target |
118
+ |---|---|---|
119
+ | `isEnabledToga2ContactsToTogadeskPeopleIntegration` | Contacts → People | `/contacts` → `people` table |
120
+ | `isEnabledToga2ItemsUnitsToTogadeskAssetsIntegration` | Units → Assets | `/units` → `assets` table |
121
+ | `isEnabledToga2PredefinedRepliesToTogadeskTicketsPrIntegration` | Predefined replies → TicketsPR | `/predefined-replies` → `tickets_pr` table |
122
+ | `isEnabledToga2TicketTeamsToTogadeskGroupsIntegration` | Ticket teams → Groups | `/ticket-teams` → `groups` table |
123
+ | `isEnabledToga2TicketCategoriesToTogadeskCategorySubcategoryItemsIntegration` | Categories → custom field | Three-level tree flattened to comma-separated string, stored in TOGaDesk `tickets_customfields` |
124
+
125
+ ## File sync helpers
126
+
127
+ ### `syncToga2NoteFilesIntoTogadesk1(object $ticketNote, int $togadeskReplyId)`
128
+ Direction: **TOGA 2 → TOGaDesk**
129
+
130
+ 1. `GET /ticket-notes/{uuid}?depth=5` → reads `ticketNoteFiles` array
131
+ 2. `SELECT name FROM files WHERE ticketreplyid = $togadeskReplyId` — existing filename dedup
132
+ 3. For each new file:
133
+ - `GET /files/{uuid}?depth=1` → `base64_decode($fileData->data->files->data)`
134
+ - INSERT `App_Model_TogaDesk_File` (ticketreplyid, name) → get `$fileId`
135
+ - Write to disk: `/var/www/html/ontrack/desk/uploads/{fileId}-{name}`
136
+ - `UPDATE files SET file='{fileId}-{name}', filetype=?, filesize=? WHERE id=?`
137
+
138
+ ### `syncTogadesk1NoteFilesIntoToga2(int $togadeskReplyId, string $ticketNoteUuid)`
139
+ Direction: **TOGaDesk → TOGA 2**
140
+
141
+ 1. `SELECT id, name, file FROM files WHERE ticketreplyid = $togadeskReplyId`
142
+ 2. `GET /ticket-notes/{uuid}?depth=5` → reads `ticketNoteFiles` — existing filename dedup
143
+ 3. For each new file:
144
+ - Read from disk: `/var/www/html/ontrack/desk/uploads/{file}`
145
+ - `POST /files` with base64-encoded data → get `$fileUuid`
146
+ - `POST /ticket-notes/{uuid}/ticket-note-files` with `{file: {uuid: $fileUuid}}`
147
+
148
+ ### `syncFreshserviceConversationAttachmentsIntoToga2(...)`
149
+ Direction: **Freshservice → TOGA 2** — see worker2 doc.
81
150
 
82
- ## Freshservice attachment handling
151
+ ## TOGA 2 API — critical gotchas
83
152
 
84
- Freshservice attachment URLs are **presigned S3 URLs that expire**. The sync must run
85
- immediately at webhook timedo not defer or re-queue the download step.
153
+ **Response key is `ticketNoteFiles`, not `ticketNotesFiles`.**
154
+ The extra `s` causes a silent empty array dedup never fires, files re-upload every run.
86
155
 
87
- ## TOGaDesk file storage
156
+ **`GET /ticket-notes/{uuid}/ticket-note-files` is NOT a list endpoint.**
157
+ `childPolicy = MATCH`: 0 records → 404 EV-6, 2+ records → 404 EV-14.
158
+ Always use `depth=5` on the parent note for listing.
88
159
 
89
- `syncToga2NoteFilesIntoTogadesk1` writes files to disk at:
90
- ```
91
- /var/www/html/ontrack/desk/uploads/{id}-{filename}
92
- ```
93
- Stored as `{id}-{filename}` in the TOGaDesk `files.file` column.
160
+ **`throwExceptionOnApiError` (8th param of `App_Api_Toga2::send()`).**
161
+ Pass `true` for the dedup GET — errors must not silently empty the dedup array.
162
+
163
+ ## Error handling
164
+
165
+ Each ticket sync is wrapped in `try/catch` — exceptions are sent to Sentry and the run
166
+ continues with the next ticket. A single bad ticket does not abort the whole sync.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.36",
3
+ "version": "1.0.37",
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",