toga-ai 1.0.340 → 1.0.342
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.
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [TOGa Supply (toga2-supply) Architecture](architecture.md) | `toga2-supply` is the **React + Vite frontend** for TOGa Supply — warehouse fulfillment tooling (shipment selection, fulfill & ship against carrier APIs, NetSui | toga2-supply/src/api/toga.ts, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx |
|
|
6
|
-
| [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
|
|
6
|
+
| [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
|
|
7
7
|
| [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-supply` (React + Vite) builds and deploys on **AWS Amplify**. | toga2-supply/amplify.yml, toga2-supply/.gitattributes, toga2-supply/.github/workflows/sync-stage-environments.yml, toga2-supply/.env.qc-security |
|
|
@@ -6,7 +6,7 @@ project: TOGa Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-15
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx
|
|
@@ -15,6 +15,8 @@ files:
|
|
|
15
15
|
- toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx
|
|
16
16
|
- toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx
|
|
17
17
|
- toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json
|
|
18
|
+
- toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx
|
|
19
|
+
- toga2-supply/tailwind.config.cjs
|
|
18
20
|
- toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx
|
|
19
21
|
- toga2-supply/src/components/ui/BaseInput/BaseInput.tsx
|
|
20
22
|
- toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts
|
|
@@ -102,6 +104,19 @@ layout, labels, required-ness, or ordering, edit the **config JSON**, not the co
|
|
|
102
104
|
**Every react-select field's form value is the whole option OBJECT `{uuid, name}`, never a
|
|
103
105
|
bare string** — see the gotcha below before wiring a new select field.
|
|
104
106
|
|
|
107
|
+
- **Renderer extension points (how to add a non-BaseInput control).** `renderEditShipmentFormInput.tsx`
|
|
108
|
+
special-cases fields by boolean flags on the JSON field object (existing: `isClearAll`, `isReadOnly`;
|
|
109
|
+
added `isUnitSelector` = a native in./cm `<select>`, mirroring the Return Shipping modal — visual-only,
|
|
110
|
+
not persisted). **Reuse pattern for any custom control: add a flag to the JSON field + a branch in the
|
|
111
|
+
renderer** rather than forcing it through `BaseInput`. A per-section `sectionTitleContainerClasses` is
|
|
112
|
+
also read by `EditShipmentForm.tsx`'s shared section-title wrapper (default unchanged) so a section can
|
|
113
|
+
e.g. right-align its title (used to right-align the Item Count column's title + value to a shared edge).
|
|
114
|
+
- **⚠ A custom/non-data field MUST set `valueKey: ""`.** Before rendering, `EditShipmentForm.tsx` runs
|
|
115
|
+
**every** field through `getValueFromPath(data, field.valueKey)`, which does `valueKey.split(".")` with
|
|
116
|
+
**no guard** — a field with no `valueKey` **white-screens the whole form** (`Cannot read properties of
|
|
117
|
+
undefined (reading 'split')`). Set `valueKey: ""` (same convention Clear All / Address already use) for
|
|
118
|
+
any field not bound to data.
|
|
119
|
+
|
|
105
120
|
- **Create vs Edit are distinct entry points (decision — kept, not merged).** Select Items →
|
|
106
121
|
Next = create a **New Shipment**; the pending-shipments pencil = **edit** an existing draft.
|
|
107
122
|
They are deliberately NOT merged and there is no auto-load from Select Items, because a Sales
|
|
@@ -180,6 +195,19 @@ the call 403s and returns no label.
|
|
|
180
195
|
|
|
181
196
|
## Gotchas / known issues
|
|
182
197
|
|
|
198
|
+
- **⚠ Tailwind does NOT scan the field-config JSON — a class used ONLY in `viewModel/FIELDS/*.json`
|
|
199
|
+
is silently inert.** The config-driven forms author Tailwind utility classes inside the field JSON
|
|
200
|
+
(e.g. `DUMMYUPDATESHIPMENTFIELDS.json`), but `tailwind.config.cjs` `content` globs only scan
|
|
201
|
+
`./src/**/*.{js,ts,jsx,tsx}` (+ `index.html` + toga-blox dist) — **not `.json`**. So a class used
|
|
202
|
+
*only* in the JSON never has its CSS rule generated: it appears in the DOM `className` but does
|
|
203
|
+
nothing. Common classes (`gap-1`, `w-4`, `w-full`) happen to also appear in `.tsx` so they compile;
|
|
204
|
+
unusual JSON-only classes do not. *Concrete failure:* `!w-fit` (keep the "Needs a return label"
|
|
205
|
+
checkbox content-width so the Edit link stays beside it) was JSON-only → never generated → `w-full`
|
|
206
|
+
won → the checkbox stretched with the column and the Edit gap grew on wide screens (col1 is `w-56`
|
|
207
|
+
above 1893px, `w-150px` below), fine only when narrow. **Fix applied (surgical):** added
|
|
208
|
+
`safelist: ["!w-fit"]` to `tailwind.config.cjs`. **Recommended clean fix (NOT yet done):** add the
|
|
209
|
+
FIELDS JSON to the `content` globs (e.g. `"./src/**/FIELDS/*.json"`) and re-test all config-driven
|
|
210
|
+
forms, since any JSON-only class is otherwise inert.
|
|
183
211
|
- **UPS test endpoint** (`wwwcie.ups.com`, debug mode) returns a constant tracking
|
|
184
212
|
number `1ZXXXXXXXXXXXXXXXX` + SAMPLE labels. On 2nd+ test runs this collides with the
|
|
185
213
|
`TrackingNumbers.number` UNIQUE index (surfaces mislabeled as "Labelary API error")
|
|
@@ -345,6 +373,15 @@ not the base `_Model_Client_ItemFulfillment`. Tested with GroWrk; UPS support wa
|
|
|
345
373
|
for Compass and is not yet in prod.
|
|
346
374
|
|
|
347
375
|
## Change history
|
|
376
|
+
- 2026-07-15 — TRUE-79191 (Figma-alignment polish pass): documented two durable items on the
|
|
377
|
+
config-driven form — (1) **Tailwind does not scan `viewModel/FIELDS/*.json`**, so a JSON-only utility
|
|
378
|
+
class is silently inert (`!w-fit` footgun; surgical `safelist` fix, recommended clean fix = add the
|
|
379
|
+
JSON to `content` globs); (2) **renderer extension points** — the `isUnitSelector` custom-control
|
|
380
|
+
branch + the flag-in-JSON/branch-in-renderer pattern, per-section `sectionTitleContainerClasses`, and
|
|
381
|
+
the **`valueKey: ""` requirement** for any custom/non-data field (empty/absent `valueKey` white-screens
|
|
382
|
+
the form via `getValueFromPath`'s unguarded `.split`). Remaining Figma tweaks (Dimensions in./cm
|
|
383
|
+
dropdown + gaps, Item Count right-align, return-label row gaps, shipment-card indicator top gap) were
|
|
384
|
+
cosmetic — no doc change. (mhammontree)
|
|
348
385
|
- 2026-07-14 — TRUE-79191 (commit `724d17c1a`): fixed **Signature Type** not saving/rehydrating —
|
|
349
386
|
documented the reusable **whole-object react-select** convention (`{uuid,name}`; map on `.uuid`,
|
|
350
387
|
rehydrate the object) as a gotcha; the string comparison `data.signatureType === "Required"`
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
| [Etilize Catalog Item Import & Refresh](features/etilize-catalog-item-import.md) | Client-generic catalog onboarding from an S3 CSV plus an Etilize re-pull. | worker2/Worker/Etilize/Items.php |
|
|
17
17
|
| [Etilize Item Translation Import](features/etilize-item-translation-import.md) | The abstract worker class `_Worker_Etilize_ItemTranslations` imports **non-English** item text from Etilize into the client's `ItemTranslations` table. | worker2/Worker/Etilize/ItemTranslations.php |
|
|
18
18
|
| [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/Monitors/RateEntitlement.php, worker2/Worker/Notification/Email.php, worker2/Worker/Rate.php, dbchanges2/Core/2026-05-21 - Monitors.sql, dbchanges2/Core/2026-06-29a - Rate Entitlement Contract Monitor.sql |
|
|
19
|
-
| [NetSuite ↔ ClickUp / 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/Worker/Clickup.php, worker2/Worker/Clickup/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, test/@dave/probe_clickup_desc_match.php, test/@dave/test_model_load_behavior.php, dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
19
|
+
| [NetSuite ↔ ClickUp / 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/Worker/Clickup.php, worker2/Worker/Clickup/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, test/@dave/probe_clickup_desc_match.php, test/@dave/test_model_load_behavior.php, dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql, _underscore/Model/Forecast/Opportunity.php, test/@dave/approach/TRUE-80044.md, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
20
20
|
| [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
21
21
|
| [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, worker/crons/toga2/forecast2/import_supporting_records.php |
|
|
22
22
|
| [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, _underscore/Model/Client/EmailTemplate.php |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-15
|
|
10
10
|
owners: ["dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite.php
|
|
@@ -25,6 +25,8 @@ files:
|
|
|
25
25
|
- test/@dave/probe_clickup_desc_match.php
|
|
26
26
|
- test/@dave/test_model_load_behavior.php
|
|
27
27
|
- "dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql"
|
|
28
|
+
- _underscore/Model/Forecast/Opportunity.php
|
|
29
|
+
- test/@dave/approach/TRUE-80044.md
|
|
28
30
|
- worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php
|
|
29
31
|
related:
|
|
30
32
|
- ./netsuite-salesorder-open-orders-sync.md
|
|
@@ -166,6 +168,39 @@ Shared helpers: `buildCustomFields()` (the field array, used by both create and
|
|
|
166
168
|
removing that `setLogging(false)` is under review (handle the 20–50 KB payload volume the
|
|
167
169
|
prod-appropriate way — retention/sampling/summary — not by defaulting logging off).
|
|
168
170
|
|
|
171
|
+
## Atomic ClickUp-task claim (TRUE-80044 — decided approach, not yet built)
|
|
172
|
+
|
|
173
|
+
Concurrent NetSuite opportunity webhooks (a real edit racing NetSuite's hourly `OPP` fallback) can both
|
|
174
|
+
pass the non-atomic find-then-create in `maybeCreateClickupTask()` and create **duplicate** ClickUp
|
|
175
|
+
tasks. The fix is a **race-safe DB claim** on the **existing `Forecast.Opportunities` table** — an added
|
|
176
|
+
`clickupTaskId` column — **not** a new dedicated table. (A prior plan proposed a new
|
|
177
|
+
`NetsuiteOpportunityClickupTask` table in `Client_True`; reviewer Rohan Girish rejected that and mandated
|
|
178
|
+
reusing `Forecast.Opportunities`.)
|
|
179
|
+
|
|
180
|
+
How the claim is race-safe:
|
|
181
|
+
- **Conditional UPDATE = the atomic claim.** A raw `_Query`:
|
|
182
|
+
`UPDATE Opportunities SET clickupTaskId='<uuid sentinel>' WHERE netsuiteOpportunityInternalId=X AND
|
|
183
|
+
clickupTaskId IS NULL`, then commit. `affectedRows === 1` → this webhook won the claim and proceeds to
|
|
184
|
+
create the task; `=== 0` → another concurrent handler already claimed it → **skip**. The winner then
|
|
185
|
+
persists the real task id over the sentinel; a `createTask()` failure runs a **compensation-delete**
|
|
186
|
+
(reset `clickupTaskId` back to `NULL`, guarded by the sentinel uuid so it only clears its own claim).
|
|
187
|
+
- It works because (a) `Forecast.Opportunities` already has a **UNIQUE index on
|
|
188
|
+
`netsuiteOpportunityInternalId`** (`dbchanges2/Forecast/2026-06-25a`) → the guarded UPDATE locks a
|
|
189
|
+
single row, and (b) in both `post()` and `put()`, `importOpportunity()` runs and **COMMITS** the
|
|
190
|
+
Forecast row (`Opportunity.php:289`) **before** `maybeCreateClickupTask()` is called, so the row exists
|
|
191
|
+
and is committed at claim time.
|
|
192
|
+
- **Raw `_Query` is required** for the guarded UPDATE — the `_underscore` ORM `save()` is load-then-write
|
|
193
|
+
and cannot express an atomic guarded UPDATE. All non-claim writes stay on the ORM.
|
|
194
|
+
- **Commit the claim BEFORE the ClickUp POST** — releases the single-row lock and dodges the
|
|
195
|
+
`DB_FORECAST` lazy-transaction drop.
|
|
196
|
+
- **`createTask()` must return the new task id (`:string`)** — it currently returns `void` and discards
|
|
197
|
+
the ClickUp response; the winner needs the id to persist over the sentinel.
|
|
198
|
+
- **Edge case:** if `importOpportunity()` was skipped (no NS status → no Forecast row), there is nothing
|
|
199
|
+
to claim; that rare path falls back to today's unguarded create.
|
|
200
|
+
|
|
201
|
+
Requires a new `dbchanges2/Forecast/` ALTER adding the `clickupTaskId` column, and a
|
|
202
|
+
`_underscore/Model/Forecast/Opportunity.php` field addition. Plan: `test/@dave/approach/TRUE-80044.md`.
|
|
203
|
+
|
|
169
204
|
## Data model
|
|
170
205
|
|
|
171
206
|
Writes `Forecast.Opportunities` (header) + `Forecast.OpportunityItems` (children), faithful to
|
|
@@ -287,16 +322,18 @@ None — platform-wide Forecast sync.
|
|
|
287
322
|
the create branch) you must change the **leading** content or clear the field entirely. Verified both ways
|
|
288
323
|
on opp 74266/internalId 7161054: `74266--` → matched (update branch, skipped); field removed → missed
|
|
289
324
|
(create branch, new task `868k2rfj8`).
|
|
290
|
-
- **ClickUp dedup keys on the `Opportunity #` field, not a DB column.**
|
|
291
|
-
a `clickupTaskId` column — the dedup lookup
|
|
292
|
-
|
|
325
|
+
- **ClickUp dedup keys on the `Opportunity #` field, not a DB column.** Historically we did NOT add
|
|
326
|
+
a `clickupTaskId` column — the dedup lookup queried ClickUp itself by `Opportunity #` (= tranId).
|
|
327
|
+
**This is being replaced (TRUE-80044) by a race-safe DB-claim approach — see "Atomic ClickUp-task
|
|
328
|
+
claim" below.** Consequences of the field-lookup approach to know:
|
|
293
329
|
- **Legacy tasks had `Opportunity #` empty** (the old `webhook/` handler never set it), so they
|
|
294
330
|
can't be matched until backfilled — see the backfill tool below. Without backfill, the first
|
|
295
331
|
edit of a pre-existing opp creates a *second* task.
|
|
296
332
|
- **The find-then-create is not atomic.** Two near-simultaneous events for one opp (e.g. the
|
|
297
333
|
NetSuite hourly `OPP` fallback firing alongside a real edit) can both miss and both create. A
|
|
298
334
|
committed local task id would close most of that window; the field-lookup approach can't.
|
|
299
|
-
ClickUp custom fields aren't unique-constrained, so some race exists regardless.
|
|
335
|
+
ClickUp custom fields aren't unique-constrained, so some race exists regardless. **TRUE-80044
|
|
336
|
+
closes this window with the atomic `clickupTaskId` DB claim — see "Atomic ClickUp-task claim" above.**
|
|
300
337
|
- **Update is per-field** (`/task/{id}/field/{id}` — no bulk set), so a mid-loop failure leaves a
|
|
301
338
|
partially-updated task; the thrown error names how far it got. The Presales Lead users field is
|
|
302
339
|
**add-only** on update (we don't read the prior assignee to remove it).
|
|
@@ -449,6 +486,19 @@ deprecated** for production opportunity code.
|
|
|
449
486
|
blocked on the Aaron stakeholder decision noted above.
|
|
450
487
|
|
|
451
488
|
## Change history
|
|
489
|
+
- 2026-07-15 — **Decided the atomic ClickUp-task dedup approach (TRUE-80044) — planning/rework, no code
|
|
490
|
+
shipped.** The non-atomic find-then-create lets concurrent NS opportunity webhooks create duplicate
|
|
491
|
+
ClickUp tasks. Decided fix: a race-safe **conditional-UPDATE claim** on a new `clickupTaskId` column of
|
|
492
|
+
the **existing `Forecast.Opportunities`** table (`UPDATE … SET clickupTaskId='<uuid>' WHERE
|
|
493
|
+
netsuiteOpportunityInternalId=X AND clickupTaskId IS NULL`; commit; `affectedRows===1` wins, `===0`
|
|
494
|
+
skips), with a sentinel-guarded compensation-delete on `createTask()` failure. Reviewer (Rohan Girish)
|
|
495
|
+
**rejected** the prior plan's new dedicated `NetsuiteOpportunityClickupTask` table and mandated the
|
|
496
|
+
column-on-Forecast.Opportunities approach — safe because the table already has a UNIQUE index on
|
|
497
|
+
`netsuiteOpportunityInternalId` (single-row lock) and `importOpportunity()` commits the row before
|
|
498
|
+
`maybeCreateClickupTask()` runs. Requires raw `_Query` (ORM `save()` can't express a guarded UPDATE),
|
|
499
|
+
claim committed before the ClickUp POST (lock release + lazy-transaction), and `createTask()` changed to
|
|
500
|
+
return the new task id. See "Atomic ClickUp-task claim" section. Plan: `test/@dave/approach/TRUE-80044.md`.
|
|
501
|
+
(dfranks)
|
|
452
502
|
- 2026-06-30 — **Built the CU→NS reverse sync + NS→CU stage drive (TRUE-79181) — integration now
|
|
453
503
|
bidirectional for field + stage.** New `_Worker_Clickup_Opportunity` handler
|
|
454
504
|
(`worker2/Worker/Clickup/Opportunity.php`), dispatched by `_Worker_Clickup::Webhook` via
|
package/package.json
CHANGED