toga-ai 1.0.930 → 1.0.932
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/knowledge/1.0/apps/togadesk/INDEX.md +1 -0
- package/knowledge/1.0/apps/togadesk/features/per-client-host-restriction.md +2 -0
- package/knowledge/1.0/apps/togadesk/features/ticket-assign-to-lists.md +73 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -1
- package/knowledge/2.0/apps/worker2/features/clickup-project-routing.md +4 -1
- package/knowledge/2.0/apps/worker2/features/netsuite-clickup-opportunity-bidirectional-design.md +17 -6
- package/knowledge/2.0/apps/worker2/features/netsuite-opportunity-sync.md +11 -3
- package/knowledge/INDEX.md +2 -1
- package/knowledge/clients/imhouston/INDEX.md +5 -0
- package/knowledge/clients/imhouston/profile.md +29 -0
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| [REST API (RPC-over-POST) & API-Key Auth](features/rest-api.md) | TOGa Desk programmatic API at `desk/api/` — an RPC-over-POST dispatcher (not verb/URL REST) with API-key auth; open for the request shape, status codes, auth fl |
|
|
12
12
|
| [SMB Contract Editing & the clientMspId Corruption Trap](features/smb-contract-editing.md) | TOGa Desk SMB contracts modal (`/desk/?route=toga/smbcontracts`) and the `clientMspId` corruption trap that made MSP portal users see "You have no assigned clie |
|
|
13
13
|
| [Staff Login Authentication (signIn) — AD vs. local password](features/staff-login-authentication.md) | TOGa Desk staff login via `signIn()` — AD branch (live) vs. |
|
|
14
|
+
| [Ticket "Assign To" Lists (who can be assigned a ticket)](features/ticket-assign-to-lists.md) | How the ticket "Assign To" pickers are built (new/edit/merge modals, ticket page) and the hardcoded client exclusion list (`ASSIGN_TO_EXCLUDED_CLIENTS`, IM Hous |
|
|
14
15
|
| [Ticket Lifecycle (class.ticket.php)](features/ticket-lifecycle.md) | TOGa Desk ticket creation + reply handling via `Ticket` (class.ticket.php): the four creation/reply doors, `deriveCustomerId()`, and the `addReply()` status swi |
|
|
15
16
|
| [Reverting a TOGa Desk _production Deploy (revert-forward)](workflows/production-revert-forward.md) | How to cleanly roll back one ticket's changes from the togadesk `_production` branch via `git revert -m 1` of its two-parent merge, verified with `git patch-id` |
|
|
16
17
|
| [Standalone PHP Test Script Bootstrap (TOGa Desk)](workflows/standalone-test-scripts.md) | How to bootstrap the TOGa Desk 1.0 framework in a standalone CLI PHP script for read-only testing of desk classes (e.g. |
|
|
@@ -23,6 +23,7 @@ files:
|
|
|
23
23
|
related:
|
|
24
24
|
- ../architecture.md
|
|
25
25
|
- ../../togaview/architecture.md
|
|
26
|
+
- ticket-assign-to-lists.md
|
|
26
27
|
---
|
|
27
28
|
|
|
28
29
|
TOGa Desk per-client branded URLs via `getRestrictedClient()` (host→CLIENT_* switch, UI gate only); open before adding a new branded subdomain (CopyrightHosts hard gate, cert, sessions) or touching login gating.
|
|
@@ -64,3 +65,4 @@ The app code is the easy part. What actually bites:
|
|
|
64
65
|
## Related
|
|
65
66
|
- [TOGa Desk architecture](../architecture.md)
|
|
66
67
|
- [TOGa View architecture](../../togaview/architecture.md)
|
|
68
|
+
- [Ticket Assign To lists](ticket-assign-to-lists.md) — `CLIENT_IMHOUSTON` excluded from ticket assignees
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Ticket "Assign To" Lists (who can be assigned a ticket)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: togadesk
|
|
5
|
+
project: TOGa Desk
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-01
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- desk/includes/functions.php
|
|
13
|
+
- desk/includes/controllers/modals.php
|
|
14
|
+
- desk/template/modals/tickets/add.php
|
|
15
|
+
- desk/template/pages/tickets/manage.php
|
|
16
|
+
- desk/includes/controllers/modals/tickets/merge.php
|
|
17
|
+
related:
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- ticket-lifecycle.md
|
|
20
|
+
- per-client-host-restriction.md
|
|
21
|
+
- ../../../../clients/imhouston/profile.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
How the ticket "Assign To" pickers are built (new/edit/merge modals, ticket page) and the hardcoded client exclusion list (`ASSIGN_TO_EXCLUDED_CLIENTS`, IM Houston); open before changing who appears as a ticket assignee.
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
- Staff and end users both live in `TOGaDeskSupport.people`, split by `type` (`'admin'` | `'user'`). `$isAdmin` = `people.type == 'admin'`.
|
|
29
|
+
- Each ticket Assign To picker runs its **own** people query — there is no shared helper for the list itself.
|
|
30
|
+
- Since TRUE-82408, every ticket picker drops people whose `clientid` is in `ASSIGN_TO_EXCLUDED_CLIENTS` (today: `CLIENT_IMHOUSTON` = 167).
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
**Exclusion list** — `desk/includes/functions.php`:
|
|
35
|
+
- `const ASSIGN_TO_EXCLUDED_CLIENTS = [CLIENT_IMHOUSTON];`
|
|
36
|
+
- `assignToExcludedClientsSql(string $clientIdColumn = 'people.clientid'): string` returns `<col> NOT IN (<ids>)` (ids `intval`'d), or `1 = 1` when the list is empty.
|
|
37
|
+
- To add/remove a client: edit **only** the array. Every ticket picker picks it up.
|
|
38
|
+
|
|
39
|
+
**The ticket pickers — 6 queries in 5 places (all apply the exclusion):**
|
|
40
|
+
1. New Ticket modal, `tickets/add` (`modals.php`) — `$users` and `$admins`.
|
|
41
|
+
- First render (`template/modals/tickets/add.php`) uses `$users` = every person at the logged-in user's client, **any type** (non-RUMCSI branch).
|
|
42
|
+
- Only after the Client dropdown changes (rendered only when `$isAdmin`) does JS `updateAdminIdForClient()` rebuild the list from `$admins` + `peopleDepartments`.
|
|
43
|
+
2. `tickets/addNew` (`modals.php`) — `$admins`.
|
|
44
|
+
3. Edit Ticket modal, `tickets/edit` (`modals.php`) — `$peopleDepartments` = admins linked via `profile_departments` → `people_profiles`; JS `changeDepartment` builds the list. The exclusion keeps the current assignee via `OR people.id = (int)$ticket['adminid']`, so saving the modal does not silently drop an existing assignment.
|
|
45
|
+
4. Ticket page "Assign to..." (`template/pages/tickets/manage.php`) — shows only when the ticket has no `adminid`/`policyid`; filters by profile department + `profile_clients`; filters by client **only** on a branded host (see [per-client-host-restriction](per-client-host-restriction.md)).
|
|
46
|
+
5. Merge modal "Assign To Staff" (`controllers/modals/tickets/merge.php`).
|
|
47
|
+
|
|
48
|
+
**Not covered (own `$admins` queries, not ticket lists):** issues, projects, escalation rules, policy tasks, `clients/assignAdmin`. Add `assignToExcludedClientsSql()` there only if the exclusion should reach them.
|
|
49
|
+
|
|
50
|
+
## Decisions
|
|
51
|
+
|
|
52
|
+
- **2026-10-01 (mhammontree): hardcoded per-client list, not a per-person `people.isAssignable` flag.** 1.0 is retired end of 2026, so no schema change. IM Houston staff do not work tickets; if one is needed, they request an exception (edit the array).
|
|
53
|
+
- **Accepted side effects:**
|
|
54
|
+
- Anyone with `people.clientid = 167` is hidden — including Christian Mayobe (Field Technician) who also works Craftex (client 169) tickets.
|
|
55
|
+
- On the IM Houston branded host the Assign To list is empty (the host already restricts to client 167).
|
|
56
|
+
|
|
57
|
+
## Gotchas
|
|
58
|
+
|
|
59
|
+
- **Edit modal shows inactive admins** — `$peopleDepartments` has no `isActive` check.
|
|
60
|
+
- **New Ticket modal's first list includes end users** (`type = 'user'`), not only staff, until the Client dropdown changes.
|
|
61
|
+
- **Pre-existing, NOT fixed (needs its own ticket):**
|
|
62
|
+
- `manage.php` ~L201: `LEFT OUTER JOIN profile_clients ON profile_clients.profileid = people_profiles.peopleid = people.id` — chained comparison, almost certainly wrong.
|
|
63
|
+
- Unescaped/uncast SQL near these queries: `$clientId`, `$userid`, `{$ticket['departmentid']}`, `{$ticket['clientid']}`, `$_GET['clientid']` (managed/addAsset), `$_GET['customerId']` (tickets/add, addNew). Cast/escape per the security rules when touching them.
|
|
64
|
+
- Data oddity: person 70954 (Minh Nguyen) has `clientid` 167 but role "Advanced User RUMC staff Member" — also hidden by the exclusion.
|
|
65
|
+
|
|
66
|
+
## Related
|
|
67
|
+
- [TOGa Desk architecture](../architecture.md)
|
|
68
|
+
- [Ticket lifecycle](ticket-lifecycle.md)
|
|
69
|
+
- [Per-client hostname restriction](per-client-host-restriction.md)
|
|
70
|
+
- [IM Houston profile](../../../../clients/imhouston/profile.md)
|
|
71
|
+
|
|
72
|
+
## Change history
|
|
73
|
+
- 2026-10-01 — TRUE-82408: added `ASSIGN_TO_EXCLUDED_CLIENTS` (IM Houston) to all 6 ticket Assign To queries; togadesk commit db899a59, not yet deployed (mhammontree)
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
| [Etilize Item Translation Import](features/etilize-item-translation-import.md) | Imports non-English item + feature text from Etilize into the client's `ItemTranslations` (and feature sidecar tables), writing directly to `DB_CLIENT`; open wh |
|
|
33
33
|
| [Logs retention cleanup worker (Infrastructure/Logs/Cleanup — Logs_<Tenant>.Api pruning)](features/logs-retention-cleanup.md) | Daily batched pruning of `Logs_<Tenant>.Api` (GET 30d, other methods 90d); open when changing log retention, adding a table to prune, or when an old Api log row |
|
|
34
34
|
| [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | DB-driven monitoring framework: one orchestrator applies anti-flap + notification rules to any registered "is X healthy?" child check; open when adding a monito |
|
|
35
|
-
| [Bidirectional NetSuite ↔ Forecast ↔ ClickUp opportunity stage sync — BUILT (stage map, echo guard, claim split)](features/netsuite-clickup-opportunity-bidirectional-design.md) | Two-way NetSuite↔ClickUp opportunity STAGE sync (shared `_Component_Opportunity_StageMap`, actor echo guard, claim split, stale-claim
|
|
35
|
+
| [Bidirectional NetSuite ↔ Forecast ↔ ClickUp opportunity stage + title sync — BUILT (stage map, echo guard, claim split, NS-owned title)](features/netsuite-clickup-opportunity-bidirectional-design.md) | Two-way NetSuite↔ClickUp opportunity STAGE sync + one-way NS→CU TITLE sync (shared `_Component_Opportunity_StageMap`, actor echo guard, claim split, stale-claim |
|
|
36
36
|
| [NetSuite Integrations Monitor (Monitor/Operations/NetsuiteIntegrations)](features/netsuite-integrations-monitor.md) | Cross-client health check that flags stuck NetSuite↔2.0 integrations; open when a NetSuite integration hangs or when building an all-client OneUptime sweep. |
|
|
37
37
|
| [NetSuite item → client routing (there is NO client on the NetSuite item record)](features/netsuite-item-client-routing.md) | Evidence that no NetSuite field can route an item to a client; open before designing NetSuite item webhook phase 2 (TRUE-80553) so the "route by a NetSuite fiel |
|
|
38
38
|
| [ClickUp Opportunity Client vs End-Customer Labels (Stakeholders / End Customer)](features/netsuite-opportunity-client-labels.md) | The NetSuite→ClickUp opportunity task's two customer labels (Stakeholders=client, End Customer=`custbody_end_customer`), their backfills/digest and the fuzzy-ma |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-01
|
|
10
10
|
owners: [jcardinal, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Clickup/Project.php
|
|
@@ -49,6 +49,8 @@ This single fact determines the shape of the whole NetSuite↔ClickUp opportunit
|
|
|
49
49
|
| Closed Won | `901111987478` |
|
|
50
50
|
| Closed Lost | `901111987480` |
|
|
51
51
|
|
|
52
|
+
**Won leaves the folder; Lost stays.** Setting Opportunity Stage = Opportunity Won makes a ClickUp native Automation move the task out of the Opportunities folder into Project Hub (space `90113939341`) → list "New Project" (`901112061579`), folder "hidden" (`90117098908`), status "new project", task type Epic. The API cannot undo it (home list). Closed Lost tasks stay in the folder (list `901111987480`). So code scoped to folder `90117060687` (e.g. the NS→CU title sync) does not see Won tasks.
|
|
53
|
+
|
|
52
54
|
All 8 share **one status group** (`proj_90113928591`), so `opportunity won` and `closed lost` are valid statuses on **every** list — a status is not what distinguishes a stage.
|
|
53
55
|
|
|
54
56
|
**ClickUp v2 cannot move a task's home list**, and it fails in two different ways — one silent. Recorded empirically at `worker2/Worker/Clickup/Project.md:14-26`:
|
|
@@ -143,4 +145,5 @@ Read-only / setup actions, invoked via `curl -X POST https://worker.togahub.com/
|
|
|
143
145
|
- [Outbound third-party API rate budget](../../_underscore/features/outbound-api-rate-budget.md)
|
|
144
146
|
|
|
145
147
|
## Change history
|
|
148
|
+
- 2026-10-01 — Won tasks are moved to Project Hub "New Project" by Automation (ajean)
|
|
146
149
|
- 2026-09-25 — Noted folder-gated opportunity dispatch, no early return (jcardinal)
|
package/knowledge/2.0/apps/worker2/features/netsuite-clickup-opportunity-bidirectional-design.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Bidirectional NetSuite ↔ Forecast ↔ ClickUp opportunity stage sync — BUILT (stage map, echo guard, claim split)
|
|
2
|
+
title: Bidirectional NetSuite ↔ Forecast ↔ ClickUp opportunity stage + title sync — BUILT (stage map, echo guard, claim split, NS-owned title)
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: worker2
|
|
5
5
|
project: Worker
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-01
|
|
10
10
|
owners: ["ajean", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Component/Opportunity/StageMap/StageMap.php
|
|
@@ -25,16 +25,18 @@ related:
|
|
|
25
25
|
- ../../../../1.0/apps/worker/features/forecast2-netsuite-reconciliation.md
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
Two-way NetSuite↔ClickUp opportunity STAGE sync (shared `_Component_Opportunity_StageMap`, actor echo guard, claim split, stale-claim takeover); open before touching any opportunity stage/close/reopen logic or the stage map rules.
|
|
28
|
+
Two-way NetSuite↔ClickUp opportunity STAGE sync + one-way NS→CU TITLE sync (shared `_Component_Opportunity_StageMap`, actor echo guard, claim split, stale-claim takeover); open before touching any opportunity stage/close/reopen/title logic or the stage map rules.
|
|
29
29
|
|
|
30
30
|
## Summary
|
|
31
31
|
|
|
32
|
-
Built 2026-09-24 (`jcardinal`, design by `ajean`). Code on worker2 `_production`; flags `SYNC_STAGE_TO_CLICKUP` (NS→CU) and `SYNC_CLOSE_TO_NETSUITE` (CU→NS close/reopen)
|
|
32
|
+
Built 2026-09-24 (`jcardinal`, design by `ajean`). Code on worker2 `_production`; flags `SYNC_STAGE_TO_CLICKUP` (NS→CU), `SYNC_TITLE_TO_CLICKUP` (NS→CU title, PR #183, 2026-10-01) and `SYNC_CLOSE_TO_NETSUITE` (CU→NS close/reopen) all `true`. `UPDATE_CLICKUP` (full-task NS→CU rewrite) stays `false`. Live verification still pending — see Handoff. Notes/files are a separate job: [opportunity documents sync](./netsuite-opportunity-documents-sync.md).
|
|
33
33
|
|
|
34
34
|
## How it works
|
|
35
35
|
|
|
36
36
|
### Per-field ownership
|
|
37
|
-
NetSuite owns: Opportunity Stage dropdown, close state. ClickUp owns: description (Fluffer output — never diffed from NS), assignees, presales fields. NS→CU writes ONLY the Opportunity Stage dropdown (`d8fd5819-d8f7-494b-8671-aff7fbdeb985`); the ClickUp Automation moves the task list from
|
|
37
|
+
NetSuite owns: Opportunity Stage dropdown, close state, **opportunity title** (decided Aaron Jean 2026-10-01). ClickUp owns: description (Fluffer output — never diffed from NS), assignees, presales fields. NS→CU writes ONLY the Opportunity Stage dropdown (`d8fd5819-d8f7-494b-8671-aff7fbdeb985`) and the task name; the ClickUp Automation moves the task list from the dropdown ([routing](./clickup-project-routing.md)).
|
|
38
|
+
|
|
39
|
+
**Title rule:** CU→NS never writes title (not NetSuite, not `Forecast.title`). A human rename in ClickUp is reverted by the next NS event — accepted on purpose, so NetSuite stays the one record.
|
|
38
40
|
|
|
39
41
|
### Stage map — `_Component_Opportunity_StageMap` (PHP, one file, both directions)
|
|
40
42
|
Replaced `SALES_STAGE_MAP` / `STAGE_OPTION_ORDERINDEX` constants (deleted). Rules from Aaron Jean + Eric, 2026-09-24:
|
|
@@ -61,18 +63,26 @@ Option orderindex is resolved LIVE from the field's `type_config.options` (fixes
|
|
|
61
63
|
|
|
62
64
|
### CU→NS (`Worker/Clickup/Opportunity.php`)
|
|
63
65
|
- Routed from `Worker/Clickup.php` by Opportunities **folder** `90117060687` (all 8 stage lists), no early return — hub routing handlers still run.
|
|
66
|
+
- Writes stage/close only — never title (PR #183 removed the title PATCH, the `Forecast.title` write, `stripTaskNamePrefix()`, `CLICK_UP_CUSTOM_FIELD_ID__CUSTOMER_NUMBER`, `FORECAST_TITLE_MAX_LENGTH`). `clickupPostComment()` is public as an internal helper — not a queue action.
|
|
64
67
|
- **Actor echo guard:** drop an event when every history item's actor is our config-token or component-token user (resolved via `GET /user`). Never drop actor `-1` (ClickUp Automation — a human list-move arrives as `taskUpdated` on the dropdown with actor `-1`). Value diff kept as backstop.
|
|
65
68
|
- **Link check (security):** before any PATCH, require `Forecast.Opportunities.clickupTaskId === taskId`. `Opportunity #` is free text; anyone could point a task at another opp.
|
|
66
69
|
- `logged()` commits `DB_LOGS`.
|
|
67
70
|
|
|
68
71
|
### NS→CU (`Worker/Netsuite/Opportunity.php`) — claim split
|
|
69
|
-
- The `clickupTaskId` claim now guards **creation only**. Linked opportunity → `syncStageToClickup()
|
|
72
|
+
- The `clickupTaskId` claim now guards **creation only**. Linked opportunity → `syncLinkedTaskToClickup()` (was `syncStageToClickup()`): one task GET, then title sync and stage sync run independently — both are tried, one combined exception if either fails. ClickUp 404 = `sync skipped (task X not found in ClickUp)`.
|
|
73
|
+
- **Title sync — `syncTitleToClickup()`** (kill switch `SYNC_TITLE_TO_CLICKUP`):
|
|
74
|
+
- Only tasks in Opportunities folder `90117060687`; Won tasks moved to Project Hub are left alone.
|
|
75
|
+
- Match = normalized name equals the NS title, or ends with ` — <title>` / ` - <title>`. Empty NS title → skip.
|
|
76
|
+
- Mismatch → PUT name from pure `buildTaskName()` (same function task creation uses, so names are byte-identical), then comment: "The opportunity title was changed in NetSuite, so the automation has updated it in ClickUp. Previous name: …".
|
|
77
|
+
- Post-PUT check: if ClickUp stored a different name → no comment, Sentry message, result `title rename did not stick`. Comment failure is non-fatal and never auto-retried.
|
|
78
|
+
- `setAutoRetry` on the name PUT and the stage POST. Result string ends `; title updated|unchanged|skipped (...)`.
|
|
70
79
|
- **Stale PENDING takeover:** `PENDING` older than 30 min (or NULL `dtClickupClaimed`) can be re-claimed. `dtClickupClaimed` is raw-SQL only — deliberately NOT on either ORM model (1.0 or 2.0); code checks `SHOW COLUMNS` first. `endClaim()` / `loadClaimTimestamp()` are fenced on the claim timestamp so a slow job can't overwrite a newer owner.
|
|
71
80
|
|
|
72
81
|
### Reconciler
|
|
73
82
|
The 1.0 `periodic_forecast_discrepancy_fix_opportunities.php` still INSERTs (Aaron's 2026-09-09 session kept and completed it) — the old "reconciler becomes a reporter" plan is dropped.
|
|
74
83
|
|
|
75
84
|
## Gotchas
|
|
85
|
+
- **NS→CU stage write echoes back as a human event.** The stage write makes the ClickUp Automation move the list; that event has actor `-1`, so the echo guard never drops it and CU→NS `Process` runs. Before PR #183 this PATCHed NetSuite title from the stale ClickUp name (old ` - ` format; `UPDATE_CLICKUP=false` meant NS title edits never reached ClickUp). 15 NetSuite titles were corrupted 2026-10-01 (e.g. "Pen test" → "68272 - 4502 Societe Generale Financial Corp"); each PATCH also re-fired the NS webhook (3 PUTs per opp). Restore list is held by Aaron, restore deferred. Lesson: anything CU→NS writes must be safe to run after every NS→CU write.
|
|
76
86
|
- `taskMoved` is not subscribed for space `90113928591`; stage detection relies on the dropdown `taskUpdated`.
|
|
77
87
|
- `Opportunities` FK children (`OpportunityDocuments`) use ON DELETE CASCADE because the 1.0 reconciler/fixer delete `Opportunities` rows.
|
|
78
88
|
- worker2 has no `?controller=` URL routing — run the map test / probes via CLI runner.
|
|
@@ -90,5 +100,6 @@ The 1.0 `periodic_forecast_discrepancy_fix_opportunities.php` still INSERTs (Aar
|
|
|
90
100
|
- [ClickUp routing](./clickup-project-routing.md) — stage-as-list Automation.
|
|
91
101
|
|
|
92
102
|
## Change history
|
|
103
|
+
- 2026-10-01 — NetSuite owns title: CU→NS title write removed, NS→CU title sync added (PR #183) (ajean)
|
|
93
104
|
- 2026-09-25 — Built: StageMap component, actor guard, link check, claim split, stale takeover; flags on. Map stays PHP; reconciler keeps INSERT (jcardinal)
|
|
94
105
|
- 2026-09-16 — Agreed design recorded, not built (ajean)
|
|
@@ -86,7 +86,7 @@ Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer
|
|
|
86
86
|
|
|
87
87
|
Shared helpers: `buildCustomFields()` (the field array, used by both create and update), `resolvePresalesUserId()` (cached `Client_True.Users.c_clickupUserIdentifier` lookup → else a `GET /team` roster scan that back-fills the cache via `cacheClickupUser()`).
|
|
88
88
|
|
|
89
|
-
- **Task name** = `{opportunityNumber} — {customer} — {title}
|
|
89
|
+
- **Task name** = `{opportunityNumber} — {customer} — {title}` from pure `buildTaskName()` (also used by the NS→CU title sync), e.g. `60533 — 560 Endeavor Health — Data Center Facility Consolidation Services` (empty parts dropped).
|
|
90
90
|
- **Task description** = a fixed structured block built by `buildDescription()`:
|
|
91
91
|
```
|
|
92
92
|
Company: 560 Endeavor Health
|
|
@@ -123,7 +123,7 @@ Required a new `dbchanges2/Forecast/` ALTER adding the `clickupTaskId` column, a
|
|
|
123
123
|
|
|
124
124
|
#### Claim guards creation only (since 2026-09-24)
|
|
125
125
|
|
|
126
|
-
The claim used to double as "do anything at all", making the integration create-only. Now split: a linked opportunity goes to `
|
|
126
|
+
The claim used to double as "do anything at all", making the integration create-only. Now split: a linked opportunity goes to `syncLinkedTaskToClickup()` (stage dropdown + task name only, behind `SYNC_STAGE_TO_CLICKUP` / `SYNC_TITLE_TO_CLICKUP`). `updateTask()` is still dead behind `UPDATE_CLICKUP = false` — leave it. A stale `PENDING` (>30 min via `dtClickupClaimed`, or NULL timestamp) is now taken over automatically. Details: [stage sync](./netsuite-clickup-opportunity-bidirectional-design.md).
|
|
127
127
|
|
|
128
128
|
`clickupTaskId` semantics: `NULL` = unclaimed; `PENDING` = claim in flight; else the real task id.
|
|
129
129
|
|
|
@@ -173,6 +173,13 @@ curl -X POST https://webhook.togahub.com/netsuite -H 'Content-Type: application/
|
|
|
173
173
|
- An opportunity with no presales lead returns `skipped (no presales lead)` and stays queued.
|
|
174
174
|
- `importOpportunity()` is self-healing, so the replay also repairs the Forecast row.
|
|
175
175
|
|
|
176
|
+
**Bulk replay rules (from the 2026-10-01 Won/Lost catch-up — 287 tasks, 149 → Closed Lost, 138 → Opportunity Won):**
|
|
177
|
+
- **Retroactive data fixes use the live-sync logic (webhook replay), never hand-set values in ClickUp** (decided Aaron Jean 2026-10-01).
|
|
178
|
+
- **Never bulk-replay without PR #183.** Before it, each replay's stage write echoed back through CU→NS and overwrote NetSuite titles (see [stage sync](./netsuite-clickup-opportunity-bidirectional-design.md) gotchas).
|
|
179
|
+
- **Space replays 3–5 s apart.** ClickUp returned HTTP 429 at 0.5 s (31 failures) and still 8 failures at 1 s with retries. A failed replay is safe to re-send.
|
|
180
|
+
- Candidate list: ClickUp folder `90117060687` tasks, match by opportunity number to `Forecast.Opportunities` + `PercentToCloseStatuses.netsuiteInternalId` (`124`/`130` = Won, `14` = Lost). `130` Alternate Quote goes to Won per StageMap even when NS status reads Closed - Lost.
|
|
181
|
+
- The ClickUp MCP filter rejects two filters on the same custom field ("use a single RANGE") — run `!= Won` and `= Lost` as separate queries.
|
|
182
|
+
|
|
176
183
|
**A `404` on replay is the correct signal for a NetSuite-deleted opportunity, not a defect.** The single failure in that run (NS internalId **7442501**) was NetSuite REST HTTP 404 *"The opportunity does not exist"* — the record had been deleted in NetSuite, which is also why it was the one row missing from Forecast.
|
|
177
184
|
|
|
178
185
|
#### Open item — item-sync watchdog timeouts (NOT investigated)
|
|
@@ -245,7 +252,7 @@ Three facts worth not rediscovering:
|
|
|
245
252
|
1. Resolve NS opp from `Opportunity #` (`a5529cdc-8dbb-4165-8bc5-9f5cc691796a`, = `tranId`) via one-row SuiteQL.
|
|
246
253
|
2. **Require `Forecast.Opportunities.clickupTaskId === taskId`** — else skip (free-text field; security).
|
|
247
254
|
3. Drop echo events by actor (our token users; never actor `-1`); GET the opp with `expandSubResources=true`.
|
|
248
|
-
4. PATCH only differing
|
|
255
|
+
4. PATCH only differing stage/`entityStatus` per `_Component_Opportunity_StageMap`. **Never title** — NetSuite owns it (PR #183, 2026-10-01).
|
|
249
256
|
|
|
250
257
|
Stage/close/reopen rules: [stage sync](./netsuite-clickup-opportunity-bidirectional-design.md). Notes/files: [documents sync](./netsuite-opportunity-documents-sync.md).
|
|
251
258
|
|
|
@@ -327,5 +334,6 @@ Stage/close/reopen rules: [stage sync](./netsuite-clickup-opportunity-bidirectio
|
|
|
327
334
|
- [MCP Tool Usage](../../../../standalone/apps/claude/workflows/mcp-tool-usage.md) — the `toga-db` query traps hit while running the verdict census.
|
|
328
335
|
|
|
329
336
|
## Change history
|
|
337
|
+
- 2026-10-01 — CU→NS no longer writes title; bulk replay rules from the Won/Lost catch-up (ajean)
|
|
330
338
|
- 2026-10-01 — Flagged `folder_ids[]` vs documented `project_ids[]` filter (unverified) (ajean)
|
|
331
339
|
- 2026-09-25 — Claim guards creation only; CU→NS routed by folder, link check, actor guard; orderindex-0 bug fixed (jcardinal)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -8,7 +8,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
8
8
|
- **worker** (Worker) — 41 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
|
|
10
10
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
|
11
|
-
- **togadesk** (TOGa Desk) —
|
|
11
|
+
- **togadesk** (TOGa Desk) — 14 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
|
|
12
12
|
- **togaview** (TOGa View) — 8 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
|
|
13
13
|
- **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
|
|
14
14
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
@@ -59,6 +59,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
59
59
|
- **Endeavor Health** (`endeavor-health`) → [clients/endeavor-health/INDEX.md](clients/endeavor-health/INDEX.md)
|
|
60
60
|
- **Fordham University** (`fordham`) → [clients/fordham/INDEX.md](clients/fordham/INDEX.md)
|
|
61
61
|
- **GroWrk** (`growrk`) → [clients/growrk/INDEX.md](clients/growrk/INDEX.md)
|
|
62
|
+
- **IM Houston** (`imhouston`) → [clients/imhouston/INDEX.md](clients/imhouston/INDEX.md)
|
|
62
63
|
- **ManageLife** (`managelife`) → [clients/managelife/INDEX.md](clients/managelife/INDEX.md)
|
|
63
64
|
- **Northwell Health** (`northwell`) → [clients/northwell/INDEX.md](clients/northwell/INDEX.md)
|
|
64
65
|
- **New York City Department of Education** (`nycdoe`) → [clients/nycdoe/INDEX.md](clients/nycdoe/INDEX.md)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "IM Houston"
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
apps:
|
|
5
|
+
- togadesk
|
|
6
|
+
- library
|
|
7
|
+
project: TOGa Desk
|
|
8
|
+
client: imhouston
|
|
9
|
+
type: profile
|
|
10
|
+
status: active
|
|
11
|
+
updated: 2026-10-01
|
|
12
|
+
owners: ["mhammontree"]
|
|
13
|
+
files: []
|
|
14
|
+
related:
|
|
15
|
+
- ../../1.0/apps/togadesk/features/ticket-assign-to-lists.md
|
|
16
|
+
- ../../1.0/apps/togadesk/features/per-client-host-restriction.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
IM Houston TOGa Desk client (togadesk `clients.id` 167, `CLIENT_IMHOUSTON`); open for its togadesk-specific rules.
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
- TOGa Desk (1.0) client: `TOGaDeskSupport.clients.id` **167**, constant `CLIENT_IMHOUSTON` in `desk/includes/functions.php`.
|
|
24
|
+
- Has its own branded TOGa Desk host — see [per-client-host-restriction](../../1.0/apps/togadesk/features/per-client-host-restriction.md).
|
|
25
|
+
- Also a 2.0 tenant (`Client_Imhouston`); 2.0 scope not yet added to `apps`.
|
|
26
|
+
|
|
27
|
+
## Rules
|
|
28
|
+
|
|
29
|
+
- **IM Houston staff are not ticket assignees.** Excluded from every ticket Assign To picker via `ASSIGN_TO_EXCLUDED_CLIENTS` (TRUE-82408). Exceptions on request — see [ticket-assign-to-lists](../../1.0/apps/togadesk/features/ticket-assign-to-lists.md).
|
package/package.json
CHANGED