toga-ai 1.0.110 → 1.0.112

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, test/@dave/clickup/probe_opportunity_fields.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,8 @@ 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
22
+ - test/@dave/clickup/probe_opportunity_fields.php
21
23
  - worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php
22
24
  related:
23
25
  - ../architecture.md
@@ -80,19 +82,50 @@ second, independently-gated concern in the same handler.
80
82
 
81
83
  ## ClickUp opportunity task
82
84
 
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`.
85
+ `maybeCreateClickupTask()` runs on both POST and PUT, gated on a **Presales Lead**
86
+ (`custbody_ctc_solution_architect` employee → email; empty email = skip). It is **find-or-update**
87
+ (dedup, since 2026-06-16):
88
+
89
+ 1. `findClickupTaskByOpportunityNumber($tranId)` — GET the list filtered by the `Opportunity #`
90
+ custom field (`include_closed=true&include_archived=true&custom_fields=[{field_id,operator:'=',value}]`).
91
+ 2. **Match → `updateTask()`**: PUT name/description, then POST each custom field individually to
92
+ `/task/{id}/field/{fieldId}` (ClickUp has no bulk custom-field set on an existing task).
93
+ 3. **No match → `createTask()`**: POST to list `901111987449`, `custom_item_id` = `1009`.
94
+
95
+ Shared helpers: `buildCustomFields()` (the field array, used by both create and update),
96
+ `resolvePresalesUserId()` (cached `Client_True.Users.c_clickupUserIdentifier` lookup → else a
97
+ `GET /team` roster scan that back-fills the cache via `cacheClickupUser()`).
86
98
 
87
99
  - **Task name** = `{opportunityNumber} — {customer} — {title}`, e.g.
88
100
  `60533 — 560 Endeavor Health — Data Center Facility Consolidation Services` (empty parts dropped).
