toga-ai 1.0.110 → 1.0.111

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,5 +7,5 @@
7
7
  | [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
8
8
  | [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
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
- | [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, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
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, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
11
11
  | [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 |
@@ -18,6 +18,7 @@ files:
18
18
  - test/@dave/NetSuite/api-message-queue/ue_amq_drain.js
19
19
  - test/@dave/NetSuite/api-message-queue/ss_amq_drain.js
20
20
  - test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md
21
+ - test/@dave/clickup/backfill_opportunity_numbers.php
21
22
  - worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php
22
23
  related:
23
24
  - ../architecture.md
@@ -80,19 +81,30 @@ second, independently-gated concern in the same handler.
80
81
 
81
82
  ## ClickUp opportunity task
82
83
 
83
- Created by `maybeCreateClickupTask()` → `createTask()` (ported verbatim from the legacy
84
- `webhook/` handler), gated on a **Presales Lead** (`custbody_ctc_solution_architect` employee →
85
- email; empty email = skip). Posts to list `901111987449` with `custom_item_id` = `1009`.
84
+ `maybeCreateClickupTask()` runs on both POST and PUT, gated on a **Presales Lead**
85
+ (`custbody_ctc_solution_architect` employee → email; empty email = skip). It is **find-or-update**
86
+ (dedup, since 2026-06-16):
87
+
88
+ 1. `findClickupTaskByOpportunityNumber($tranId)` — GET the list filtered by the `Opportunity #`
89
+ custom field (`include_closed=true&include_archived=true&custom_fields=[{field_id,operator:'=',value}]`).
90
+ 2. **Match → `updateTask()`**: PUT name/description, then POST each custom field individually to
91
+ `/task/{id}/field/{fieldId}` (ClickUp has no bulk custom-field set on an existing task).
92
+ 3. **No match → `createTask()`**: POST to list `901111987449`, `custom_item_id` = `1009`.
93
+
94
+ Shared helpers: `buildCustomFields()` (the field array, used by both create and update),
95
+ `resolvePresalesUserId()` (cached `Client_True.Users.c_clickupUserIdentifier` lookup → else a
96
+ `GET /team` roster scan that back-fills the cache via `cacheClickupUser()`).
86
97
 
87
98
  - **Task name** = `{opportunityNumber} — {customer} — {title}`, e.g.
88
99
  `60533 — 560 Endeavor Health — Data Center Facility Consolidation Services` (empty parts dropped).
89
100
  - **Custom-field value mapping** (the non-obvious part — these were swapped before 2026-06-16):
90
101
  `Opportunity #` (`a5529cdc-…`) ← `tranId`; `Customer #` (`170dc118-…`) ← `entity->refName`
91
102
  (the customer **name**, deliberately — not the NetSuite customer number). Plus `Sales Rep`,
92
- `Presales Lead Email`, and `Presales Lead` (users field, resolved via a cached
93
- `Client_True.Users.c_clickupUserIdentifier` lookup, falling back to a `GET /team` scan).
103
+ `Presales Lead Email`, and `Presales Lead` (users field).
94
104
  - The ClickUp token comes from `[clickup] token` in the worker2 config (legacy webhook received
95
105
  it on the payload; the new contract drops it).
106
+ - Every ClickUp `_ApiRequest` calls `setLogging(self::CLICKUP_API_LOGGING_ENABLED=false)` — the
107
+ `_ApiRequest` Logs-DB default otherwise surfaces a misleading "connection refused" on dev laptops.
96
108
 
97
109
  ## Data model
98
110
 
@@ -158,13 +170,29 @@ None — platform-wide Forecast sync.
158
170
  (`https://webhook.togahub.com/netsuite`) with `debugWrap` off. Also flip the enqueuer deployment
159
171
  `Testing → Released` (Testing fires only for the deploying user). Full checklist in
160
172
  `DEPLOY_RUNBOOK.md` §6.
161
- - **No ClickUp dedup (deferred).** `maybeCreateClickupTask()` only gates on the Presales Lead — it
162
- does **not** check whether a task already exists. Since both POST and PUT now create, **every edit
163
- of an opportunity with a Presales Lead creates a NEW ClickUp task** (and the NetSuite-side
164
- enqueuer fires on every save). Before this hits production traffic, add dedup — recommended:
165
- store the created ClickUp task id on the `Forecast.Opportunities` row and skip when present.
166
- Until 2026-06-16 PUT skipped ClickUp entirely, which masked this (an edit produced no duplicate
167
- only because the edit path didn't create at all).
173
+ - **ClickUp dedup keys on the `Opportunity #` field, not a DB column.** We deliberately did NOT add
174
+ a `clickupTaskId` column — the dedup lookup queries ClickUp itself by `Opportunity #` (= tranId).
175
+ Consequences to know:
176
+ - **Legacy tasks had `Opportunity #` empty** (the old `webhook/` handler never set it), so they
177
+ can't be matched until backfilled — see the backfill tool below. Without backfill, the first
178
+ edit of a pre-existing opp creates a *second* task.
179
+ - **The find-then-create is not atomic.** Two near-simultaneous events for one opp (e.g. the
180
+ NetSuite hourly `OPP` fallback firing alongside a real edit) can both miss and both create. A
181
+ committed local task id would close most of that window; the field-lookup approach can't.
182
+ ClickUp custom fields aren't unique-constrained, so some race exists regardless.
183
+ - **Update is per-field** (`/task/{id}/field/{id}` — no bulk set), so a mid-loop failure leaves a
184
+ partially-updated task; the thrown error names how far it got. The Presales Lead users field is
185
+ **add-only** on update (we don't read the prior assignee to remove it).
186
+ - **Null guard:** the Presales Lead users field is omitted when the email has no matching ClickUp
187
+ user — a `null` in the `add` array is a 400 from ClickUp.
188
+ - **Backfill legacy `Opportunity #` with `test/@dave/clickup/backfill_opportunity_numbers.php`.**
189
+ Parses the opportunity number from the task title (the leading integer before the separator) and
190
+ writes it to the `Opportunity #` field, **never overwriting** a set value. Two legacy title
191
+ formats exist and both must be handled: em-dash (`74253 — 5780 Cdw — …`) **and** plain hyphen
192
+ (`70539 - 450 Living Well - …`). Safe by default: dry-run unless `--commit`, one task unless
193
+ `--all`; `--task=<id>` targets one. Token from `CLICKUP_API_KEY`. Run 2026-06-16: 146/148 tasks
194
+ backfilled; the 2 skipped are genuine test tasks (`Test Opportunity 2`, `TEST TEST 71437 …` —
195
+ number not leading).
168
196
  - **NetSuite doubled-prefix ids.** The queue record and its fields were created with the full
169
197
  custom prefix typed into the ID box, so the real ids are doubled:
170
198
  type `customrecordcustomrecord_api_msg_queue`, fields `custrecordcustrecord_amq_*`. The
@@ -178,6 +206,12 @@ None — platform-wide Forecast sync.
178
206
  (`Created`,`Pending`,`Retry`,`Sending`) — omitting `Created` silently matches nothing (`candidates:0`).
179
207
 
180
208
  ## Change history
209
+ - 2026-06-16 — ClickUp dedup implemented (find-or-update by `Opportunity #`, no DB column): edits
210
+ now update the existing task instead of duplicating. Added `findClickupTaskByOpportunityNumber()`,
211
+ `updateTask()`, and shared `buildCustomFields()`/`resolvePresalesUserId()`/`cacheClickupUser()`
212
+ helpers; `setLogging(false)` on all ClickUp calls; null-user guard on the users field;
213
+ `include_archived=true` in the lookup. Built `backfill_opportunity_numbers.php` and backfilled
214
+ 146/148 legacy tasks (two title formats: em-dash + hyphen). (dfranks)
181
215
  - 2026-06-16 — ClickUp parity hardening (TRUE-79182): verified the `Opportunity #`/`Customer #`
182
216
  custom fields exist on list 901111987449 and **fixed their swapped values**; `PUT` now also
183
217
  creates the ClickUp task (was POST-only); task name reformatted to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.110",
3
+ "version": "1.0.111",
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",