toga-ai 1.0.108 → 1.0.110
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.
|
@@ -40,10 +40,13 @@ second, independently-gated concern in the same handler.
|
|
|
40
40
|
`test/@dave/NetSuite/api-message-queue/` — upload all four `.js` to one File Cabinet folder):
|
|
41
41
|
- `lib_amq_queue.js` — shared module: record-type/field ids, `transmit()`, `findSendableIds()`.
|
|
42
42
|
- `ue_api_msg_queue_enqueue.js` — User Event on the synced source records (Opportunity, …):
|
|
43
|
-
writes one queue row per create/edit/delete
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
writes one queue row per create/edit/delete, then **kicks the scheduled drainer via `N/task`**
|
|
44
|
+
(async, non-blocking). The immediate trigger lives here — see gotchas for why.
|
|
45
|
+
- `ss_amq_drain.js` — Scheduled Script: drains rows (the on-demand kick *and* a cron safety-net).
|
|
46
|
+
Needs **two deployments**: one `Scheduled` (cron) + one `Not Scheduled` (the on-demand slot
|
|
47
|
+
`task.create` submits into).
|
|
48
|
+
- `ue_amq_drain.js` — **RETIRED.** A UE on the queue record can't fire on enqueuer-created rows
|
|
49
|
+
(UE-can't-trigger-UE); undeploy it.
|
|
47
50
|
- The custom record itself: **"API Message Queue"**, internal id **1185**.
|
|
48
51
|
|
|
49
52
|
**worker2 side:**
|
|
@@ -59,12 +62,12 @@ second, independently-gated concern in the same handler.
|
|
|
59
62
|
(`{recordType, internalId, eventType, action}`), `headers`. It maps `record.Type`
|
|
60
63
|
(lowercase) → the camelCase `recordType` worker2 expects (explicit map; e.g.
|
|
61
64
|
`salesorder`→`salesOrder`), and `context.type` → CRUD verb (`create`→POST, `edit`/`xedit`→PUT,
|
|
62
|
-
`delete`→DELETE).
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
endpoint, then **deletes on 2xx** /
|
|
67
|
-
(terminal — never auto-deleted; Jeff's data-loss rule).
|
|
65
|
+
`delete`→DELETE). It then **kicks the drainer**:
|
|
66
|
+
`task.create({taskType:SCHEDULED_SCRIPT, scriptId:'customscript_ss_amq_drain'})` — scriptId only,
|
|
67
|
+
no deployment id. If no on-demand slot is free the kick is skipped and the cron backs it up.
|
|
68
|
+
2. **Drain (NetSuite):** `ss_amq_drain` runs (from the kick or its cron). `transmit()` claims a row
|
|
69
|
+
(`Created`→`Sending`) before the HTTP call, POSTs to the row's endpoint, then **deletes on 2xx** /
|
|
70
|
+
sets `Retry` (+exponential backoff) / `Failed` (terminal — never auto-deleted; Jeff's data-loss rule).
|
|
68
71
|
3. **Ingest (prod):** `webhook.togahub.com/netsuite` → WebhookIngestion **Python Lambda** →
|
|
69
72
|
INSERT `Core.WorkerJobs` + SQS → returns 200 immediately. Fully async; outcome lives in
|
|
70
73
|
`WorkerJobs.isSuccess`/`failureReason`.
|
|
@@ -72,7 +75,24 @@ second, independently-gated concern in the same handler.
|
|
|
72
75
|
`_Worker::runTask('Netsuite/Opportunity/<verb>', {internalId})`.
|
|
73
76
|
5. **Handle (worker2):** `_Worker_Netsuite_Opportunity` fetches the record from NetSuite by id
|
|
74
77
|
and upserts it into `Forecast.Opportunities` (+ `OpportunityItems`), and — only when a
|
|
75
|
-
Presales Lead is set — creates the ClickUp opportunity task.
|
|
78
|
+
Presales Lead is set — creates the ClickUp opportunity task. **Both `POST` (create) and `PUT`
|
|
79
|
+
(edit) create the task** (since 2026-06-16); `DELETE` only removes from Forecast.
|
|
80
|
+
|
|
81
|
+
## ClickUp opportunity task
|
|
82
|
+
|
|
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`.
|
|
86
|
+
|
|
87
|
+
- **Task name** = `{opportunityNumber} — {customer} — {title}`, e.g.
|
|
88
|
+
`60533 — 560 Endeavor Health — Data Center Facility Consolidation Services` (empty parts dropped).
|
|
89
|
+
- **Custom-field value mapping** (the non-obvious part — these were swapped before 2026-06-16):
|
|
90
|
+
`Opportunity #` (`a5529cdc-…`) ← `tranId`; `Customer #` (`170dc118-…`) ← `entity->refName`
|
|
91
|
+
(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).
|
|
94
|
+
- The ClickUp token comes from `[clickup] token` in the worker2 config (legacy webhook received
|
|
95
|
+
it on the payload; the new contract drops it).
|
|
76
96
|
|
|
77
97
|
## Data model
|
|
78
98
|
|
|
@@ -97,6 +117,16 @@ None — platform-wide Forecast sync.
|
|
|
97
117
|
|
|
98
118
|
## Gotchas / known issues
|
|
99
119
|
|
|
120
|
+
- **A User Event can't trigger another User Event → the immediate trigger lives in the enqueuer.**
|
|
121
|
+
The enqueuer (a UE) creating the queue row does NOT fire any UE on the queue record (only the
|
|
122
|
+
*scheduled* drainer's `submitFields` does). So a drain-UE on the queue record never runs for
|
|
123
|
+
enqueued rows — the enqueuer kicks the drainer itself via `task.create`. (`ue_amq_drain.js` is
|
|
124
|
+
retired for exactly this reason. Evidence: its log only ever showed `xedit`/`Sending`, never `create`.)
|
|
125
|
+
- **`task.create(SCHEDULED_SCRIPT)` needs a `Not Scheduled` deployment.** A `Scheduled` deployment is
|
|
126
|
+
reserved for its recurrence → `NO_DEPLOYMENTS_AVAILABLE`; a `Testing` deployment can't be submitted
|
|
127
|
+
at all. Give `ss_amq_drain` a dedicated `Not Scheduled` deployment as the on-demand slot (add more
|
|
128
|
+
for concurrency) and keep a `Scheduled` one for the cron. Submit by **scriptId only** — the
|
|
129
|
+
deployment id is prefix-doubled (`customdeploycustomdeploy_ss_amq_drain`).
|
|
100
130
|
- **worker2 routes EVERY request to `_Controller_Index::worker()`** — `worker2/_.php` sets
|
|
101
131
|
`DEFAULT_CONTROLLER_METHOD = ['_Controller_Index','worker']`, which short-circuits `_Route`'s
|
|
102
132
|
URI matching. There is no per-URL routing, and `.htaccess` denies direct `.php` files, so you
|
|
@@ -115,7 +145,26 @@ None — platform-wide Forecast sync.
|
|
|
115
145
|
- **No WebhookIngestion Lambda locally.** Dev points the enqueuer at an ngrok tunnel to local
|
|
116
146
|
worker2 via the `DEV_OVERRIDE` block, which also wraps the body in worker2's legacy
|
|
117
147
|
`{action,parameters}` envelope (since there's no Lambda to convert `/netsuite` → an action).
|
|
118
|
-
|
|
148
|
+
- **`debugWrap` is the dev/prod body-shape switch — not just the URL.** `debugWrap=true` (dev)
|
|
149
|
+
nests the raw `{recordType,internalId,…}` contract inside worker2's legacy `{action,parameters}`
|
|
150
|
+
envelope (`action='Netsuite/Webhook'`); the local `worker()` legacy path needs this because
|
|
151
|
+
there's no Lambda. `debugWrap=false` (prod) sends the raw contract — the WebhookIngestion Lambda
|
|
152
|
+
derives the `Netsuite/Webhook` action from the `/netsuite` URL path itself. Changing only the
|
|
153
|
+
endpoint to prod while leaving `debugWrap` on double-wraps the body and breaks the handler.
|
|
154
|
+
- **Prod cutover: flip `DEV_OVERRIDE.enabled=false` — do NOT comment out the block.** `getConfig()`
|
|
155
|
+
reads `DEV_OVERRIDE.enabled/.endpoint/.debugWrap`; deleting the `const` throws `ReferenceError`,
|
|
156
|
+
which `afterSubmit`'s catch swallows as `AMQ enqueue FAILED` → **no row enqueued, sync silently
|
|
157
|
+
stops.** Flipping `enabled=false` makes `getConfig()` fall through to `DEFAULTS.endpoint`
|
|
158
|
+
(`https://webhook.togahub.com/netsuite`) with `debugWrap` off. Also flip the enqueuer deployment
|
|
159
|
+
`Testing → Released` (Testing fires only for the deploying user). Full checklist in
|
|
160
|
+
`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).
|
|
119
168
|
- **NetSuite doubled-prefix ids.** The queue record and its fields were created with the full
|
|
120
169
|
custom prefix typed into the ID box, so the real ids are doubled:
|
|
121
170
|
type `customrecordcustomrecord_api_msg_queue`, fields `custrecordcustrecord_amq_*`. The
|
|
@@ -124,13 +173,21 @@ None — platform-wide Forecast sync.
|
|
|
124
173
|
string throws `Object of class stdClass could not be converted to string`. Normalize before
|
|
125
174
|
matching.
|
|
126
175
|
- **Drain dedup** uses a `Sending` claim status: `transmit()` flips `Created`→`Sending` before the
|
|
127
|
-
HTTP call so a concurrent pass skips it
|
|
128
|
-
|
|
129
|
-
`
|
|
130
|
-
every sendable status (`Created`,`Pending`,`Retry`,`Sending`) — omitting `Created` silently
|
|
131
|
-
matches nothing (`candidates:0`).
|
|
176
|
+
HTTP call so a concurrent drainer pass skips it (`Sending` ∉ sendable), narrowing the double-send
|
|
177
|
+
window to the load→claim gap. `findSendableIds` must list every sendable status
|
|
178
|
+
(`Created`,`Pending`,`Retry`,`Sending`) — omitting `Created` silently matches nothing (`candidates:0`).
|
|
132
179
|
|
|
133
180
|
## Change history
|
|
181
|
+
- 2026-06-16 — ClickUp parity hardening (TRUE-79182): verified the `Opportunity #`/`Customer #`
|
|
182
|
+
custom fields exist on list 901111987449 and **fixed their swapped values**; `PUT` now also
|
|
183
|
+
creates the ClickUp task (was POST-only); task name reformatted to
|
|
184
|
+
`{opp#} — {customer} — {title}`; `createTask()` made private + generic `Exception` throws
|
|
185
|
+
narrowed to `RuntimeException`. Dedup still deferred (see gotcha). (dfranks)
|
|
186
|
+
- 2026-06-16 — Trigger correction: the immediate trigger moved INTO the enqueuer (`task.create`
|
|
187
|
+
the drainer) after discovering a UE can't trigger a UE — `ue_amq_drain` retired. `task.create`
|
|
188
|
+
needs a `Not Scheduled` deployment of `ss_amq_drain` (else `NO_DEPLOYMENTS_AVAILABLE`); submit by
|
|
189
|
+
scriptId only (deployment id is prefix-doubled). Fixed an `itemType`-as-object crash in the
|
|
190
|
+
importer. POST/DELETE coded but only PUT (edit) verified live. (dfranks)
|
|
134
191
|
- 2026-06-16 — Built the queue enqueuer/drainer SuiteScripts, the worker2 `/netsuite` router, and
|
|
135
192
|
the `_Worker_Netsuite_Opportunity` Forecast importer (header + items + status), with ClickUp
|
|
136
193
|
parity. Verified opportunity 5483750 imports end-to-end in dev via ngrok. (dfranks)
|
package/package.json
CHANGED