101
+ - **Task description** = a fixed structured block built by `buildDescription()`:
102
+ ```
103
+ Company: 560 Endeavor Health
104
+ Opportunity: 60533 — Data Center Facility Consolidation Services
105
+ Amount: 750000
106
+ Expected Close: Tue Nov 24 2026 00:00:00 GMT-0800 (PST)
107
+ Stage: 10%
108
+
109
+ Details:
110
+ <memo>
111
+
112
+ NetSuite Internal ID: 5483750
113
+ ```
114
+ Field sources (confirmed against opp 5483750 via `test/@dave/clickup/probe_opportunity_fields.php`):
115
+ Company = `entity.refName`; Opportunity = `tranId` + `title`; Amount = `projectedTotal`
116
+ (`formatAmount()`, plain number); Expected Close = `expectedCloseDate` (`formatExpectedClose()` —
117
+ JS `Date.toString()` style in **America/Los_Angeles**, e.g. `GMT-0800 (PST)`, auto PST/PDT by date);
118
+ Stage = `entityStatus.refName` (the percent string like "10%", NOT `probability`); Details = `memo`
119
+ (which carries NetSuite's "Missing Required Details…" fallback verbatim for pre-mandatory records).
120
+ The blank separator lines are a single space. Rendered verbatim — no trailing-period normalization.
89
121
  - **Custom-field value mapping** (the non-obvious part — these were swapped before 2026-06-16):
90
122
  `Opportunity #` (`a5529cdc-…`) ← `tranId`; `Customer #` (`170dc118-…`) ← `entity->refName`
91
123
  (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).
124
+ `Presales Lead Email`, and `Presales Lead` (users field).
94
125
  - The ClickUp token comes from `[clickup] token` in the worker2 config (legacy webhook received
95
126
  it on the payload; the new contract drops it).
127
+ - Every ClickUp `_ApiRequest` calls `setLogging(self::CLICKUP_API_LOGGING_ENABLED=false)` — the
128
+ `_ApiRequest` Logs-DB default otherwise surfaces a misleading "connection refused" on dev laptops.
96
129
 
97
130
  ## Data model
98
131
 
@@ -158,13 +191,29 @@ None — platform-wide Forecast sync.
158
191
  (`https://webhook.togahub.com/netsuite`) with `debugWrap` off. Also flip the enqueuer deployment
159
192
  `Testing → Released` (Testing fires only for the deploying user). Full checklist in
160
193
  `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).
194
+ - **ClickUp dedup keys on the `Opportunity #` field, not a DB column.** We deliberately did NOT add
195
+ a `clickupTaskId` column — the dedup lookup queries ClickUp itself by `Opportunity #` (= tranId).
196
+ Consequences to know:
197
+ - **Legacy tasks had `Opportunity #` empty** (the old `webhook/` handler never set it), so they
198
+ can't be matched until backfilled — see the backfill tool below. Without backfill, the first
199
+ edit of a pre-existing opp creates a *second* task.
200
+ - **The find-then-create is not atomic.** Two near-simultaneous events for one opp (e.g. the
201
+ NetSuite hourly `OPP` fallback firing alongside a real edit) can both miss and both create. A
202
+ committed local task id would close most of that window; the field-lookup approach can't.
203
+ ClickUp custom fields aren't unique-constrained, so some race exists regardless.
204
+ - **Update is per-field** (`/task/{id}/field/{id}` — no bulk set), so a mid-loop failure leaves a
205
+ partially-updated task; the thrown error names how far it got. The Presales Lead users field is
206
+ **add-only** on update (we don't read the prior assignee to remove it).
207
+ - **Null guard:** the Presales Lead users field is omitted when the email has no matching ClickUp
208
+ user — a `null` in the `add` array is a 400 from ClickUp.
209
+ - **Backfill legacy `Opportunity #` with `test/@dave/clickup/backfill_opportunity_numbers.php`.**
210
+ Parses the opportunity number from the task title (the leading integer before the separator) and
211
+ writes it to the `Opportunity #` field, **never overwriting** a set value. Two legacy title
212
+ formats exist and both must be handled: em-dash (`74253 — 5780 Cdw — …`) **and** plain hyphen
213
+ (`70539 - 450 Living Well - …`). Safe by default: dry-run unless `--commit`, one task unless
214
+ `--all`; `--task=<id>` targets one. Token from `CLICKUP_API_KEY`. Run 2026-06-16: 146/148 tasks
215
+ backfilled; the 2 skipped are genuine test tasks (`Test Opportunity 2`, `TEST TEST 71437 …` —
216
+ number not leading).
168
217
  - **NetSuite doubled-prefix ids.** The queue record and its fields were created with the full
169
218
  custom prefix typed into the ID box, so the real ids are doubled:
170
219
  type `customrecordcustomrecord_api_msg_queue`, fields `custrecordcustrecord_amq_*`. The
@@ -178,6 +227,16 @@ None — platform-wide Forecast sync.
178
227
  (`Created`,`Pending`,`Retry`,`Sending`) — omitting `Created` silently matches nothing (`candidates:0`).
179
228
 
180
229
  ## Change history
230
+ - 2026-06-16 — ClickUp task description switched to a structured block (`buildDescription()` +
231
+ `formatAmount()`/`formatExpectedClose()`): Company/Opportunity/Amount/Expected Close/Stage/Details/
232
+ Internal ID. Field sources confirmed via `probe_opportunity_fields.php`; Expected Close rendered
233
+ JS-style in Pacific to match the legacy format; Details = memo. (dfranks)
234
+ - 2026-06-16 — ClickUp dedup implemented (find-or-update by `Opportunity #`, no DB column): edits
235
+ now update the existing task instead of duplicating. Added `findClickupTaskByOpportunityNumber()`,
236
+ `updateTask()`, and shared `buildCustomFields()`/`resolvePresalesUserId()`/`cacheClickupUser()`
237
+ helpers; `setLogging(false)` on all ClickUp calls; null-user guard on the users field;
238
+ `include_archived=true` in the lookup. Built `backfill_opportunity_numbers.php` and backfilled
239
+ 146/148 legacy tasks (two title formats: em-dash + hyphen). (dfranks)
181
240
  - 2026-06-16 — ClickUp parity hardening (TRUE-79182): verified the `Opportunity #`/`Customer #`
182
241
  custom fields exist on list 901111987449 and **fixed their swapped values**; `PUT` now also
183
242
  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.112",
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",