toga-ai 1.0.175 → 1.0.177
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.
- package/CLAUDE.md +71 -0
- package/agents/sql-reviewer.md +95 -26
- package/knowledge/1.0/apps/toga/INDEX.md +1 -0
- package/knowledge/1.0/apps/toga/features/bundleconfirmation-cart-preservation.md +68 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/wje-freshservice-sync.md +143 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/wje/INDEX.md +1 -1
- package/knowledge/clients/wje/profile.md +23 -9
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -227,3 +227,74 @@ toga-harness
|
|
|
227
227
|
```
|
|
228
228
|
|
|
229
229
|
See `package.json` for full details.
|
|
230
|
+
|
|
231
|
+
## TOGA Technology Claude Harness
|
|
232
|
+
|
|
233
|
+
Installed from: npm bundle v1.0.174
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### Session Workflow (STRICT — follow every session)
|
|
238
|
+
|
|
239
|
+
**Start of every session — run this first, no exceptions:**
|
|
240
|
+
```
|
|
241
|
+
/kickoff
|
|
242
|
+
```
|
|
243
|
+
This loads the team knowledge base, framework context, and active client config into
|
|
244
|
+
Claude's context. Without it, Claude has no knowledge of TOGA patterns, the codebase
|
|
245
|
+
history, or decisions made by other devs. Do not skip it.
|
|
246
|
+
|
|
247
|
+
**End of every session — run this before closing:**
|
|
248
|
+
```
|
|
249
|
+
/capture
|
|
250
|
+
```
|
|
251
|
+
This saves what you learned, fixed, or decided to the shared knowledge base and
|
|
252
|
+
auto-pushes it to the team git repo. Your teammates get it on their next install.
|
|
253
|
+
If you close without running /capture, the knowledge is lost.
|
|
254
|
+
|
|
255
|
+
**Long session checkpoint (run before switching tasks or closing mid-work):**
|
|
256
|
+
```
|
|
257
|
+
/session-save
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Resuming previous work:**
|
|
261
|
+
```
|
|
262
|
+
/session-resume latest
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### Skills & agents
|
|
268
|
+
|
|
269
|
+
Your Claude Code session already lists every available `/skill` and specialist agent on each
|
|
270
|
+
launch — this file intentionally does **not** duplicate that catalog (a static copy only drifts
|
|
271
|
+
out of date). The rules that matter:
|
|
272
|
+
|
|
273
|
+
- **`/kickoff` first, `/capture` last** — every session (see above).
|
|
274
|
+
- Specialist agents (php-reviewer, sql-reviewer, framework-pattern-checker, planner,
|
|
275
|
+
knowledge-writer, session-capture, cto, cso, devops, harness-optimizer) fire **automatically**
|
|
276
|
+
on the matching file or operation. You can also invoke one explicitly — e.g. "use the
|
|
277
|
+
php-reviewer agent on this file."
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### Knowledge Base
|
|
282
|
+
|
|
283
|
+
Team knowledge lives in `.claude/knowledge/`. It is seeded from the npm bundle
|
|
284
|
+
and grows every time any developer runs `/capture`.
|
|
285
|
+
|
|
286
|
+
- Search it: `node .claude/knowledge.js search --q="payments"`
|
|
287
|
+
- Validate it: `node .claude/knowledge.js validate`
|
|
288
|
+
- Re-index it: `node .claude/knowledge.js index`
|
|
289
|
+
|
|
290
|
+
Every time you edit a file inside `.claude/knowledge/`, the post-edit-validate
|
|
291
|
+
hook runs automatically and warns you of any frontmatter or schema errors.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
### Updating
|
|
296
|
+
|
|
297
|
+
Pull latest skills, agents, and knowledge from the team repo:
|
|
298
|
+
```
|
|
299
|
+
npx toga-ai
|
|
300
|
+
```
|
package/agents/sql-reviewer.md
CHANGED
|
@@ -1,53 +1,122 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sql-reviewer
|
|
3
|
-
description: TOGA SQL reviewer — uses ecc:database-reviewer for deep query analysis (injection, N+1,
|
|
3
|
+
description: TOGA SQL/schema reviewer — uses ecc:database-reviewer for deep query analysis (injection, N+1, indexes), enhanced with the ACTUAL TOGA framework DB conventions for both frameworks (1.0 App_Database/App_Model on mysqli vs 2.0 _Database/_Model), enforcing the knowledge/{1.0,2.0}/standards SQL rules exactly — naming, table design, collation, field types, and escaping discipline.
|
|
4
4
|
model: sonnet
|
|
5
5
|
tools: Read, Grep, Glob, Bash, Agent
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# TOGA SQL Reviewer
|
|
8
|
+
# TOGA SQL / Schema Reviewer
|
|
9
9
|
|
|
10
|
-
Deep SQL quality and security reviewer combining ECC's database expertise with TOGA
|
|
10
|
+
Deep SQL quality and security reviewer combining ECC's database expertise with the TOGA
|
|
11
|
+
framework DB conventions. **This agent enforces the team's published standards verbatim** —
|
|
12
|
+
not a generic ORM model. The two frameworks have materially different DB layers and schema
|
|
13
|
+
rules; getting the framework wrong produces false positives and missed violations, so
|
|
14
|
+
**identify the framework first** and apply only that framework's rules.
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
Authoritative sources (read the relevant one when a call is borderline — do not guess):
|
|
17
|
+
- 1.0: `knowledge/1.0/standards/backend-php.md`, `knowledge/1.0/standards/framework-rules.md`
|
|
18
|
+
- 2.0: `knowledge/2.0/standards/backend-php.md`, `knowledge/2.0/standards/framework-rules.md`
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
## Step 1 — Identify the framework and its DB layer
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
**Framework 2.0 (_underscore):** `_Db::select()`, `_Db::insert()`, `_Db::update()`, `_Db::delete()`
|
|
22
|
+
Determine framework from the class prefix / repo (look it up in `registry.json` if unsure):
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
**Framework 1.0 — `App_` (repos: `library`, `worker`)**
|
|
25
|
+
- Raw queries: `App_Database::query($sql, $linkRegistryString)` — a **static wrapper over mysqli**, with result caching. NOT PDO, NOT prepared statements.
|
|
26
|
+
- Reads: `App_Database::fetchRow()`, `fetchOne()`, `numRows()`. Writes: `affectedRows()`, `getInsertId()`.
|
|
27
|
+
- Cached lookups: `App_Database::lookupField()`, `lookupRecord()` (the latter uses `SELECT *` internally **by design** — do not flag it).
|
|
28
|
+
- Escaping: `App_Database::sqlEscape()` (safe interpolation) and `App_Database::sqlProtect()` (lossy strip-filter for free-form search only). In `browser/` action files, `getVarEscaped('field')` wraps `sqlEscape()`.
|
|
29
|
+
- ORM: `App_Model` subclasses under `app/model/`, columns declared as `FIELDTYPE_*` constants.
|
|
20
30
|
|
|
21
|
-
|
|
31
|
+
**Framework 2.0 — `_underscore` (repos: `_underscore`, `worker2`, `api2`)**
|
|
32
|
+
- Escaping: `_Database::escape()` (safe interpolation) and `_Database::protect()` (lossy strip-filter for free-form search only).
|
|
33
|
+
- Built-in query-result cache enabled by default; `_Database::useQueryCache(false)` for guaranteed-fresh reads after a write.
|
|
34
|
+
- ORM: `_Model` subclasses, columns declared as `FIELD_*` constants; load via `$model->load()`.
|
|
35
|
+
- Background work goes through `_Worker::runTask()` / the queue — never inline.
|
|
22
36
|
|
|
23
|
-
|
|
37
|
+
> **Do not invent an API.** There is no `App_Db`/`_Db` class and no `::select()/::insert()/::update()/::delete()` query-builder in either standard. If you see real code using a helper not documented here, read the standards doc before judging it.
|
|
38
|
+
|
|
39
|
+
**Always CRITICAL, any framework:** raw `mysql_*` calls (removed in PHP 7), and any user input concatenated/interpolated into a SQL string without `sqlEscape()` / `_Database::escape()` (or the model layer).
|
|
40
|
+
|
|
41
|
+
## Step 2 — Delegate deep analysis to ECC database-reviewer
|
|
42
|
+
|
|
43
|
+
Spawn the `ecc:database-reviewer` agent with the framework-correct context:
|
|
24
44
|
|
|
25
45
|
```
|
|
26
|
-
TOGA CONTEXT
|
|
27
|
-
|
|
46
|
+
TOGA CONTEXT — framework <1.0 | 2.0>:
|
|
47
|
+
DB layer is a custom mysqli wrapper (<App_Database | _Database>), NOT Eloquent/Laravel and NOT PDO/prepared statements. Escaping discipline at the call site is the injection control.
|
|
28
48
|
Pay special attention to:
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
49
|
+
- User input interpolated into a hand-written SQL string WITHOUT <App_Database::sqlEscape() | _Database::escape()> → injection (CRITICAL)
|
|
50
|
+
- Use of <sqlProtect() | _Database::protect()> on values meant to be stored intact (it strips % \ / * " ' and " or") → data corruption
|
|
51
|
+
- SELECT * appearing in feature code (allowed only in the framework's own lookupRecord helper)
|
|
52
|
+
- Any <App_Database:: | _Database::> call inside a loop where a loop variable feeds the WHERE → N+1
|
|
53
|
+
- Missing LIMIT on SELECT from large tables (orders/items/fulfillment can be millions of rows)
|
|
54
|
+
- Missing index support for the WHERE/JOIN columns
|
|
33
55
|
```
|
|
34
56
|
|
|
35
|
-
|
|
57
|
+
Maker ≠ checker: do not let the agent that wrote the SQL review it.
|
|
58
|
+
|
|
59
|
+
## Step 3 — TOGA standards enforcement (apply ONLY the in-scope framework)
|
|
60
|
+
|
|
61
|
+
### 3a. SQL injection & escaping (both frameworks)
|
|
62
|
+
- Raw user input in a query string with no escaping and no model layer → **CRITICAL**.
|
|
63
|
+
- `sqlProtect()` / `_Database::protect()` used to "escape" a value that is then stored → **WARNING** (lossy; corrupts data). It is only for sanitizing free-form search input.
|
|
64
|
+
- Prefer the model layer (`App_Model` / `_Model`) over hand-written SQL — note it as **INFO** when hand-written SQL could have used the model.
|
|
65
|
+
|
|
66
|
+
### 3b. SQL formatting (both frameworks — shared rules)
|
|
67
|
+
- All SQL keywords UPPERCASE (`SELECT`, `FROM`, `INNER JOIN`, `WHERE`, `AND`, `ORDER BY`, `LIMIT`, …).
|
|
68
|
+
- Tabs for indentation; each clause on its own line; a line break after each comma in the SELECT list.
|
|
69
|
+
- SQL embedded in PHP: opening/closing quotes on their own lines, query indented.
|
|
70
|
+
- `INNER JOIN`s before outer joins; prefer `LEFT OUTER JOIN` over `RIGHT OUTER JOIN`; each JOIN on its own line.
|
|
71
|
+
- Mixing `AND` and `OR` **requires parentheses**; `AND`/`OR` start a new line; spaces around operators.
|
|
72
|
+
- Subqueries parenthesized, on a new line, indented.
|
|
73
|
+
- Prefer `#` over `--` for SQL comments. → formatting issues are **INFO/WARNING**, not CRITICAL.
|
|
74
|
+
|
|
75
|
+
### 3c. SQL naming (both frameworks — shared, strictly enforced)
|
|
76
|
+
- Databases PascalCase/formal name; **tables PascalCase**; **fields camelCase**.
|
|
77
|
+
- Foreign keys: referenced table → singular → append `Id`, camelCase (`PurchaseOrders` → `purchaseOrderId`). Flag `PurchaseOrdersId`, `purchase_order_id`, etc.
|
|
78
|
+
- Human-readable field must be `name` — never `label`, `type`, `displayName`, `niceName`, `companyName`.
|
|
79
|
+
- Bridge tables: parent first, underscore-joined, plurals aligned with real table names (`Locations_LocationAttributes`).
|
|
80
|
+
- Boolean/flag fields begin with `is` (`isVisible`) and are `UNSIGNED TINYINT`.
|
|
81
|
+
- PK/FK columns must be **UNSIGNED** integers.
|
|
36
82
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
83
|
+
### 3d. Table design — **DIVERGES by framework** (most common false-positive source)
|
|
84
|
+
|
|
85
|
+
| Rule | 1.0 (`App_` / legacy) | 2.0 (`_underscore`) |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| Primary key | `id` UNSIGNED AUTO_INCREMENT, first column | same |
|
|
88
|
+
| Second column | **NO `uuid` column** — do not add one | **`uuid` mandatory**, second column, from `_String::generateUuid()` (never MySQL `UUID()`) |
|
|
89
|
+
| Custom fields | **NO `c_` prefix block** | client custom fields prefixed `c_`, camelCase after, at end of table |
|
|
90
|
+
| Collation | **`latin1_swedish_ci`** — match the tables you JOIN against; do NOT introduce `utf8mb4` into a legacy schema | **`utf8mb4` / `utf8mb4_0900_ai_ci`** required |
|
|
91
|
+
| Engine | InnoDB | InnoDB |
|
|
92
|
+
| Table/field comments | rarely present; encouraged but not enforced | **required**, short & descriptive; `id`/`uuid`/FK comments have mandated formats; don't begin with "The" |
|
|
93
|
+
| Datetime field prefix | legacy uses `dt` prefix (`dtCreated`, `dtUpdated`) | no `dt` prefix |
|
|
94
|
+
| Field-type constants | `FIELDTYPE_*` (e.g. `FIELDTYPE_PRIMARYKEY`) | `FIELD_*` (e.g. `FIELD_PRIMARYKEY`) |
|
|
95
|
+
|
|
96
|
+
→ **Flag a `uuid` column, `c_` field, or `utf8mb4` collation added to a 1.0 table** as a standards violation. **Flag a missing `uuid`, missing comments, or `latin1` collation in a 2.0 table** as a violation. Applying the wrong framework's table rules is itself the error to avoid.
|
|
97
|
+
|
|
98
|
+
2.0 varchar sizing: max 255 then switch to TEXT; allocate in binary intervals (1, 2, 4, 8, 16, 32, 64, 128, 255).
|
|
99
|
+
|
|
100
|
+
### 3e. Destructive-operation safety (both frameworks)
|
|
101
|
+
- Hand-written `UPDATE`/`DELETE` with no `WHERE`, or a model bulk operation with no condition → **CRITICAL** (table wipe).
|
|
102
|
+
- SELECT from a large table with no `LIMIT` where pagination is expected → **WARNING**.
|
|
103
|
+
|
|
104
|
+
### 3f. 2.0-only checks
|
|
105
|
+
- Long-running/background DB work inline in a request instead of dispatched via `_Worker::runTask()` → **WARNING**.
|
|
106
|
+
- Reliance on stale cached reads immediately after a write without `_Database::useQueryCache(false)` → **WARNING**.
|
|
42
107
|
|
|
43
108
|
## Output format
|
|
44
109
|
|
|
45
110
|
```
|
|
46
|
-
## SQL Review — <filename>
|
|
47
|
-
Framework: <1.0 | 2.0> | DB layer: <
|
|
111
|
+
## SQL / Schema Review — <filename>
|
|
112
|
+
Framework: <1.0 | 2.0> | DB layer: <App_Database / App_Model | _Database / _Model>
|
|
48
113
|
|
|
49
|
-
| File:Line | Severity | Issue | Fix |
|
|
50
|
-
|
|
114
|
+
| File:Line | Severity | Standard | Issue | Fix |
|
|
115
|
+
|-----------|----------|----------|-------|-----|
|
|
51
116
|
|
|
52
117
|
**Summary:** X critical, Y warnings, Z info
|
|
118
|
+
Verdict: <SAFE TO MERGE | FIX REQUIRED | BLOCK>
|
|
53
119
|
```
|
|
120
|
+
|
|
121
|
+
`Standard` cites the rule (e.g. "1.0 §SQL Injection Prevention", "2.0 §UUIDs", "shared §Naming/FK").
|
|
122
|
+
Report only what you can ground in the standards above or the source files — no speculative findings.
|
|
@@ -2,3 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
|
+
| [Bundle Confirmation — Cart Preservation When Adding Add-On Services](features/bundleconfirmation-cart-preservation.md) | Fix: adding any add-on service (Data Transfer, Promotional Bundle, New PC Services, etc.) was silently removing the 1-year tech support SKU from the cart. | toga/app/togarefresh2026/servicerequests/bundleconfirmation.php |
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Bundle Confirmation — Cart Preservation When Adding Add-On Services
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: toga
|
|
5
|
+
project: TOGa
|
|
6
|
+
client: office-depot
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-23
|
|
10
|
+
owners: [snaredla]
|
|
11
|
+
files:
|
|
12
|
+
- toga/app/togarefresh2026/servicerequests/bundleconfirmation.php
|
|
13
|
+
related:
|
|
14
|
+
- ../../../clients/office-depot/features/togarefresh2026-linkbuilder-routing.md
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Summary
|
|
18
|
+
Fix: adding any add-on service (Data Transfer, Promotional Bundle, New PC Services, etc.)
|
|
19
|
+
was silently removing the 1-year tech support SKU from the cart. The `?services=` block in
|
|
20
|
+
`bundleconfirmation.php` now only strips tech support when the service being added IS itself
|
|
21
|
+
a tech support plan.
|
|
22
|
+
|
|
23
|
+
## Key files / entry points
|
|
24
|
+
- `toga/app/togarefresh2026/servicerequests/bundleconfirmation.php` — the `$_GET['services']`
|
|
25
|
+
block (lines ~118–150) is the only place this logic lives.
|
|
26
|
+
|
|
27
|
+
## How it works
|
|
28
|
+
Two callers pass `?services=` to `bundleconfirmation.php`:
|
|
29
|
+
|
|
30
|
+
| Caller | Service IDs passed | Expected behavior |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `techsupport.php` (lines 88, 120, 152) | 194, 195, 101900 (tech support plans) | Remove old tech support, add new, set session vars |
|
|
33
|
+
| `additionalservices.php` (lines 277, 288) | Add-on IDs (categories 174, 175, 178, 179) | Append to cart only — never touch existing items |
|
|
34
|
+
|
|
35
|
+
The fixed block:
|
|
36
|
+
```php
|
|
37
|
+
if (in_array($serviceID, $techSupportServiceIds)) {
|
|
38
|
+
// Replacing tech support — remove the old plan before adding the new selection
|
|
39
|
+
$_SESSION['cartProducts'] = array_values(array_filter(...));
|
|
40
|
+
$_SESSION['techSupportServiceId'] = $serviceID;
|
|
41
|
+
$_SESSION['selectedJobJacket'] = 2;
|
|
42
|
+
}
|
|
43
|
+
if (!in_array($serviceID, $_SESSION['cartProducts'])) {
|
|
44
|
+
$_SESSION['cartProducts'][] = $serviceID;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Data model
|
|
49
|
+
Session vars touched:
|
|
50
|
+
- `$_SESSION['cartProducts']` — array of service IDs in the quote
|
|
51
|
+
- `$_SESSION['techSupportServiceId']` — ID of the selected tech support plan
|
|
52
|
+
- `$_SESSION['selectedJobJacket']` — job jacket type (2 = tech support flow)
|
|
53
|
+
|
|
54
|
+
## Gotchas / known issues
|
|
55
|
+
- The old code also called `unset($_SESSION['techSupportServiceId'])` for every add-on.
|
|
56
|
+
This was also a bug — it would flip `$isTechSupportFlow` (line ~296) to false mid-session
|
|
57
|
+
even when tech support was still in the cart. The fix leaves the var alone for add-ons.
|
|
58
|
+
- Four total `cartProducts` removal points exist in the 2026 service request flow. The other
|
|
59
|
+
three are all safe: McAfee replacement (`bundleconfirmation.php:103`, removes only the
|
|
60
|
+
prior McAfee ID), and two empty-value cleanups (lines ~641 and `mcafeeupdates.php:453`).
|
|
61
|
+
- `$techSupportServiceIds = [194, 195, 101900]` — if a new tech support SKU is introduced,
|
|
62
|
+
this array must be updated or the new plan will be treated as an add-on (won't replace old).
|
|
63
|
+
|
|
64
|
+
## Related docs
|
|
65
|
+
- `clients/office-depot/features/togarefresh2026-linkbuilder-routing.md`
|
|
66
|
+
|
|
67
|
+
## Change history
|
|
68
|
+
- 2026-06-23 — Bug fix: add-on services no longer strip tech support from cart. (snaredla)
|
|
@@ -13,3 +13,4 @@
|
|
|
13
13
|
| [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 |
|
|
14
14
|
| [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 |
|
|
15
15
|
| [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 |
|
|
16
|
+
| [WJE Freshservice Sync (worker2)](features/wje-freshservice-sync.md) | WJE ("WJE IT", helpdesk `wje.freshservice.com`) is a **Freshservice**-based help-desk client whose tickets, contacts, assets, groups, categories, and canned res | worker2/Worker/Wje.php, _underscore/Component/Api/Wje/Wje.php, _underscore/Model/Wje/Ticket.php, _underscore/Model/Wje/TicketNote.php, _underscore/Model/Wje/Contact.php, _underscore/Model/Wje/Unit.php, _underscore/Model/Wje/TicketTeam.php, _underscore/Model/Wje/TicketCategory.php, _underscore/Model/Wje/AssetType.php, _underscore/Model/Wje/PredefinedReply.php, library/app/api/wje.php, worker/crons/toga2/wje/import_supporting_records.php, worker/crons/toga2/wje/sync_togasupply_wje.php, worker/crons/notifications/reports/wje/wje_common.php, library/app/systemmonitor/wje.php, dbchanges2/Client_Wje/2024-10-04 - WjeOnboarding.sql |
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: WJE Freshservice Sync (worker2)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: wje
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-23
|
|
10
|
+
owners: ["snaredla"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Wje.php
|
|
13
|
+
- _underscore/Component/Api/Wje/Wje.php
|
|
14
|
+
- _underscore/Model/Wje/Ticket.php
|
|
15
|
+
- _underscore/Model/Wje/TicketNote.php
|
|
16
|
+
- _underscore/Model/Wje/Contact.php
|
|
17
|
+
- _underscore/Model/Wje/Unit.php
|
|
18
|
+
- _underscore/Model/Wje/TicketTeam.php
|
|
19
|
+
- _underscore/Model/Wje/TicketCategory.php
|
|
20
|
+
- _underscore/Model/Wje/AssetType.php
|
|
21
|
+
- _underscore/Model/Wje/PredefinedReply.php
|
|
22
|
+
- library/app/api/wje.php
|
|
23
|
+
- worker/crons/toga2/wje/import_supporting_records.php
|
|
24
|
+
- worker/crons/toga2/wje/sync_togasupply_wje.php
|
|
25
|
+
- worker/crons/notifications/reports/wje/wje_common.php
|
|
26
|
+
- library/app/systemmonitor/wje.php
|
|
27
|
+
- dbchanges2/Client_Wje/2024-10-04 - WjeOnboarding.sql
|
|
28
|
+
related:
|
|
29
|
+
- clients/wje/profile.md
|
|
30
|
+
- 2.0/apps/worker2/features/elite-freshservice-sync.md
|
|
31
|
+
- 1.0/apps/library/features/toga2-api-client-and-bridge.md
|
|
32
|
+
- 2.0/apps/worker2/features/startech-webhook-handler.md
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Summary
|
|
36
|
+
|
|
37
|
+
WJE ("WJE IT", helpdesk `wje.freshservice.com`) is a **Freshservice**-based help-desk client whose
|
|
38
|
+
tickets, contacts, assets, groups, categories, and canned responses are synced into TOGA. Unlike
|
|
39
|
+
Startech (multi-client, resolved dynamically by `company_id`), **WJE is a single hardcoded client** —
|
|
40
|
+
its IDs and credentials are baked into the integration files.
|
|
41
|
+
|
|
42
|
+
Data reaches TOGA two ways: a **real-time Freshservice webhook** handled by `_Worker_Wje::Webhook`, and
|
|
43
|
+
a **daily batch import** of supporting records. A separate 2-minute cron bridges Toga2 ↔ TOGaDesk (see
|
|
44
|
+
`clients/wje/profile.md`).
|
|
45
|
+
|
|
46
|
+
## Key files / entry points
|
|
47
|
+
|
|
48
|
+
- **Webhook handler:** `worker2/Worker/Wje.php` — `abstract _Worker_Wje`, entry `Webhook($payload, $headers)`.
|
|
49
|
+
Reached via `webhook.togahub.com/wje` → WebhookIngestion Lambda → `Core.WorkerJobs` action `Wje/Webhook`
|
|
50
|
+
→ SQS → EB worker.
|
|
51
|
+
- **API clients (two parallel, same creds):** `_Component_Api_Wje` (`_underscore/Component/Api/Wje/Wje.php`,
|
|
52
|
+
used by worker2/2.0) and `App_Api_Wje extends App_Api` (`library/app/api/wje.php`, used by 1.0 crons).
|
|
53
|
+
Both hit `https://wje.freshservice.com` with basic auth; `send($method, $route, $payload)`, 90s timeout,
|
|
54
|
+
1s sleep between calls. **Production only — no test/stage Freshservice instance.**
|
|
55
|
+
- **Crons (worker 1.0):** `crons/toga2/wje/import_supporting_records.php` (daily import),
|
|
56
|
+
`crons/toga2/wje/sync_togasupply_wje.php` (2-min Togadesk bridge),
|
|
57
|
+
`crons/notifications/reports/wje/{wje_monthly_report,wje_quarterly_report,wje_common}.php`.
|
|
58
|
+
- **Models:** `_underscore/Model/Wje/*` (8 classes, see Data model).
|
|
59
|
+
- **Monitoring:** `library/app/systemmonitor/wje.php` (`App_SystemMonitor_WJE`).
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
### Real-time webhook (`_Worker_Wje::Webhook`)
|
|
64
|
+
1. Payload arrives JSON-wrapped under `freshdesk_webhook` (it is Freshservice despite the name):
|
|
65
|
+
`$wjeRecord = (object)((object)json_decode($payload, true))->freshdesk_webhook;`
|
|
66
|
+
2. Branches on shape: **ticket** webhook if `property_exists($wjeRecord, 'ticket_id')`; **asset** webhook
|
|
67
|
+
if `property_exists($wjeRecord, 'asset_display_id')`.
|
|
68
|
+
3. **Ticket path:** upserts associated assets (`associated_asset_ids`), the requester contact
|
|
69
|
+
(`requester_id`), and the agent contact (`agent_id`). Sleeps a few seconds to dodge duplicate-ticket
|
|
70
|
+
races, then **re-fetches the full ticket** from WJE `/tickets/{id_numeric}` (the webhook body omits
|
|
71
|
+
`group_id` and category detail). Resolves the category/subcategory/item hierarchy by **name** against
|
|
72
|
+
TOGA `TicketCategories`. Upserts the ticket (by `c_wjeTicketId`) and syncs conversation notes from
|
|
73
|
+
`/tickets/{id}/conversations` (by `c_wjeConversationId`).
|
|
74
|
+
4. **Asset path:** upserts the unit by asset tag `ASSET-{display_id}`, matches the contact by email when
|
|
75
|
+
`c_wjeUserId` is unavailable, and updates serial/tag/type/contact custom fields.
|
|
76
|
+
|
|
77
|
+
### Daily import (`import_supporting_records.php`)
|
|
78
|
+
Canned responses → `PredefinedReplies` (`c_wjeCannedResponseId`); requesters → `Contacts` (`c_wjeUserId`);
|
|
79
|
+
assets → `Units` (`c_wjeAssetId`, tag `ASSET-{id}`); groups → `TicketTeams` (`c_wjeGroupId`);
|
|
80
|
+
`ticket_form_fields` (field `13000126389`) → `TicketCategories` (`c_wjeCategoryId`, 3-level hierarchy).
|
|
81
|
+
|
|
82
|
+
### Reports & monitoring
|
|
83
|
+
`wje_common.php::generateWJEReport(...)` builds Excel metrics (SLA, first-response, resolution, reopens)
|
|
84
|
+
via PhpSpreadsheet for client 153 / dept 269; monthly cron emails `serviceops@togatech.com` +
|
|
85
|
+
`mhicks@togatech.com`. `App_SystemMonitor_WJE` checks every 5 min (300s window, 30-min reminders) and
|
|
86
|
+
alerts `devteam@togatech.com` on Freshservice/webhook responses > 300, reading both Logs 1.0 (`API`) and
|
|
87
|
+
Logs 2.0 (`Api` in `db_prod_logs_wje`).
|
|
88
|
+
|
|
89
|
+
## Data model
|
|
90
|
+
|
|
91
|
+
Models in `_underscore/Model/Wje/` extend the base `_Model_Client_*` and add WJE linkage fields
|
|
92
|
+
(all `self::FIELD_CHAR`):
|
|
93
|
+
|
|
94
|
+
| Model | Custom field(s) |
|
|
95
|
+
|-------|-----------------|
|
|
96
|
+
| `_Model_Wje_Ticket` | `c_wjeTicketId`, `c_wjeAgentId` |
|
|
97
|
+
| `_Model_Wje_TicketNote` | `c_wjeConversationId` (unique in DB) |
|
|
98
|
+
| `_Model_Wje_Contact` | `c_wjeUserId` |
|
|
99
|
+
| `_Model_Wje_Unit` | `c_wjeAssetId` |
|
|
100
|
+
| `_Model_Wje_TicketTeam` | `c_wjeGroupId` |
|
|
101
|
+
| `_Model_Wje_TicketCategory` | `c_wjeCategoryId` |
|
|
102
|
+
| `_Model_Wje_AssetType` | `c_wjeAssetTypeId` |
|
|
103
|
+
| `_Model_Wje_PredefinedReply` | `c_wjeCannedResponseId` |
|
|
104
|
+
|
|
105
|
+
Schema in `dbchanges2/Client_Wje/2024-10-04 - WjeOnboarding.sql` (+ `2024-10-23.sql`, `2024-10-25.sql`):
|
|
106
|
+
API record, the `c_wje*` CustomRecordFields, TicketStages (Open=2, Pending=3, Resolved=4, Closed=5),
|
|
107
|
+
asset type "Laptop", urgencies (Low/Normal/High/Critical), system params
|
|
108
|
+
`TOGADESK_LAST_TICKET_INTEGRATION_DATETIME` / `TOGA_LAST_TICKET_INTEGRATION_DATETIME`.
|
|
109
|
+
|
|
110
|
+
Hardcoded IDs: `CLIENT_UUID_WJE=c2f5180f-…`, `API_UUID_WJE=4750650f-…` (and `API_SECRET_WJE`) in
|
|
111
|
+
`worker2/Worker/Wje.php`; TOGaDesk client `153`, dept `269` (internal `290`); default group
|
|
112
|
+
`13000155420` / agent `13000771757`; category form field `13000126389`.
|
|
113
|
+
|
|
114
|
+
## Client variations
|
|
115
|
+
|
|
116
|
+
WJE-specific: single hardcoded tenant (no `company_id` resolution); requires a group on every ticket and
|
|
117
|
+
falls back to the default group/agent when none is assigned. Sister Freshservice client **Elite** uses a
|
|
118
|
+
parallel but separate `_Worker_Elite` handler — see related doc.
|
|
119
|
+
|
|
120
|
+
## Gotchas / known issues
|
|
121
|
+
|
|
122
|
+
- **W1** Payload is wrapped under `freshdesk_webhook`, not top-level. Decode with `json_decode($p, true)` then cast.
|
|
123
|
+
- **W2** Webhook body is thin — `ticket_category`/`group_id` often null; the handler re-fetches `/tickets/{id_numeric}`. Don't trust the webhook body for category/group.
|
|
124
|
+
- **W3** Two id forms: `ticket_id` (string "INC-46687", stored in `c_wjeTicketId`) vs `id_numeric` (46687, used in API calls). Mixing them → 404s / missed matches.
|
|
125
|
+
- **W4** Webhook resolves category by **name** against TOGA `TicketCategories`; the **daily import** is what populates `c_wjeCategoryId`. A WJE rename before the import runs breaks name matching — check the import cron first.
|
|
126
|
+
- **W5** Units keyed by asset tag `ASSET-{display_id}`, linked via `c_wjeAssetId`; asset webhook falls back to email matching when `c_wjeUserId` is absent.
|
|
127
|
+
- **W6** Asset import catches per-asset errors and continues; if WJE revokes `/assets` API access it silently skips assets — check logs.
|
|
128
|
+
- **W7** `c_wjeUserId` is the source of truth for contact identity; email is only opportunistically added.
|
|
129
|
+
- **W8** `_Component_Api_Wje` is production-only — local testing hits production WJE; be careful with writes.
|
|
130
|
+
- **W9** Credentials are duplicated in `_Component_Api_Wje` and `App_Api_Wje` — rotate **both**.
|
|
131
|
+
- **W10** Lazy-transaction rule: write back `c_wje*` identifiers via direct model `->save()`, not an internal API round trip inside an open transaction (cf. Startech G4).
|
|
132
|
+
- **W11** `json_decode($s, true)` + top-level `(object)` cast leaves nested fields as arrays — cast each level you traverse.
|
|
133
|
+
- **W12** TicketStages use Freshservice status codes (Open=2 … Closed=5); status sync depends on those matching live Freshservice statuses.
|
|
134
|
+
|
|
135
|
+
> Secrets (`API_SECRET_WJE`, the Freshservice password) are hardcoded in source — read them from the file
|
|
136
|
+
> when needed; never copy the values into docs or commits.
|
|
137
|
+
|
|
138
|
+
## Related docs
|
|
139
|
+
|
|
140
|
+
- `clients/wje/profile.md` — WJE client profile + the 2-min Togadesk bridge sync.
|
|
141
|
+
- `2.0/apps/worker2/features/elite-freshservice-sync.md` — sister Freshservice client.
|
|
142
|
+
- `2.0/apps/worker2/features/startech-webhook-handler.md` — the multi-client webhook pattern WJE predates.
|
|
143
|
+
- `1.0/apps/library/features/toga2-api-client-and-bridge.md` — `App_Api_Toga2::syncWithTogadesk`.
|
package/knowledge/INDEX.md
CHANGED
|
@@ -11,12 +11,12 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
11
11
|
- **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
|
|
12
12
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
13
13
|
- **test** (Test) — 11 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
|
|
14
|
-
- **toga** (TOGa) —
|
|
14
|
+
- **toga** (TOGa) — 2 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
|
|
15
15
|
|
|
16
16
|
## 2.0 framework
|
|
17
17
|
|
|
18
18
|
- **_underscore** (_Underscore) _(framework core)_ — 11 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
19
|
-
- **worker2** (Worker) —
|
|
19
|
+
- **worker2** (Worker) — 12 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
20
20
|
- **api2** (API) — 5 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
21
21
|
- **dbchanges2** (Database Changes) _(framework core)_ — 2 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
22
22
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
-
| [Wiss, Janney, Elstner Associates, Inc.](profile.md) | 2.0 | Wiss, Janney, Elstner Associates, Inc. | worker/crons/toga2/wje/sync_togasupply_wje.php |
|
|
5
|
+
| [Wiss, Janney, Elstner Associates, Inc.](profile.md) | 2.0 | Wiss, Janney, Elstner Associates, Inc. | worker/crons/toga2/wje/sync_togasupply_wje.php, worker2/Worker/Wje.php, worker/crons/toga2/wje/import_supporting_records.php |
|
|
@@ -12,19 +12,33 @@ updated: 2026-06-23
|
|
|
12
12
|
owners: [jcardinal]
|
|
13
13
|
files:
|
|
14
14
|
- worker/crons/toga2/wje/sync_togasupply_wje.php
|
|
15
|
+
- worker2/Worker/Wje.php
|
|
16
|
+
- worker/crons/toga2/wje/import_supporting_records.php
|
|
15
17
|
related:
|
|
16
18
|
- ../../1.0/apps/library/features/toga2-api-client-and-bridge.md
|
|
19
|
+
- ../../2.0/apps/worker2/features/wje-freshservice-sync.md
|
|
17
20
|
---
|
|
18
21
|
|
|
19
22
|
## Summary
|
|
20
23
|
|
|
21
|
-
Wiss, Janney, Elstner Associates, Inc. (WJE) is a
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
set of bridge sub-syncs (tickets both directions, ticket-notes, contacts→people, items/units→
|
|
25
|
-
assets, predefined-replies, ticket-teams→groups, ticket-categories) for TOGaDesk client id 153,
|
|
26
|
-
department 269 (plus internal-tickets department 290). WJE requires a group on every ticket and
|
|
27
|
-
falls back to a default group/agent when none is assigned.
|
|
24
|
+
Wiss, Janney, Elstner Associates, Inc. (WJE — "WJE IT") is a **Freshservice**-based help-desk
|
|
25
|
+
client (`wje.freshservice.com`). It is a **single hardcoded tenant** (no dynamic `company_id`
|
|
26
|
+
resolution). Data moves between WJE and TOGA on three paths:
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
1. **Real-time Freshservice webhook** → `_Worker_Wje::Webhook` (worker2) — creates/updates tickets,
|
|
29
|
+
contacts, assets, groups, categories, and conversation notes, keyed by `c_wje*` custom fields.
|
|
30
|
+
2. **Daily batch import** (`worker/crons/toga2/wje/import_supporting_records.php`) — imports canned
|
|
31
|
+
responses, requesters, assets, groups, and the 3-level category hierarchy.
|
|
32
|
+
3. **2-minute TOGaDesk bridge** (`App_Api_Toga2::syncWithTogadesk`) — the cron
|
|
33
|
+
`worker/crons/toga2/wje/sync_togasupply_wje.php` enables the **full** set of bridge sub-syncs
|
|
34
|
+
(tickets both directions, ticket-notes, contacts→people, items/units→assets, predefined-replies,
|
|
35
|
+
ticket-teams→groups, ticket-categories) for TOGaDesk client id 153, department 269 (plus
|
|
36
|
+
internal-tickets department 290).
|
|
37
|
+
|
|
38
|
+
WJE requires a group on every ticket and falls back to a default group/agent (`13000155420` /
|
|
39
|
+
`13000771757`) when none is assigned. No NetSuite TOGa Supply importer runs for WJE (it is a
|
|
40
|
+
help-desk integration, not a supply/procurement client).
|
|
41
|
+
|
|
42
|
+
See **`2.0/apps/worker2/features/wje-freshservice-sync.md`** for the full integration (webhook
|
|
43
|
+
handler, import cron, `c_wje*` models, API clients, schema, reports, monitoring, and gotchas), and
|
|
44
|
+
the bridge feature doc for the `syncWithTogadesk` mechanism.
|
package/package.json
CHANGED