toga-ai 1.0.109 → 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
@@ -75,7 +76,35 @@ second, independently-gated concern in the same handler.
75
76
  `_Worker::runTask('Netsuite/Opportunity/<verb>', {internalId})`.
76
77
  5. **Handle (worker2):** `_Worker_Netsuite_Opportunity` fetches the record from NetSuite by id
77
78
  and upserts it into `Forecast.Opportunities` (+ `OpportunityItems`), and — only when a
78
- Presales Lead is set — creates the ClickUp opportunity task.
79
+ Presales Lead is set — creates the ClickUp opportunity task. **Both `POST` (create) and `PUT`
80
+ (edit) create the task** (since 2026-06-16); `DELETE` only removes from Forecast.
81
+
82
+ ## ClickUp opportunity task
83
+
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()`).
97
+
98
+ - **Task name** = `{opportunityNumber} — {customer} — {title}`, e.g.
99
+ `60533 — 560 Endeavor Health — Data Center Facility Consolidation Services` (empty parts dropped).
100
+ - **Custom-field value mapping** (the non-obvious part — these were swapped before 2026-06-16):
101
+ `Opportunity #` (`a5529cdc-…`) ← `tranId`; `Customer #` (`170dc118-…`) ← `entity->refName`
102
+ (the customer **name**, deliberately — not the NetSuite customer number). Plus `Sales Rep`,
103
+ `Presales Lead Email`, and `Presales Lead` (users field).
104
+ - The ClickUp token comes from `[clickup] token` in the worker2 config (legacy webhook received
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.
79
108
 
80
109
  ## Data model
81
110
 
@@ -128,7 +157,42 @@ None — platform-wide Forecast sync.
128
157
  - **No WebhookIngestion Lambda locally.** Dev points the enqueuer at an ngrok tunnel to local
129
158
  worker2 via the `DEV_OVERRIDE` block, which also wraps the body in worker2's legacy
130
159
  `{action,parameters}` envelope (since there's no Lambda to convert `/netsuite` → an action).
131
- Set `DEV_OVERRIDE.enabled=false` for prod.
160
+ - **`debugWrap` is the dev/prod body-shape switch — not just the URL.** `debugWrap=true` (dev)
161
+ nests the raw `{recordType,internalId,…}` contract inside worker2's legacy `{action,parameters}`
162
+ envelope (`action='Netsuite/Webhook'`); the local `worker()` legacy path needs this because
163
+ there's no Lambda. `debugWrap=false` (prod) sends the raw contract — the WebhookIngestion Lambda
164
+ derives the `Netsuite/Webhook` action from the `/netsuite` URL path itself. Changing only the
165
+ endpoint to prod while leaving `debugWrap` on double-wraps the body and breaks the handler.
166
+ - **Prod cutover: flip `DEV_OVERRIDE.enabled=false` — do NOT comment out the block.** `getConfig()`
167
+ reads `DEV_OVERRIDE.enabled/.endpoint/.debugWrap`; deleting the `const` throws `ReferenceError`,
168
+ which `afterSubmit`'s catch swallows as `AMQ enqueue FAILED` → **no row enqueued, sync silently
169
+ stops.** Flipping `enabled=false` makes `getConfig()` fall through to `DEFAULTS.endpoint`
170
+ (`https://webhook.togahub.com/netsuite`) with `debugWrap` off. Also flip the enqueuer deployment
171
+ `Testing → Released` (Testing fires only for the deploying user). Full checklist in
172
+ `DEPLOY_RUNBOOK.md` §6.
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).
132
196
  - **NetSuite doubled-prefix ids.** The queue record and its fields were created with the full
133
197
  custom prefix typed into the ID box, so the real ids are doubled:
134
198
  type `customrecordcustomrecord_api_msg_queue`, fields `custrecordcustrecord_amq_*`. The
@@ -142,6 +206,17 @@ None — platform-wide Forecast sync.
142
206
  (`Created`,`Pending`,`Retry`,`Sending`) — omitting `Created` silently matches nothing (`candidates:0`).
143
207
 
144
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)
215
+ - 2026-06-16 — ClickUp parity hardening (TRUE-79182): verified the `Opportunity #`/`Customer #`
216
+ custom fields exist on list 901111987449 and **fixed their swapped values**; `PUT` now also
217
+ creates the ClickUp task (was POST-only); task name reformatted to
218
+ `{opp#} — {customer} — {title}`; `createTask()` made private + generic `Exception` throws
219
+ narrowed to `RuntimeException`. Dedup still deferred (see gotcha). (dfranks)
145
220
  - 2026-06-16 — Trigger correction: the immediate trigger moved INTO the enqueuer (`task.create`
146
221
  the drainer) after discovering a UE can't trigger a UE — `ue_amq_drain` retired. `task.create`
147
222
  needs a `Not Scheduled` deployment of `ss_amq_drain` (else `NO_DEPLOYMENTS_AVAILABLE`); submit by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.109",
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",