toga-ai 1.0.370 → 1.0.372
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.
- package/knowledge/2.0/apps/_underscore/INDEX.md +3 -1
- package/knowledge/2.0/apps/_underscore/architecture/external-identifiers-map-table.md +77 -0
- package/knowledge/2.0/apps/_underscore/features/model-save-vs-query-atomic-update.md +75 -0
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +45 -11
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +78 -1
- package/knowledge/2.0/apps/worker2/features/netsuite-opportunity-sync.md +4 -0
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
|
+
| [Proposed — ExternalIdentifiers map table for integration id ↔ external-system linkage](architecture/external-identifiers-map-table.md) | A **proposal / counter-proposal (status: proposed — nothing here is built)** that cross-system identifier linkage (e.g. | _underscore/Model/Forecast/Opportunity.php, worker2/Worker/Netsuite/Opportunity.php |
|
|
5
6
|
| [Proposed — git-sourced base+overlay JSON authoring for the Surface layer](architecture/surface-authoring-proposal.md) | A **proposal / handoff recommendation** (not implemented) that the Surface layer's *authoring* model move off hand-authored SQL against the `SurfaceOverrides` E | _underscore/Model/Core/Surface.php, _underscore/Model/Client/SurfaceOverride.php |
|
|
6
7
|
| [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
|
|
7
8
|
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
|
|
@@ -18,10 +19,11 @@
|
|
|
18
19
|
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/probe_je_shape.php, test/@dave/fixer.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
19
20
|
| [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Every ItemFulfillment (IF) now carries an explicit **stage** — picked → packed → shipped — resolved through `ItemFulfillmentStages → ItemFulfillmentStatuses` (m | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderStatus.php, _underscore/Model/Client/SalesOrderItem.php, _underscore/Model/Client/Item.php, _underscore/Model/Client/PurchaseOrderItem.php, library/app/api/toga2.php, dbchanges2/Client/2026-06-30a - BackfillNullStageItemFulfillmentsToShipped.sql, dbchanges2/Client/2026-06-30b - SalesOrderStatusesPickedPacked.sql, dbchanges2/Client/2026-06-30c - ItemFulfillmentStageIdNotNull.sql, dbchanges2/Client_CompassCanada/2026-06-30a - ItemFulfillmentLifecycleAndShippedBackfill.sql |
|
|
20
21
|
| [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php |
|
|
22
|
+
| [_Model::save() vs raw _Query — no atomic conditional update](features/model-save-vs-query-atomic-update.md) | `_Model::save()` is a plain load-then-write ORM primitive and **cannot express an atomic conditional update** (an optimistic-concurrency / row-claim guard such | _underscore/Model.php, _underscore/Query.php |
|
|
21
23
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
|
|
22
24
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
|
|
23
25
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
|
|
24
|
-
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
|
|
26
|
+
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
|
|
25
27
|
| [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
|
|
26
28
|
| [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
|
|
27
29
|
| [Refreshing a Local Dev Database from Beta (dev-sandbox)](workflows/local-db-refresh-from-beta.md) | How to reset a local 2.0 dev database from the **beta / dev-sandbox** environment: dump each schema (`Core`, `Client_<Id>`, `Logs_<Id>`, …) from the beta host, | api2/Config/, _underscore/Loader.php, _underscore/Model/Client/BundleTranslation.php, api2/Component/Api/V2/V2.php, toga25-supply/sync_compasscanada_schema.sql |
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Proposed — ExternalIdentifiers map table for integration id ↔ external-system linkage
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: architecture
|
|
8
|
+
status: proposed
|
|
9
|
+
updated: 2026-07-17
|
|
10
|
+
owners: ["dfranks"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Forecast/Opportunity.php
|
|
13
|
+
- worker2/Worker/Netsuite/Opportunity.php
|
|
14
|
+
related:
|
|
15
|
+
- ../features/model-save-vs-query-atomic-update.md
|
|
16
|
+
- ../../worker2/features/netsuite-opportunity-sync.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
A **proposal / counter-proposal (status: proposed — nothing here is built)** that cross-system
|
|
22
|
+
identifier linkage (e.g. a NetSuite opportunity ↔ its ClickUp task id) live in a dedicated
|
|
23
|
+
**`ExternalIdentifiers` map table** rather than as ad-hoc columns bolted onto whichever business
|
|
24
|
+
table happens to be nearby. The immediate driver is the TRUE-80044 atomic ClickUp-task claim, whose
|
|
25
|
+
currently-mandated approach adds a `clickupTaskId` column to `Forecast.Opportunities`. This doc is
|
|
26
|
+
`dfranks`'s architectural counter-proposal to that mandate, raised for open discussion — **not** a
|
|
27
|
+
silent supersede of the reviewer decision. See the rebuttal section below.
|
|
28
|
+
|
|
29
|
+
**Critical framing:** This is a **proposal under open discussion**, not the current system and not a
|
|
30
|
+
settled reversal. The mandated approach on record (a `clickupTaskId` column on
|
|
31
|
+
`Forecast.Opportunities`) remains the standing decision until the disagreement is resolved with the
|
|
32
|
+
reviewer.
|
|
33
|
+
|
|
34
|
+
## Design
|
|
35
|
+
|
|
36
|
+
A single shared table keyed by (local record identity, external system, external id kind):
|
|
37
|
+
|
|
38
|
+
- **Columns (sketch):** `id`, `entityType` (e.g. `opportunity`), `entityId` (the local business
|
|
39
|
+
key, e.g. `netsuiteOpportunityInternalId`), `externalSystem` (e.g. `clickup`), `externalIdKind`
|
|
40
|
+
(e.g. `task`), `externalId` (the value/sentinel), plus `dtCreated`/`dtUpdated`.
|
|
41
|
+
- **Uniqueness:** a UNIQUE index on `(entityType, entityId, externalSystem, externalIdKind)` — this
|
|
42
|
+
is the single-row lock that makes the atomic claim (`UPDATE … SET externalId='<uuid sentinel>'
|
|
43
|
+
WHERE … AND externalId IS NULL`, commit, `affectedRows===1` wins) work exactly as the mandated
|
|
44
|
+
column approach would, but without touching the Forecast schema.
|
|
45
|
+
- **Atomic claim mechanics are unchanged** from the mandated design — same raw `_Query` guarded
|
|
46
|
+
UPDATE (ORM `save()` cannot express it — see
|
|
47
|
+
[model-save-vs-query-atomic-update](../features/model-save-vs-query-atomic-update.md)), same
|
|
48
|
+
commit-before-external-side-effect, same sentinel-guarded compensation-delete on failure.
|
|
49
|
+
|
|
50
|
+
## Rebuttal to the clickupTaskId-column mandate
|
|
51
|
+
|
|
52
|
+
`2.0/apps/worker2/features/netsuite-opportunity-sync.md` records reviewer **Rohan Girish** mandating
|
|
53
|
+
(2026-07-15) that the ClickUp-task dedup claim be implemented as a **`clickupTaskId` column on
|
|
54
|
+
`Forecast.Opportunities`**, and rejecting the earlier plan's dedicated table. This doc is `dfranks`'s
|
|
55
|
+
**counter-proposal**, offered as an open architectural disagreement to be resolved *with* the
|
|
56
|
+
reviewer — the mandate stands until then. The three rebuttal arguments:
|
|
57
|
+
|
|
58
|
+
1. **Separation of concerns.** A ClickUp task id is *integration-linkage* state, not a business
|
|
59
|
+
attribute of a Forecast opportunity. Storing it on `Forecast.Opportunities` conflates the
|
|
60
|
+
integration/mapping layer with the forecasting domain model.
|
|
61
|
+
2. **Forecast-schema dumping-ground.** Every future external linkage (a second external system, a
|
|
62
|
+
different id kind) under the column approach means another one-off nullable column on a business
|
|
63
|
+
table, accreting integration cruft on the Forecast schema. A map table absorbs all of them with
|
|
64
|
+
no schema change per linkage.
|
|
65
|
+
3. **Forecast-sync lifecycle decoupling.** The integration mapping must be able to **outlive a
|
|
66
|
+
possibly-discontinued Forecast sync.** Binding the ClickUp linkage to the Forecast opportunity row
|
|
67
|
+
ties its lifetime to the Forecast importer; if that sync is ever retired or reworked, the mapping
|
|
68
|
+
goes with it. A standalone map table keeps the NS↔ClickUp linkage durable independently of
|
|
69
|
+
Forecast.
|
|
70
|
+
|
|
71
|
+
This is framed as a disagreement to resolve, not a reversal already taken. If the reviewer position
|
|
72
|
+
holds, the mandated column approach is what ships.
|
|
73
|
+
|
|
74
|
+
## Change history
|
|
75
|
+
|
|
76
|
+
- 2026-07-17 — Proposed as a rebuttal to the reviewer-mandated clickupTaskId-column approach
|
|
77
|
+
(TRUE-80044). (dfranks)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: _Model::save() vs raw _Query — no atomic conditional update
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-17
|
|
10
|
+
owners: ["dfranks"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model.php
|
|
13
|
+
- _underscore/Query.php
|
|
14
|
+
related:
|
|
15
|
+
- ./model-magic-field-access.md
|
|
16
|
+
- ../../worker2/features/netsuite-opportunity-sync.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
`_Model::save()` is a plain load-then-write ORM primitive and **cannot express an atomic
|
|
22
|
+
conditional update** (an optimistic-concurrency / row-claim guard such as
|
|
23
|
+
`... WHERE col IS NULL`). For any DB-level claim / optimistic-concurrency pattern you must drop
|
|
24
|
+
to raw `_Query` — `_Query::getAffectedRows()` is the only surface that exposes an affected-rows
|
|
25
|
+
count, which is what a claim needs to know whether *this* caller won. `save()` remains the correct
|
|
26
|
+
idiom for ordinary by-primary-key writes.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
`_Model::save()`'s update branch always emits:
|
|
31
|
+
|
|
32
|
+
```sql
|
|
33
|
+
UPDATE <table> SET <changed cols> WHERE <primaryKey> = <id>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
(`Model.php` ~line 285). Consequences that make it unusable for a conditional claim:
|
|
37
|
+
|
|
38
|
+
- **No conditional guard.** The `WHERE` is hard-coded to the primary key; there is no way to attach
|
|
39
|
+
an extra predicate like `AND clickupTaskId IS NULL`. So two concurrent callers both pass any
|
|
40
|
+
in-PHP check and both write — the update is not a claim.
|
|
41
|
+
- **Skips the write when nothing changed.** If no field differs from the loaded row, `save()` emits
|
|
42
|
+
no UPDATE at all. A claim that relies on "did my write land?" gets no signal.
|
|
43
|
+
- **Returns no affected-rows count.** `save()` does not surface how many rows the UPDATE touched, so
|
|
44
|
+
even a guarded statement (if it could emit one) couldn't be adjudicated through the model.
|
|
45
|
+
|
|
46
|
+
**For an atomic claim / conditional update, use `_Query` directly.** `_Query::getAffectedRows()`
|
|
47
|
+
(`Query.php` line 106) is the only API that exposes the affected-rows count. The idiom is:
|
|
48
|
+
|
|
49
|
+
```php
|
|
50
|
+
// UPDATE ... SET claim='<sentinel>' WHERE <businessKey>=? AND claim IS NULL (raw _Query)
|
|
51
|
+
// then commit; affectedRows === 1 → this caller won the claim; === 0 → another already claimed it.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Back the guarded UPDATE with a UNIQUE index on the business key so it locks a single row, and commit
|
|
55
|
+
before any external side effect so the row lock is released (and to dodge the `_Database` lazy-transaction
|
|
56
|
+
write-drop). All **non-claim** writes stay on the ORM — `save()` is fine for plain
|
|
57
|
+
load → set → save → `transactionCommit` by primary key (the idiom
|
|
58
|
+
`_Worker_Netsuite_Opportunity::delete()` already uses).
|
|
59
|
+
|
|
60
|
+
## Gotchas / known issues
|
|
61
|
+
|
|
62
|
+
- Do **not** reach for `save()` to implement a row-claim / optimistic-concurrency guard — it silently
|
|
63
|
+
degrades to a non-atomic check-then-act because its `WHERE` is primary-key-only and it exposes no
|
|
64
|
+
affected-rows count. Use raw `_Query` + `getAffectedRows()`.
|
|
65
|
+
- A no-diff `save()` emits **no** UPDATE, so "the write must have run because save() returned" is not a
|
|
66
|
+
safe assumption.
|
|
67
|
+
|
|
68
|
+
## Change history
|
|
69
|
+
|
|
70
|
+
- 2026-07-17 — Documented that `_Model::save()` cannot perform an atomic conditional update
|
|
71
|
+
(primary-key-only `WHERE`, skips no-diff writes, no affected-rows count) and that raw `_Query` +
|
|
72
|
+
`_Query::getAffectedRows()` is the required surface for any DB row-claim / optimistic-concurrency
|
|
73
|
+
pattern. Surfaced while designing the TRUE-80044 atomic ClickUp-task claim. (dfranks)
|
|
74
|
+
</content>
|
|
75
|
+
</invoke>
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-17
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Core/Surface.php
|
|
@@ -20,6 +20,13 @@ files:
|
|
|
20
20
|
- dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql
|
|
21
21
|
- dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
|
|
22
22
|
- dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql
|
|
23
|
+
- dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql
|
|
24
|
+
- dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql
|
|
25
|
+
- dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql
|
|
26
|
+
- dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql
|
|
27
|
+
- dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql
|
|
28
|
+
- dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql
|
|
29
|
+
- dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql
|
|
23
30
|
- _underscore/Model/Core/SurfaceElement.php
|
|
24
31
|
- _underscore/Model/Core/Action.php
|
|
25
32
|
- _underscore/Model/Core/Vocabulary.php
|
|
@@ -186,16 +193,35 @@ Compass's shape. Core was re-baselined as a true shared default so clients store
|
|
|
186
193
|
4. Edit-in-Commerce only for Compass / CompassCanada.
|
|
187
194
|
5. Columns button hidden by default.
|
|
188
195
|
|
|
189
|
-
Per-client diffs
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
### Per-client sales-order button diffs — materialized 2026-07-17
|
|
197
|
+
The Core neutral default now covers the **approvals filter button too**: `Core/2026-07-17h` cleared
|
|
198
|
+
element 22's `config` to `{}`, so Core carries no `_status` default and **every** client (even Quad,
|
|
199
|
+
whose value matches the old default) supplies the whole filter `CONFIG` via its own override (REPLACE
|
|
200
|
+
semantics). Concrete per-client diffs, all via `SurfaceOverrides` (rule JSON is
|
|
201
|
+
`{"all":[{"field":"order._status","op":"in","value":[…]}]}`; seed authoring/element-id map in the
|
|
202
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md) doc):
|
|
203
|
+
|
|
204
|
+
- **Compass USA / Compass Canada** (mirror each other) — two-stage approval:
|
|
205
|
+
- **ADMIN**: opts into all four gated record-action buttons (approve/deny/approvalWorkflow/editOrder)
|
|
206
|
+
`IS_VISIBLE=1`, and widens **both** `VISIBILITY_RULE` **and** `ENABLED_RULE` to
|
|
207
|
+
`_status in [pendingApproval, pendingInitialApproval]`. (For approve, the Tier-2 `stepTwoAssigned`
|
|
208
|
+
predicate still further gates *enabled* in PHP.) On the approvals filter button: `IS_VISIBLE=1` +
|
|
209
|
+
`CONFIG` `additionalData._status = [pendingApproval, pendingInitialApproval]`.
|
|
210
|
+
- **MANAGER**: opts into **approve + deny only** (`IS_VISIBLE=1`); rules left at the Core base
|
|
211
|
+
(`pendingApproval` only, single-stage). No approvals-filter override → inherits Core `isVisible=0`
|
|
212
|
+
(hidden).
|
|
213
|
+
- **Quad** — Approve + Deny only, visible for **role ids 7/8/9** (`IS_VISIBLE=1`), with **no** rule
|
|
214
|
+
override (status stays at the Core base `pendingApproval`). This is deliberately **per-role**, unlike
|
|
215
|
+
Quad's usual client-wide (`roleId NULL`) pattern — per request. Approvals filter button is
|
|
216
|
+
client-wide with `CONFIG` `_status=[pendingApproval]`; the "Enter PO Details" button (element 32) is
|
|
217
|
+
made client-wide visible (⚠ dual-gate — see the schema doc; the FE may also read `config.isVisible`).
|
|
218
|
+
|
|
219
|
+
> ✅ **Regression (mostly) closed:** the two-stage rule overrides and the record-action/filter opt-ins
|
|
220
|
+
> for Compass / Compass Canada that were wiped in the earlier rebuild were **re-added 2026-07-17**
|
|
221
|
+
> (`Client_Compass`/`Client_CompassCanada/2026-07-17a`+`b`). These surface DB changes remain **local/
|
|
222
|
+
> beta only** pending promotion; the `_underscore` rule-override code + the ENUM widening still ship
|
|
223
|
+
> together on promotion, and the php-reviewer pass + PHPUnit regression for the rule-override path are
|
|
224
|
+
> still outstanding.
|
|
199
225
|
|
|
200
226
|
### Operational gotcha — raw SQL bypasses the cache-bust
|
|
201
227
|
Raw SQL against the surface tables bypasses the model-layer cache-bust, so the on-disk bundle cache
|
|
@@ -312,6 +338,14 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
|
|
|
312
338
|
match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
|
|
313
339
|
|
|
314
340
|
## Change history
|
|
341
|
+
- 2026-07-17 — Materialized the per-client sales-order button diffs (re-adding the overrides wiped in
|
|
342
|
+
the earlier rebuild): Compass/CompassCanada ADMIN = all four record-action buttons visible + both
|
|
343
|
+
`VISIBILITY_RULE`/`ENABLED_RULE` widened to `[pendingApproval, pendingInitialApproval]`, MANAGER =
|
|
344
|
+
approve+deny visible with Core-base rules; Quad = approve+deny visible per-role (ids 7/8/9), no rule
|
|
345
|
+
override. Extended the Core neutral default to the **approvals filter button** — `Core/2026-07-17h`
|
|
346
|
+
cleared element 22's config so every client (including Quad) now supplies the full filter `CONFIG`
|
|
347
|
+
(REPLACE semantics): Compass/CC admin = two statuses, Quad client-wide = `pendingApproval`. Overrides
|
|
348
|
+
remain local/beta only pending promotion. (apeterson)
|
|
315
349
|
- 2026-07-16 — Recorded a resolver gotcha: a `SurfaceOverrides` row with `attribute='CONFIG'` carrying
|
|
316
350
|
a rule value is silently dead — `_castOverride` routes CONFIG→`$config`, not
|
|
317
351
|
`$visibilityRule`/`$enabledRule` (logs + returns null), and `overridesByElementAndAttr` keyed by
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
|
|
6
|
-
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql |
|
|
6
|
+
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql |
|
|
7
7
|
| [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | How to manually stand up a new 2.0 client (tenant). | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-17
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
|
|
@@ -31,6 +31,13 @@ files:
|
|
|
31
31
|
- dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql
|
|
32
32
|
- dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql
|
|
33
33
|
- dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql
|
|
34
|
+
- dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql
|
|
35
|
+
- dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql
|
|
36
|
+
- dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql
|
|
37
|
+
- dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql
|
|
38
|
+
- dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql
|
|
39
|
+
- dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql
|
|
40
|
+
- dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql
|
|
34
41
|
related:
|
|
35
42
|
- ../../_underscore/features/surface-resolver.md
|
|
36
43
|
---
|
|
@@ -114,6 +121,52 @@ it `isVisible: true`. Both `Core/2026-06-29a` (Item) and `Core/2026-06-29d` (Ven
|
|
|
114
121
|
off." A client opts a role in via a `SurfaceOverride` (`IS_VISIBLE=1`), scoped by role/persona. This is
|
|
115
122
|
the seed-authoring corollary of the Core neutral-default re-baseline recorded in the resolver doc.
|
|
116
123
|
|
|
124
|
+
## Sales-order button overrides — element identities & the id-agnostic authoring house style
|
|
125
|
+
|
|
126
|
+
When a client opts its roles into the sales-order action/filter buttons (the resolved per-client
|
|
127
|
+
*behavior* lives in the [surface-resolver](../../_underscore/features/surface-resolver.md)
|
|
128
|
+
per-client-diffs section), the override seeds follow a strict **id-agnostic house style** so one file
|
|
129
|
+
runs unchanged across local/beta/prod:
|
|
130
|
+
|
|
131
|
+
- **Resolve `roleId` by `Roles.name`** (`'Admin'`/`'Manager'`) — client role ids are environment-specific.
|
|
132
|
+
- **Resolve the Core element by surface slug + one discriminator**, never a hardcoded Core id:
|
|
133
|
+
`Core.Actions.slug` for action buttons; the **label `messageKey`** (`salesOrder.filter.approvals`)
|
|
134
|
+
for the approvals filter button — it has no `actionId` and, after the Core config wipe (below), no
|
|
135
|
+
`config` left to match on; or `config.valueKey` (`poNumber`) for the "Enter PO Details" button.
|
|
136
|
+
- **`personaId`/`languageId` NULL** for role/client scope (language-agnostic).
|
|
137
|
+
- **Every INSERT guarded by `NOT EXISTS`** on `(surfaceElementId, roleId, attribute, personaId NULL,
|
|
138
|
+
languageId NULL)` so the file is re-runnable. A per-role Quad seed does this with a
|
|
139
|
+
`CROSS JOIN (SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9)` + a `Roles` existence join, so a
|
|
140
|
+
missing role id is skipped rather than erroring.
|
|
141
|
+
- Cross-DB read of `Core.*` from the client seed mirrors the established Surface override pattern
|
|
142
|
+
(safe where Core+client are co-located; the prod cross-cluster caveat in the gotchas applies).
|
|
143
|
+
|
|
144
|
+
**Which attribute for which change:** `IS_VISIBLE`/`CONFIG` are in the base attribute ENUM;
|
|
145
|
+
`VISIBILITY_RULE`/`ENABLED_RULE` require the widened ENUM
|
|
146
|
+
(`Client/2026-07-15 - SurfaceOverridesAttributeRuleValues.sql`) applied per-tenant first (see the
|
|
147
|
+
resolver doc — an unknown ENUM value silently coerces to `''` in non-strict mode). A `CONFIG` override
|
|
148
|
+
**replaces the whole object**, so a filter override must carry the full config
|
|
149
|
+
(`role`+`filterKey`+`additionalData`), never just the changed key.
|
|
150
|
+
|
|
151
|
+
**Sales-order surface element identities** (Core seed `2026-07-17c` assigns EXPLICIT, stable ids — but
|
|
152
|
+
author overrides by slug/messageKey/valueKey, not by these numbers):
|
|
153
|
+
|
|
154
|
+
| Surface (id) | Element | id | Discriminator |
|
|
155
|
+
|---|---|---|---|
|
|
156
|
+
| `sales-order-record-actions` (8) | approve | 17 | action `salesOrder.approve` |
|
|
157
|
+
| | deny | 18 | action `salesOrder.deny` |
|
|
158
|
+
| | approvalWorkflow | 19 | action `salesOrder.approvalWorkflow` |
|
|
159
|
+
| | viewLog | 20 | always-visible / ungated (excluded from overrides) |
|
|
160
|
+
| | editOrder | 21 | action `salesOrder.editOrder` |
|
|
161
|
+
| | Enter PO Details | 32 | no `actionId`; `config.valueKey='poNumber'` |
|
|
162
|
+
| `sales-order-listing-actions` (9) | approvals filter | 22 | label messageKey `salesOrder.filter.approvals` |
|
|
163
|
+
| | columns | 23 | — |
|
|
164
|
+
|
|
165
|
+
**Core approvals-filter default removed (`Core/2026-07-17h`).** Core element 22's `config` was cleared
|
|
166
|
+
to `{}` so Core carries **no** `_status` default. Every client — even Quad, whose config matches the
|
|
167
|
+
old default — now declares the full filter config via its own `CONFIG` override. This makes the Core
|
|
168
|
+
seed a true neutral default for the filter button, matching the action-button rule above.
|
|
169
|
+
|
|
117
170
|
## ThemeTokens — per-tenant, physically isolated in each client DB
|
|
118
171
|
|
|
119
172
|
`ThemeTokens` lives **inside each tenant's own database** (`Client_Compass.ThemeTokens`,
|
|
@@ -251,8 +304,32 @@ rule resumes.
|
|
|
251
304
|
flip a section per role/persona. Also discovered: `elementType` in `orderViewFields.json` is **dead
|
|
252
305
|
config** (no renderer reads it); and MANAGER/Quad summary tooltips can't be removed via the cascade
|
|
253
306
|
because `TOOLTIP_MESSAGE` is numeric-only (cosmetic, deferred).
|
|
307
|
+
- **`Client_CompassCanada` may be missing the `SurfaceOverrides` table entirely** (its beta reset
|
|
308
|
+
predates the surface rollout). Prereqs — in order — **before any CompassCanada surface override
|
|
309
|
+
runs:** `Client/2026-06-25a - SurfaceClientTables.sql` (creates `ThemeTokens`/`MessageTranslations`/
|
|
310
|
+
`SurfaceOverrides`), then `Client/2026-07-15 - SurfaceOverridesAttributeRuleValues.sql` (ENUM
|
|
311
|
+
widening for the rule attributes); and for the `/surfaces/meta` endpoint to resolve at all, the ACL
|
|
312
|
+
dispatch grants (`Client/2026-06-25c`, `2026-06-29c`, `2026-06-30a`). Skipping the table/ENUM
|
|
313
|
+
bring-up makes the `2026-07-17a/b` override inserts fail or silently coerce.
|
|
314
|
+
- **"Enter PO Details" (element 32) has a DUAL visibility gate.** Its rendering is gated by BOTH the
|
|
315
|
+
element-level `isVisible` flag AND a `config`-embedded `"isVisible":false` (migrated from Quad's
|
|
316
|
+
legacy field JSON, alongside `isEnabled`/`isComplete`). Flipping the element-level `IS_VISIBLE=1`
|
|
317
|
+
(as `Client_Quad/2026-07-17b` block 3 does) may **not** render the button if the FE
|
|
318
|
+
(`SalesOrderTopBar`) reads `config.isVisible` — in which case a `CONFIG` override is also needed.
|
|
319
|
+
Element 32 is likewise `isEnabled=0` (appears but disabled until an `IS_ENABLED`/`ENABLED_RULE`
|
|
320
|
+
override is added). **Open follow-up.**
|
|
254
321
|
|
|
255
322
|
## Change history
|
|
323
|
+
- 2026-07-17 — Applied the Core-neutral-default → client-opt-in pattern to the sales-order action &
|
|
324
|
+
filter buttons across Compass, CompassCanada, and Quad (`Client_*/2026-07-17a`+`b`). Cleared the
|
|
325
|
+
Core approvals-filter `_status` default (`Core/2026-07-17h`) so every client declares its own filter
|
|
326
|
+
`CONFIG`. Documented the sales-order surface **element-identity map** (surface 8 elements 17-21/32;
|
|
327
|
+
surface 9 elements 22-23) and the **id-agnostic override authoring house style** (resolve `roleId`
|
|
328
|
+
by name; resolve Core elements by surface slug + `Actions.slug`/label messageKey/`config.valueKey`;
|
|
329
|
+
`personaId`/`languageId` NULL; `NOT EXISTS`-guarded). Added gotchas: `Client_CompassCanada` may lack
|
|
330
|
+
the `SurfaceOverrides` table (beta reset predates rollout — table/ENUM/ACL prereqs must run first),
|
|
331
|
+
and "Enter PO Details" (element 32) has a dual visibility gate (element-level `isVisible` AND a
|
|
332
|
+
`config.isVisible` the FE may also read). (apeterson)
|
|
256
333
|
- 2026-07-16 — Documented `SurfaceElements`' two orthogonal axes (`valueType` display vs `inputType`
|
|
257
334
|
edit widget — neither derived from the other) and the edit-mode columns added in `Core/2026-06-25a`
|
|
258
335
|
(`inputType`, `validationType` [renamed from `hasTypeCheck`], `showRequiredIndicator`, `isSearchable`,
|
|
@@ -31,6 +31,7 @@ files:
|
|
|
31
31
|
related:
|
|
32
32
|
- ./netsuite-salesorder-open-orders-sync.md
|
|
33
33
|
- ../architecture.md
|
|
34
|
+
- ../../_underscore/architecture/external-identifiers-map-table.md
|
|
34
35
|
---
|
|
35
36
|
|
|
36
37
|
## Summary
|
|
@@ -177,6 +178,9 @@ tasks. The fix is a **race-safe DB claim** on the **existing `Forecast.Opportuni
|
|
|
177
178
|
`NetsuiteOpportunityClickupTask` table in `Client_True`; reviewer Rohan Girish rejected that and mandated
|
|
178
179
|
reusing `Forecast.Opportunities`.)
|
|
179
180
|
|
|
181
|
+
A counter-proposal (ExternalIdentifiers map table) has been raised — see
|
|
182
|
+
`_underscore/architecture/external-identifiers-map-table.md`; unresolved.
|
|
183
|
+
|
|
180
184
|
How the claim is race-safe:
|
|
181
185
|
- **Conditional UPDATE = the atomic claim.** A raw `_Query`:
|
|
182
186
|
`UPDATE Opportunities SET clickupTaskId='<uuid sentinel>' WHERE netsuiteOpportunityInternalId=X AND
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
20
|
+
- **_underscore** (_Underscore) _(framework core)_ — 35 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 30 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
22
|
- **api2** (API) — 12 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
package/package.json
CHANGED