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`
|
|
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`
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
+
14-parameter static method. Called from a cron script for each client.
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
53
|
+
## Watermark timestamps
|
|
53
54
|
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**`
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
##
|
|
115
|
+
## Other sync operations within `syncWithTogadesk`
|
|
73
116
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
##
|
|
151
|
+
## TOGA 2 API — critical gotchas
|
|
83
152
|
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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