toga-ai 1.0.1074 → 1.0.1076
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 +2 -1
- package/knowledge/2.0/apps/_underscore/features/acl-capabilities.md +84 -0
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +11 -3
- package/knowledge/2.0/apps/_underscore/features/autoincrement-record-numbering.md +37 -24
- package/knowledge/2.0/apps/_underscore/features/record-hard-delete.md +4 -1
- package/knowledge/2.0/apps/_underscore/features/record-meta-endpoint.md +14 -6
- package/knowledge/2.0/apps/api2/features/v2-deadlock-retry.md +3 -3
- package/knowledge/2.0/apps/dbchanges2/architecture.md +16 -1
- package/knowledge/2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md +5 -5
- package/knowledge/2.0/standards/framework-rules.md +5 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-canada/profile.md +2 -1
- package/knowledge/clients/compass-usa/profile.md +2 -1
- package/knowledge/clients/nychh/features/oneuptime-transfer-order-netsuite-push-monitor.md +1 -1
- package/knowledge/clients/nychh/features/transfer-order-notification-emails.md +1 -1
- package/knowledge/clients/prudential/profile.md +2 -2
- package/knowledge/clients/true/profile.md +2 -1
- package/package.json +1 -1
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|-----|---------|
|
|
5
5
|
| [Proposed — git-sourced base+overlay JSON authoring for the Surface layer](architecture/surface-authoring-proposal.md) | Draft proposal (NOT implemented) to move Surface authoring off hand-written SQL onto git-sourced base+overlay JSON; open only for that design rationale. |
|
|
6
6
|
| [_underscore Framework Architecture](architecture.md) | The 2.0 `_underscore` PHP backend framework — DB abstraction, ORM models, routing, ACL, Surface, multi-tenant clusters; open for how any 2.0 app backend works. |
|
|
7
|
+
| [ACL Capabilities (roles = collections of capabilities; dual-read transition; role delete)](features/acl-capabilities.md) | Client-DB ACL layer between roles and permissions (Capabilities, Roles_Capabilities, capabilityId on ACL rows); open before writing any client ACL grant, readin |
|
|
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 |
|
|
8
9
|
| [Address Uniqueness Normalization (unit identifier + 5-digit ZIP comparison)](features/address-uniqueness-normalization.md) | Two reusable normalization primitives for any "one X per physical address" rule; open when comparing addresses for uniqueness. |
|
|
9
10
|
| [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | US address validation via a carrier waterfall (USPS → FedEx → UPS) exposed as a scripted endpoint; open when validating/normalizing addresses. |
|
|
10
11
|
| [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest`, the 2.0 outbound HTTP client — JSON encode/decode, retry, and api-logging traps; open before calling it or reading `Logs.Api`. |
|
|
11
12
|
| [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment names in multiple languages via an `AssortmentTranslations` sidecar table on the metadata-driven translation layer; open when translating asso |
|
|
12
13
|
| [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | Run a slow WRITE asynchronously via the Worker tier so a long request write cannot hold a stale connection and hit "MySQL server has gone away"; open before usi |
|
|
13
|
-
| [Record numbering (
|
|
14
|
+
| [Record numbering (FIELD_AUTOINCREMENT: per-client sequential numbers from RecordNumbers)](features/autoincrement-record-numbering.md) | ORM record numbers (`SA139197`, `AJ100000`, `CL146`) counted in order per client from a `RecordNumbers` table; open when adding a numbered model/prefix, debuggi |
|
|
14
15
|
| [Calculated-column filter options (`<field>FilterOptions` model static → table meta)](features/calculated-column-filter-options.md) | Lets a calculated (`FIELD_SQL`) table column supply its own filter dropdown options via a `<field>FilterOptions` model static; open when adding a data-driven co |
|
|
15
16
|
| [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent |
|
|
16
17
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind Fulfill & Ship: buying/persisting a carrier label (UPS/FedEx) and creating the NetSuite Item Fulfillment; open for label, reprint, retu |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ACL Capabilities (roles = collections of capabilities; dual-read transition; role delete)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-18
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/Capability.php
|
|
13
|
+
- _underscore/Model/Client/Roles/Capability.php
|
|
14
|
+
- _underscore/Model/Client/Role.php
|
|
15
|
+
- _underscore/Model/Client/AclRecordPermission.php
|
|
16
|
+
- _underscore/Model/Client/AclFieldPermission.php
|
|
17
|
+
- _underscore/Model/Client/AclCustomFieldPermission.php
|
|
18
|
+
- api2/Component/Api/V2/V2.php
|
|
19
|
+
- dbchanges2/Client/2026-10-18b - TRUE-82880 Capabilities.sql
|
|
20
|
+
- dbchanges2/Core/2026-10-18e - TRUE-82880 CapabilitiesCore.sql
|
|
21
|
+
related:
|
|
22
|
+
- acl-permission-chain.md
|
|
23
|
+
- record-hard-delete.md
|
|
24
|
+
- ../../api2/features/api-key-authentication-and-scoping.md
|
|
25
|
+
---
|
|
26
|
+
Client-DB ACL layer between roles and permissions (Capabilities, Roles_Capabilities, capabilityId on ACL rows); open before writing any client ACL grant, reading ACL rows in SQL, deleting a role, or dropping `roleId`.
|
|
27
|
+
|
|
28
|
+
## Summary
|
|
29
|
+
|
|
30
|
+
A **Capability** (`Client_*.Capabilities`, Core.Records **409**) is a per-client named set of
|
|
31
|
+
`AclRecordPermissions` (+ their logic groups), `AclFieldPermissions` and `AclCustomFieldPermissions`
|
|
32
|
+
rows, linked by a new `capabilityId` column. A **Role** is a set of capabilities
|
|
33
|
+
(`Roles_Capabilities`, Records **410**). Access = **union** of all capabilities of all the user's
|
|
34
|
+
roles. No per-role or per-user overrides. Authoring UI = Hub (later). Built TRUE-82880.
|
|
35
|
+
|
|
36
|
+
**Not capability-keyed (stay role-keyed):** `AclActionPermissions`, `AclRecordScripts`,
|
|
37
|
+
`AclCustomRecordScripts`, `Users_Roles`, `Apis_Roles`. **Core DB ACL stays role-based** — capabilities
|
|
38
|
+
exist only for `aclDatabase = 'CLIENT'` records.
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
- **JWT unchanged** — it still carries role ids. Per request, `V2::buildLookups()` expands
|
|
43
|
+
role → capability ids (`buildClientRoleIdsByCapabilityId`).
|
|
44
|
+
- **Dual-read (transition):** client ACL rows load with
|
|
45
|
+
`capabilityId IN (<caps>) OR (capabilityId IS NULL AND roleId IN (<roles>))`
|
|
46
|
+
(`buildClientAclDualReadCondition`). So legacy role-only rows still work.
|
|
47
|
+
- **Lookup buckets unchanged:** `[db][roleId][recordId]`, keyed by permission id, so a row reached
|
|
48
|
+
through two roles is not counted twice. `resolveClientAclRowRoleIds` maps a capability row back to
|
|
49
|
+
every role holding that capability.
|
|
50
|
+
- **Field permissions merge writable-wins:** same field from two capabilities → writable if any says writable.
|
|
51
|
+
- **`AclFieldPermissions.appId`:** NULL = all apps; otherwise must equal the request's app.
|
|
52
|
+
- **Migration** (`Client/2026-10-18b`, dynamic, re-runnable): one capability per role, named after
|
|
53
|
+
the role, linked only to that role; existing ACL rows get its `capabilityId`. `roleId` stays filled
|
|
54
|
+
and becomes nullable.
|
|
55
|
+
- **`roleId` drop is later:** its own forward `dbchanges2` file, after a release. Do the gotchas
|
|
56
|
+
below first.
|
|
57
|
+
|
|
58
|
+
### Role delete (`_Model_Client_Role::preDelete`, PRE DELETE interceptor on record 274)
|
|
59
|
+
|
|
60
|
+
- Gated only by `allowDelete` on `roles` ACL. Hard delete. **No special system-role logic.**
|
|
61
|
+
- **Blocked (400)** while any `Users_Roles` or `Apis_Roles` row holds the role.
|
|
62
|
+
- Uses [record hard delete](record-hard-delete.md) `relatedRows()`: deletes `Roles_Capabilities`
|
|
63
|
+
links and role-keyed rows (`AclActionPermissions`, `AclRecordScripts`, `AclCustomRecordScripts`,
|
|
64
|
+
`Announcements_Roles`, `ApprovalTemplateStageAllowedRoles`, `SurfaceOverrides`).
|
|
65
|
+
- Then `removeAclRows()`: legacy rows (`capabilityId IS NULL`) are deleted (record permissions with
|
|
66
|
+
their logic groups first); capability rows are kept with `roleId = NULL`. **Capabilities are never
|
|
67
|
+
deleted with a role.**
|
|
68
|
+
|
|
69
|
+
## Gotchas
|
|
70
|
+
|
|
71
|
+
- **⚠ Before the `roleId` drop: worker2 `Worker/Sync/TrackingNumbers.php` (~L460) still joins
|
|
72
|
+
`AclRecordPermissions` / `AclFieldPermissions` on `roleId`.** Move it to the capability path first.
|
|
73
|
+
Grep every repo for raw-SQL reads of `roleId` on these three tables too.
|
|
74
|
+
- **⚠ New unique keys with NULL `appId` / `indirectRecordId` do not block duplicates** (MySQL treats
|
|
75
|
+
NULLs as distinct). Today the old role-based keys still protect; after the `roleId` drop they do
|
|
76
|
+
not — guard inserts with `NOT EXISTS`.
|
|
77
|
+
- **Writers (Hub, migrations) must write `roleId = NULL` on new capability rows.** A filled `roleId`
|
|
78
|
+
makes the row look legacy-shaped and keeps it alive on the old key.
|
|
79
|
+
- **⚠ Open (security, pre-existing):** the client `Base` role (every user holds it) has full CRUD on
|
|
80
|
+
`user-roles`, `roles`, `acl-field-permissions`, `acl-custom-field-permissions` — a user can grant
|
|
81
|
+
themselves more access. Developer decision: fix when Hub is built.
|
|
82
|
+
|
|
83
|
+
## Change history
|
|
84
|
+
- 2026-10-18 — Created: Capabilities layer, dual-read, writable-wins field merge, role delete rules (TRUE-82880). (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: ["jcardinal", "mhammontree", "tcox", "bala", "apeterson", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -28,6 +28,11 @@ inserting only an `AclRecordPermissions` row is the single most common mistake
|
|
|
28
28
|
silently does nothing without the rest of the chain. A complete grant requires **all four** tables,
|
|
29
29
|
plus `AclFieldPermissions` for the fields to be readable/writable.
|
|
30
30
|
|
|
31
|
+
**Client-DB ACL rows now belong to a Capability** (`capabilityId`), and roles are sets of
|
|
32
|
+
capabilities; legacy `roleId`-only rows still work during the transition (dual-read). Read
|
|
33
|
+
[ACL Capabilities](acl-capabilities.md) before writing or querying client ACL rows. Core DB ACL is
|
|
34
|
+
still role-based.
|
|
35
|
+
|
|
31
36
|
## The complete chain (all four are required)
|
|
32
37
|
|
|
33
38
|
```
|
|
@@ -412,7 +417,8 @@ first (see
|
|
|
412
417
|
- **Two separate role spaces with different ids and names:** Core roles (`Core.Roles`: Public, Integrations, Super User, Base) and per-tenant client roles (`Client_<X>.Roles`). Ids are not comparable across spaces.
|
|
413
418
|
- `AclRecordPermissions.roleId` / `AclFieldPermissions.roleId` are read in the record's `aclDatabase` role space: CORE record → Core role id; CLIENT record → client role id. `roleId=1` is Core Public on a Core record but a client role on a client record — always filter by the record's `aclDatabase`.
|
|
414
419
|
- **Link:** `Client.Roles.coreRoleId`. At login the user's client roles map to Core roles through their distinct non-null `coreRoleId`s, set as `id.core.roles`. CORE records filter on the user's Core roles; CLIENT records on their client roles.
|
|
415
|
-
- **No inheritance:** no parent role / hierarchy; roles are used verbatim by the resolver.
|
|
420
|
+
- **No inheritance:** no parent role / hierarchy; roles are used verbatim by the resolver. Client roles
|
|
421
|
+
reach client ACL rows through their capabilities (union) — see [ACL Capabilities](acl-capabilities.md).
|
|
416
422
|
- **Public = read-only baseline every authenticated user inherits.** Public is MERGED (additively) into the effective core roles at token build, `processRoutePairs`, and `buildLookups`; without the merge the mapped `coreRoleId`s overwrite it and a role like Core Base cannot read Core metadata (e.g. `Core.Clients.uuid`). Keep Public read-only: never grant it write on Core metadata.
|
|
417
423
|
- Related: no backend ACL cache in 2.0 — a stale 403 is the UI cache or ACL logic, never a backend cache.
|
|
418
424
|
|
|
@@ -925,11 +931,13 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
|
|
|
925
931
|
NULL plus `sqlExpression='1'` means no row scoping whatsoever (see the section above).
|
|
926
932
|
- [ ] **No business-critical column is `isWritable = 1` for `Base`** unless customers are genuinely
|
|
927
933
|
meant to write it via plain CRUD.
|
|
928
|
-
- [ ] For `aclDatabase = 'CLIENT'` records: rows go in client DB(s); resolve `roleId` by subselect
|
|
934
|
+
- [ ] For `aclDatabase = 'CLIENT'` records: rows go in client DB(s); resolve `roleId` by subselect;
|
|
935
|
+
set `capabilityId` (see [ACL Capabilities](acl-capabilities.md)).
|
|
929
936
|
- [ ] The field is declared on the **generated** `_underscore/Model/Client/<Name>.php` (else `EO-1`),
|
|
930
937
|
and every repo is on the **same branch** so the generated model matches the DB.
|
|
931
938
|
|
|
932
939
|
## Change history
|
|
940
|
+
- 2026-10-18 — Client ACL rows now carry `capabilityId`; linked the new Capabilities doc (TRUE-82880). (jcardinal)
|
|
933
941
|
- 2026-09-21 — Named the mechanism behind the half-built chain: `_Model_Core_AclRecordPermission::buildSqlExpression`
|
|
934
942
|
returns **`'(0)'`** with no logic group, so `allowRead = 1` alone denies everything (second symptom: 403 listing
|
|
935
943
|
`aclRecordPermissionIds` with `[null, null, null]`). Added that a `CLIENT` record's grant must exist in every
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "Record numbering (
|
|
2
|
+
title: "Record numbering (FIELD_AUTOINCREMENT: per-client sequential numbers from RecordNumbers)"
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: _underscore
|
|
5
5
|
project: _Underscore
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: ["bala", "rgirish", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model.php
|
|
13
13
|
- _underscore/Database.php
|
|
14
14
|
- _underscore/Model/Client/SalesOrder.php
|
|
15
|
-
- _underscore/Model/Client/ServiceRequest.php
|
|
16
|
-
- _underscore/Model/Client/TransferOrder.php
|
|
17
15
|
- _underscore/Model/Compass/Canada/SalesOrder.php
|
|
18
16
|
- _underscore/Model/Core/Client.php
|
|
19
|
-
- dbchanges2/Core/2026-10-
|
|
17
|
+
- dbchanges2/Core/2026-10-18a - RecordNumbers.sql
|
|
18
|
+
- dbchanges2/Client/2026-10-18a - RecordNumbers.sql
|
|
19
|
+
- dbchanges2/Client_CompassCanada/2026-10-18a - RecordNumbers SAC prefix.sql
|
|
20
20
|
related:
|
|
21
21
|
- ../architecture.md
|
|
22
22
|
- ../../dbchanges2/architecture.md
|
|
@@ -25,39 +25,51 @@ related:
|
|
|
25
25
|
- ../../worker2/features/netsuite-salesorder-outbound-push.md
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
ORM record numbers (`
|
|
28
|
+
ORM record numbers (`SA139197`, `AJ100000`, `CL146`) counted in order per client from a `RecordNumbers` table; open when adding a numbered model/prefix, debugging a 1062 or missing-row error on `number`, or seeding a new client.
|
|
29
29
|
|
|
30
30
|
## Summary
|
|
31
31
|
|
|
32
|
-
A model declares `public $number =
|
|
32
|
+
A model declares `public $number = self::FIELD_AUTOINCREMENT;` — nothing else. Prefix and next number live in a `RecordNumbers` row (data, not model options). Numbers are per client: the same number (e.g. `TK100005`) can exist for two clients. TRUE-82715.
|
|
33
|
+
|
|
34
|
+
Removed: `FIELDOPT_AUTOINCREMENT_PREFIX` / `_FIRSTNUMBER` / `_SUFFIX`, the `MAX()+1` path, and the per-tenant `Parameters` counter (PR `_underscore#756` — never merge). The 2026-10-13 platform-wide scrambled design (`Core.Records.numberPrefix/nextNumber`, multiplier) was dropped before release. App repo `_underscore/CLAUDE.md` (~line 227) still lists the removed options — stale.
|
|
33
35
|
|
|
34
36
|
## How it works
|
|
35
37
|
|
|
36
|
-
1. **
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
1. **Table `RecordNumbers`** (`id, uuid, dtCreated, dtUpdated, recordId INT UNSIGNED UNIQUE, prefix VARCHAR(8) UNIQUE, nextNumber INT UNSIGNED`). `recordId` = `Core.Records.id`; `nextNumber` = next number to issue.
|
|
39
|
+
- One in **every client DB** — no FK (Core is another cluster).
|
|
40
|
+
- One in **Core** — real FK to `Records` — for Core-based records (`Core.Clients`, recordId 51, `CL`).
|
|
41
|
+
2. **Which table:** from the model's `const DATABASE`: `DB_CORE` → `Core.RecordNumbers`; `DB_CLIENT` → that client's; anything else throws.
|
|
42
|
+
3. **Which row:** walk class + parents against `Core.Records.model` (stored with leading backslash), cached per class. Client overrides (`_Model_Compass_Canada_SalesOrder`) use the base row. No row → plain `Exception` (fail loud, no self-seeding).
|
|
43
|
+
4. **Allocate:** `UPDATE RecordNumbers SET nextNumber = LAST_INSERT_ID(nextNumber + 1) WHERE recordId = X` on a separate sequence connection, committed at once (`_Database::DB_CORE_SEQUENCE` `'CoreSequence'` / `DB_CLIENT_SEQUENCE` `'ClientSequence'`). Number = insertId − 1; prefix read on the same connection.
|
|
44
|
+
5. **Sequence connections** follow their source alias on `register()`; dropped/closed when the source is re-pointed or re-registered with other settings (a stale ClientSequence never uses another tenant's counter). `connect()` falls back to the source DB when Core/Client is not registered by alias (worker2 prod).
|
|
45
|
+
6. **Format:** `prefix . nextNumber` — counting in order, no padding, no hyphen. Prefix validated in PHP: 1–8 letters A–Z except O, I, B (look like 0, 1, 8); L allowed. No CHECK constraint ([dbchanges2 rule 15](../../dbchanges2/architecture.md)).
|
|
46
|
+
7. **Hand-set number wins** (counter untouched). `forSearching` with null number skips the field. 1062 retry up to 5, only when the dup is the number this save generated.
|
|
47
|
+
|
|
48
|
+
**Prefixes:** client DBs SA(14), PU(17), FF(28), SR(35), RC(78), TK(99), AJ(154), ET(191), CS(222), TR(312); Core CL(51). Per-client override allowed (Compass Canada sales orders = `SAC`).
|
|
49
|
+
|
|
50
|
+
**Seeds (continue each client's old series under the new prefix):**
|
|
51
|
+
- `Client/` file: `nextNumber = GREATEST(100000, max(old-prefix number)+1)`. Old → new: SA→SA, P→PU, F→FF, SR→SR, IR→RC, TK→TK, IA→AJ, ET→ET, CA→CS, TA→TR (TA was a likely mistake). Old numbers matched with `REGEXP_LIKE(number, '^OLD[0-9]+$', 'c')` — case-sensitive; skips NetSuite numbers and free text like `REPL SA123681`.
|
|
52
|
+
- `Core/` file: `Clients.number VARCHAR(16) NOT NULL UNIQUE`, backfilled CL100, CL101… in id order; CL `nextNumber` = 100 + client count; RecordFields 2740 = `clients.number`.
|
|
53
|
+
- `Client_CompassCanada/` file (runs after `Client/`): sales orders `SAC`, continuing after the highest SAC number.
|
|
42
54
|
|
|
43
|
-
**
|
|
55
|
+
**Why per-client and in order (jcardinal):** clients spot gaps in their own series (Compass USA); clients want own prefixes (Compass Canada); bigger = newer. Desk shows the client column next to the number — number display unchanged.
|
|
44
56
|
|
|
45
|
-
**
|
|
57
|
+
**Turned down:** platform-wide counter (every client sees constant gaps; Core write per create); scrambled / multiplier numbers (confuse the business; little security gain — uuid is the API id, ACL protects records); check digit; Crockford base32; client id or date in number; million bands; plain counter with no prefix.
|
|
46
58
|
|
|
47
59
|
## Gotchas
|
|
48
60
|
|
|
49
|
-
-
|
|
50
|
-
- **New
|
|
51
|
-
- **
|
|
61
|
+
- **New numbered model needs a `RecordNumbers` row in every client DB** (or Core for `DB_CORE` models), else creates throw. Scopes: no `Core.Records` row yet; `Model/Client/Scope.php` exists only on Desk branch TRUE-82615 — on merge make it plain `FIELD_AUTOINCREMENT` + seed a row.
|
|
62
|
+
- **New client DB** must get `RecordNumbers` rows from the `Client/` blank seed.
|
|
63
|
+
- **Gaps only come from failed saves.** A gap = a failed create → check `Logs.Api` / `Logs.Issue`.
|
|
52
64
|
- **SA / TR numbers are placeholders** — NetSuite `tranId` overwrites them on write-back (accepted). Never key a match on `number`; pushes key `externalId` on uuid.
|
|
53
|
-
- **Old `MAX()+1` race (why this exists):** concurrent creates got one number → 1062 → HTTP 400 `EV-10`, record lost (Prudential SR, TRUE-82209).
|
|
54
|
-
- **
|
|
55
|
-
- **Reproducing a race needs distinct rows per call** (different contact + serial) and a gate-file release ([V2 request retry](../../api2/features/v2-deadlock-retry.md)); sandbox-dev `max_connections = 90` →
|
|
65
|
+
- **Old `MAX()+1` race (why this exists):** concurrent creates got one number → 1062 → HTTP 400 `EV-10`, record lost (Prudential SR, TRUE-82209).
|
|
66
|
+
- **Core seed fails on a local stray `RecordFields` row** for `clients.number` (one local had id 2598, not in prod/dev-sandbox/repo) → guarded insert of 2740 hits unique `recordId_field`. Delete the stray row locally; the file fails loud on drift on purpose.
|
|
67
|
+
- **Reproducing a race needs distinct rows per call** (different contact + serial) and a gate-file release ([V2 request retry](../../api2/features/v2-deadlock-retry.md)); sandbox-dev `max_connections = 90` → bursts ~10.
|
|
56
68
|
- **Run `php -l Model.php Database.php` after merge conflicts on env branches** — a past merge dropped `_Database::$_aliases` → beta outage.
|
|
57
69
|
|
|
58
70
|
## Verification
|
|
59
71
|
|
|
60
|
-
2026-10-
|
|
72
|
+
2026-10-18 local, real runs: seeds correct for Client_True / Compass / Elite (Compass SA continues SA139197; Canada SAC after SAC100923); single create AJ100000; 10 concurrent creates distinct + consecutive, no 1062; hand-set kept; Elite→True switch in one process used each client's own counter; Core client got CL146.
|
|
61
73
|
|
|
62
74
|
## Related
|
|
63
75
|
|
|
@@ -67,6 +79,7 @@ A model declares `public $number = [self::FIELD_AUTOINCREMENT];` — nothing els
|
|
|
67
79
|
- [NetSuite SalesOrder outbound push](../../worker2/features/netsuite-salesorder-outbound-push.md)
|
|
68
80
|
|
|
69
81
|
## Change history
|
|
70
|
-
- 2026-10-
|
|
71
|
-
- 2026-10-
|
|
82
|
+
- 2026-10-18 — Final design: per-client `RecordNumbers` table (client DBs + Core), counting in order, old series continued under new prefixes, Compass Canada SAC; dropped the platform-wide scrambled `Core.Records` counter. TRUE-82715. (jcardinal)
|
|
83
|
+
- 2026-10-13 — Platform-wide scrambled counter in `Core.Records` (dropped before release). (jcardinal)
|
|
84
|
+
- 2026-10-02 — Per-tenant Parameters counter on `_beta` (superseded, PR 756 not merged). (rgirish)
|
|
72
85
|
- 2026-09-02 — `TA` added to shared TransferOrder (now TR). (bala)
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/RecordDelete/RecordDelete.php
|
|
@@ -53,6 +53,8 @@ Tickets, Cases, ServiceRequests, Customers, Contacts. The class DocBlock is the
|
|
|
53
53
|
ContactNotes, ContactAttempts, ContactMessagingOptIns, Campaigns_Contacts, Campaigns_MeetWithContacts,
|
|
54
54
|
Contacts_Tags, Contacts_Vendors, Contacts_Locations, Locations_Contacts, ContactAddresses / EmailAddresses /
|
|
55
55
|
PhoneNumbers. `preDelete` clears the contact's `primary*` pointers first (they and the children point at each other).
|
|
56
|
+
- **Role rule (Records 274):** blocked by Users_Roles / Apis_Roles; ACL rows handled after `purge()` — full rule in
|
|
57
|
+
[ACL Capabilities § Role delete](acl-capabilities.md).
|
|
56
58
|
|
|
57
59
|
## Gotchas
|
|
58
60
|
|
|
@@ -71,5 +73,6 @@ Tickets, Cases, ServiceRequests, Customers, Contacts. The class DocBlock is the
|
|
|
71
73
|
losing the link would lose meaning (orders, required FKs like `Contacts.customerId`).
|
|
72
74
|
|
|
73
75
|
## Change history
|
|
76
|
+
- 2026-10-18 — Roles opted in (TRUE-82880). (jcardinal)
|
|
74
77
|
- 2026-10-17 — Contacts opted in (block / detach / cascade map, schema-drift probes); Contacts_Files added to FILE_REFERENCE_TABLES (TRUE-82871). (jcardinal)
|
|
75
78
|
- 2026-10-17 — Created; Customers opted in, Customers_Files added to FILE_REFERENCE_TABLES (TRUE-82870). (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Core/Record.php
|
|
@@ -22,15 +22,22 @@ Per-route record meta (names, label/sublabel field, per-field display labels); o
|
|
|
22
22
|
## Summary
|
|
23
23
|
|
|
24
24
|
`GET /v2/records/meta?route=<route>` → `_Model_Core_Record::meta()`. Returns
|
|
25
|
-
`{route, name, pluralName, labelField, sublabelField, identifierField, fieldLabels}`.
|
|
25
|
+
`{route, name, pluralName, icon, iconWeight, labelField, sublabelField, identifierField, fieldLabels}`.
|
|
26
|
+
Batch: `?routes=a,b,c` (max `META_BATCH_MAX_ROUTES` = 400; callers chunk) →
|
|
27
|
+
`{records: {<route>: {route, name, pluralName, icon, iconWeight}}}` — light shape, no field labels.
|
|
28
|
+
Record-type icons come only from here (`Core.Records.iconToken` / `iconWeightToken`); apps never
|
|
29
|
+
hardcode them (rule: dbchanges2 architecture → *`Core.Records.name` + icons*).
|
|
26
30
|
`fieldLabels` = `{<field>: "<label>"}` — the data-driven visible label of each field, in the session
|
|
27
31
|
language. Platform rule: **a field's visible label comes from `Core.RecordFields.labelMessageId` via
|
|
28
32
|
`fieldLabels`, never hardcoded** (platform apps' `PLATFORM.md` rule 15).
|
|
29
33
|
|
|
30
34
|
## How it works
|
|
31
35
|
|
|
32
|
-
- **Names:** `
|
|
33
|
-
`
|
|
36
|
+
- **Names:** `name` / `pluralName` from Messages via `_Model_Core_Surface::resolveMessageValues()`;
|
|
37
|
+
fallback order: Message → frozen `DefaultRecordSettings` → `Core.Records.name`.
|
|
38
|
+
- **Batch (`metaForRoutes`):** one Records query, one Messages lookup, one `DefaultRecordSettings` query for all routes.
|
|
39
|
+
- **Response path:** record-script nesting adds the payload key — batch is read at
|
|
40
|
+
`data.records.meta.records.<route>` (see [record scripts](../../api2/features/record-scripts.md)).
|
|
34
41
|
- **`fieldLabels`:** `resolveFieldLabels()`:
|
|
35
42
|
1. Caller must have record READ (`aclRecordFlags`), else `{}`.
|
|
36
43
|
2. Only fields the caller may READ (`$api->getAclFieldPermissions`, same resolver as
|
|
@@ -47,11 +54,12 @@ language. Platform rule: **a field's visible label comes from `Core.RecordFields
|
|
|
47
54
|
## Gotchas
|
|
48
55
|
|
|
49
56
|
- **⚠ Open (security): the base meta has NO record READ check.** `route`, `name`, `pluralName`,
|
|
50
|
-
`labelField`, `sublabelField` come back for any route to any caller (pre-existing
|
|
51
|
-
`fieldLabels` is gated. A caller can probe which routes exist (unknown route → 400
|
|
57
|
+
`labelField`, `sublabelField`, `icon` come back for any route to any caller (pre-existing; the
|
|
58
|
+
batch form too); only `fieldLabels` is gated. A caller can probe which routes exist (unknown route → 400
|
|
52
59
|
`No record found for route`). Not fixed yet — Desk question `lx`.
|
|
53
60
|
- `fieldLabels` is **per caller** (field ACL + language) — never cache it across users or tenants.
|
|
54
61
|
The rest of the meta is the same for every tenant.
|
|
55
62
|
|
|
56
63
|
## Change history
|
|
64
|
+
- 2026-10-18 — Added `icon` / `iconWeight`, name fallback to `Core.Records.name`, and batch `?routes=` (max 400) (TRUE-82880). (jcardinal)
|
|
57
65
|
- 2026-10-15 — Added `fieldLabels` (gated by record + field READ) and blox `fieldLabelOf`; noted the ungated base meta (TRUE-82836). (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: [jcardinal, bala, ajean, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Controller/Index.php
|
|
@@ -34,7 +34,7 @@ api2's front controller ("**Prong A**") can **detect a retriable concurrency fai
|
|
|
34
34
|
|
|
35
35
|
The replay is **route-scoped by an allowlist** (`DEADLOCK_RETRIABLE_ROUTES` — currently `['/v2/advance-shipping-notices', '/v2/service-requests']`) — because whole-request in-process replay is **only safe for routes with no non-rollbackable external side effect**. The retry gate is now **`wasDeadlock() || wasRetriableUniqueRace()`**.
|
|
36
36
|
|
|
37
|
-
> **Status (2026-10-
|
|
37
|
+
> **Status (2026-10-18).** The SR number race is fixed at the source — a per-client `RecordNumbers` counter in `_Model` (TRUE-82715; replaces the unmerged `Parameters` counter of TRUE-82209 / PR 756), see [FIELD_AUTOINCREMENT record numbering](../../_underscore/features/autoincrement-record-numbering.md). The `/v2/service-requests` replay below (api2 `_sandbox-dev` only; `_beta`/`_production` list ASN only) is no longer the fix for numbers. **Extending replay to every numbered route was rejected**: Adyen / Elite / Growrk `postPost` hooks have external side effects. Prod still has neither fix.
|
|
38
38
|
|
|
39
39
|
**Constants (verified 2026-10-02):** `MAX_DEADLOCK_ATTEMPTS = 5`, backoff 20ms-120ms widening per attempt. `DEADLOCK_RETRIABLE_ROUTES` = `['/v2/advance-shipping-notices', '/v2/service-requests']` on api2 `_sandbox-dev`; ASN only on `_beta` and `_production`.
|
|
40
40
|
|
|
@@ -82,7 +82,7 @@ Two consequences of the typed exception worth knowing:
|
|
|
82
82
|
**Symptom:** `/v2/service-requests` returned `400 EV-10` *"Duplicate entry 'SR131023' for key 'ServiceRequests.number'"* — 21 consecutive failures, all ~1.8s with no backoff visible. ServiceNow was **not** sending duplicates. The retry existed and was correctly wired; it just never fired.
|
|
83
83
|
|
|
84
84
|
**The root-cause chain, in order:**
|
|
85
|
-
1. **`ServiceRequests.number` is not a MySQL auto-increment.** The old `_Model` `FIELD_AUTOINCREMENT` did `SELECT MAX(number)+1` then an `INSERT`, **with no lock between them** (since replaced by
|
|
85
|
+
1. **`ServiceRequests.number` is not a MySQL auto-increment.** The old `_Model` `FIELD_AUTOINCREMENT` did `SELECT MAX(number)+1` then an `INSERT`, **with no lock between them** (since replaced by the `RecordNumbers` counter, TRUE-82715) (see [FIELD_AUTOINCREMENT record numbering](../../_underscore/features/autoincrement-record-numbering.md)). Two concurrent POSTs read the same MAX, pick the same number, and the loser gets MySQL 1062.
|
|
86
86
|
2. **The retry was already built for exactly this.** `_underscore/Model.php` set `_Database::$retriableUniqueRace` on that 1062 (removed by TRUE-82209 — no branch's `Model.php` sets it now), and `api2/Controller/Index.php` replays the whole request when `_Database::wasRetriableUniqueRace()` is true. That helper requires **both** the flag **and** `$lastErrorNumber === 1062`.
|
|
87
87
|
3. **The logger destroyed the second half of that condition.** `_Component_Api_V2::saveApiLogUnderItsOwnTransactionId()` runs **between the failed INSERT and the retry check**, inside `execute()`. Its resend loop **deliberately causes its own 1062s** — it renames `transactionId` on collision and retries (see [V2 request logging](./request-logging.md)). Those bookkeeping 1062s **overwrote the caller's `$lastErrorNumber`**, so `wasRetriableUniqueRace()` returned false and the replay never ran.
|
|
88
88
|
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: [jcardinal, mhammontree, bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- Core/
|
|
@@ -141,6 +141,19 @@ These **two tables, and only these two**, have **team-maintained primary keys**.
|
|
|
141
141
|
|
|
142
142
|
This is what makes the *Database isolation* rule practical — it is the reason a `Client_<Tenant>` migration never needs to read `Core` to find a `recordFieldId`.
|
|
143
143
|
|
|
144
|
+
### `Core.Records.name` + icons (team rule 2026-10-18, TRUE-82880)
|
|
145
|
+
|
|
146
|
+
- `name` is REQUIRED (NOT NULL), written for a person: Title Case, PLURAL, built from the route. `sales-orders` → "Sales Orders". Acronyms stay upper case: "ACL Record Permissions".
|
|
147
|
+
- Bridge record = BOTH sides are real tables/records. Both sides plural: "Locations <-> Location Attributes", "Users <-> Roles", "Users <-> Files".
|
|
148
|
+
- Child table whose second part is not its own table is NOT a bridge: one plural name ("Location Hours", not "Location <-> Hours"); table `LocationHours`, not `Locations_Hours`.
|
|
149
|
+
- Typed variants: type first: `support-methods-chat` → "Chat Support Methods".
|
|
150
|
+
- Product / feature names keep their form ("Meet With Contacts"). Uncountable words stay singular ("User Memory", "Conversation History", "Item Media"). Acronyms upper case ("AI Models").
|
|
151
|
+
- Routes match the table words: `service-request-note-types`, not `service-request-notes-types`.
|
|
152
|
+
- Change history: 2026-10-18 TRUE-82880 refined bridge/child/typed/uncountable rules.
|
|
153
|
+
- Every new `Core.Records` insert sets `name` this way.
|
|
154
|
+
- Two layers: `Core.Records.name` = base name, the fallback everywhere; `nameMessageId` / `pluralNameMessageId` Messages = translatable UI text, wins when set.
|
|
155
|
+
- Record-type icons live in `Core.Records.iconToken` (+ `iconWeightToken`): one icon per record platform-wide; the nav icon is the source when choosing. Apps read it from the records meta, never hardcode record icons. Stored as camelCase Font Awesome names without the `fa-` prefix (blox `toFaClass` normalizes).
|
|
156
|
+
|
|
144
157
|
### Inserting into them — ASK FIRST, never rely on AUTO_INCREMENT
|
|
145
158
|
|
|
146
159
|
**When a change inserts into `Core.Records` or `Core.RecordFields`, stop and ask the developer for the next `id` value(s)**, then write them as explicit literals:
|
|
@@ -164,6 +177,7 @@ The rule above keeps *existing* ids aligned, but each environment's `Core` has d
|
|
|
164
177
|
|
|
165
178
|
- **Never subselect for a Core id from a `Client_*` file** — `Core` and `Client_*` are on separate clusters, so it cannot resolve there; write the literal.
|
|
166
179
|
- When a change inserts a **new** `Core.RecordFields` row *and* client-side rows that reference it, expect the reference to need a **per-environment value**, and say so in a header comment. A single file that "works everywhere" is only safe when every id it touches is long-standing Core data (as in `Client_Elite/2026-08-17 - FulfillmentTableViews.sql`, which needed no split).
|
|
180
|
+
- **A dev's local `Core` can hold stray `RecordFields` rows** not in prod/dev-sandbox/repo (TRUE-82715: local had `clients.number` as 2598) → a guarded insert of the central id hits unique `recordId_field`. Correct: the file fails loud. Fix by deleting the stray row locally — never change the central id.
|
|
167
181
|
- This qualifies the claim in `2.0/apps/_underscore/features/acl-permission-chain.md` that these ids are "identical in every environment": true for established rows, **not** for a row you are creating now.
|
|
168
182
|
|
|
169
183
|
### Referencing them — hardcode the `id`, including foreign keys
|
|
@@ -400,6 +414,7 @@ The extra `SELECT … FROM ( … ) existing` layer both dodges error 1093 and pi
|
|
|
400
414
|
|
|
401
415
|
## Change history
|
|
402
416
|
|
|
417
|
+
- 2026-10-18 — Core.Records.name required + human naming; record icons in Core.Records (TRUE-82880). (jcardinal)
|
|
403
418
|
- 2026-10-17 — /finish reports central ids used + next free (TRUE-82889). (jcardinal)
|
|
404
419
|
- 2026-10-17 — Second same-folder file also allowed for interceptor rows that must run after the code deploy (TRUE-82889). (jcardinal)
|
|
405
420
|
- 2026-10-13 — Added rule 15: no MySQL `CHECK` constraints (TRUE-82715). (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: ["bala", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite/SalesOrder.php
|
|
@@ -136,16 +136,16 @@ At depth 4 the address comes back **missing with no error** — the max-depth om
|
|
|
136
136
|
|
|
137
137
|
Same mechanism as the sales-order push (`created` / `adopted` / `recovered` / `dryRun`), different prefix. **Keyed on the uuid, deliberately:**
|
|
138
138
|
|
|
139
|
-
1. NetSuite account **1095849 is shared by every client** and rejects a duplicate `externalId` **account-wide**, not per customer. `number` is unique only inside one client database — and
|
|
139
|
+
1. NetSuite account **1095849 is shared by every client** and rejects a duplicate `externalId` **account-wide**, not per customer. `number` is unique only inside one client database — and record numbers are counted per client (`TR`, was `TA`), so **two clients can hold the same number**.
|
|
140
140
|
2. **`number` changes.** The write-back replaces `TA100000` with NetSuite's number ~12s later, so an external id built from `number` would stop matching, the duplicate check would miss, and a second NetSuite order would be created.
|
|
141
141
|
|
|
142
142
|
The uuid is the only value on the row unique everywhere and never moving. `externalId` is a **NetSuite-only** field, written nowhere in the Toga DB, and is **not** the internal id — NetSuite assigns `internalId` itself and the bare number lands in `c_netsuiteInternalSalesOrderId`.
|
|
143
143
|
|
|
144
144
|
> **⚠ The format is frozen once orders carry it.** Changing the `toga-to-` prefix makes the adopt/recover path miss existing transfers and create duplicates.
|
|
145
145
|
|
|
146
|
-
### `TransferOrders.number` is a PLACEHOLDER (`
|
|
146
|
+
### `TransferOrders.number` is a PLACEHOLDER (`TR…`, was `TA…`), replaced by NetSuite's
|
|
147
147
|
|
|
148
|
-
`number` is
|
|
148
|
+
`number` is `FIELD_AUTOINCREMENT` on the **shared** client model, so every client's transfer orders get it. Prefix `TR` since TRUE-82715 (2026-10-18; was `TA`), continuing each client's old TA series, mirroring `SA` on sales orders. Write-back sets `number` to NetSuite's `tranId` immediately after the create, exactly as `SA` numbers behave. Mechanics + the reverted "keep TA forever" attempt: [autoincrement record numbering](../../_underscore/features/autoincrement-record-numbering.md). Short version: **the generator only fires when `number` arrives NULL**, so the NetSuite importer (which sends `number = $nsOrder->tranId`) is unaffected — and the **Create Transfer Order modal must stop sending `number`** or the TA number is never generated.
|
|
149
149
|
|
|
150
150
|
### The trigger — `postPost` on the SHARED `_Model_Client_TransferOrder`
|
|
151
151
|
|
|
@@ -266,7 +266,7 @@ When the payload omits the ship-to address the create **succeeds**, so it looks
|
|
|
266
266
|
|
|
267
267
|
- **The interceptor row is authored `isActive = 0` per the standard but shipped `isActive = 1`.** `_underscore` **must** be deployed before that migration runs, or every transfer-order POST 500s with *"Call to undefined method"* — the dispatch is unguarded (see [API payload interceptors](../../api2/features/api-payload-interceptors.md), and the Elite production incident for what that looks like live).
|
|
268
268
|
- **The worker's transfer-order read must stay at depth 6** — shallower silently loses the PO number (aborts the push) and the ship-to address. Do not "optimise" the depth.
|
|
269
|
-
- **
|
|
269
|
+
- **Placeholder numbers were reused under the old `MAX()+1` generator** after write-back replaced them (TA100001 was TO 1970, later TO 1975). The `RecordNumbers` counter (TRUE-82715) no longer reuses them. Only unsent orders keep a placeholder; never key on it.
|
|
270
270
|
- **This action does not retry itself**, exactly like the sales-order push: a throw records `isSuccess = 0` and stops. An event-triggered push fires once.
|
|
271
271
|
- **A NetSuite HTTP 400 naming a tax code is a CUSTOMER data problem, not a payload problem.** Check the customer's `taxitem` first; do not pin the tax fields in the payload (ignored) and do not drop the address to make the error go away (silently ships to the wrong site).
|
|
272
272
|
- **`git diff --stat` after any programmatic full-file write to `_underscore`.** A full-file write with an editor default flipped CRLF → LF on `_underscore/Model/Nychh/TransferOrder.php` — content identical, git reported it modified with no content hunks. Restored with `git checkout --`. Standing rule: [2.0 backend PHP standards](../../standards/backend-php.md).
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-10-
|
|
8
|
+
updated: 2026-10-18
|
|
9
9
|
owners: [jcardinal, ajean, apeterson, mhammontree]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -171,6 +171,10 @@ literal rather than a lookup — including in SQL embedded in PHP in `worker2` /
|
|
|
171
171
|
`_underscore`. Always add a comment naming the record/field the number refers to; a bare `4188` is
|
|
172
172
|
unverifiable later.
|
|
173
173
|
|
|
174
|
+
**Naming + icons.** `Core.Records.name` is required: Title Case, plural, from the route
|
|
175
|
+
("Sales Orders"; bridge records, both sides plural: "Users <-> Roles"; child tables are not bridges: "Location Hours"). Record icons live in `Core.Records.iconToken`
|
|
176
|
+
(+ `iconWeightToken`), never hardcoded in apps. Rule + examples: dbchanges2 architecture, same section.
|
|
177
|
+
|
|
174
178
|
This is also what makes the `dbchanges2` isolation rule above workable: a `Client_<Tenant>` change
|
|
175
179
|
never has to read `Core` to resolve a `recordFieldId`. Full detail and examples:
|
|
176
180
|
`2.0/apps/dbchanges2/architecture.md` → *`Core.Records` / `Core.RecordFields`*.
|
package/knowledge/INDEX.md
CHANGED
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
|
|
22
22
|
## 2.0 framework
|
|
23
23
|
|
|
24
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
24
|
+
- **_underscore** (_Underscore) _(framework core)_ — 105 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
25
25
|
- **worker2** (Worker) — 90 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
26
26
|
- **api2** (API) — 27 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
27
27
|
- **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -19,7 +19,7 @@ project: _Underscore
|
|
|
19
19
|
client: compass-canada
|
|
20
20
|
type: profile
|
|
21
21
|
status: active
|
|
22
|
-
updated: 2026-10-
|
|
22
|
+
updated: 2026-10-18
|
|
23
23
|
owners: [jcardinal, bala, tcox, apeterson, ajean, mhammontree, rgirish]
|
|
24
24
|
files: []
|
|
25
25
|
related:
|
|
@@ -166,6 +166,7 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
166
166
|
`languageSwitcher.isEnabled = true` (`fieldsConfig/clientFields/COMPASSCANADA.json`).
|
|
167
167
|
|
|
168
168
|
## Notes
|
|
169
|
+
- **Sales order numbers use prefix `SAC`, not `SA` (TRUE-82715, 2026-10-18).** Override row in `Client_CompassCanada.RecordNumbers` (`dbchanges2/Client_CompassCanada/2026-10-18a - RecordNumbers SAC prefix.sql`, runs after the `Client/` file); continues after SAC100923. [Record numbering](../../2.0/apps/_underscore/features/autoincrement-record-numbering.md).
|
|
169
170
|
- **⚠ Canada managers got NO approval reminders 2026-08-04 → TRUE-82868 fix (2026-10-16, not yet deployed)** — the reminder cron crashed every run on an ambiguous `firstName`. Same 30-day reminder/cancel split as USA: [Approval reminder & auto-cancel crons](../../1.0/apps/worker/features/compass-manager-approval-reminder-emails.md).
|
|
170
171
|
- **VIP support techs come from the shared Compass VIP file.** One quarterly file covers both portals; Canada VIPs get the Canada tech in `Client_CompassCanada.Users.c_supportedByUserId`. See [VIP Support Import](../../2.0/apps/worker2/features/compass-vip-support-importer.md).
|
|
171
172
|
- **Item modal "Restrict to Persona" needed a Surface opt-in that was never written (2026-09-04).**
|
|
@@ -23,7 +23,7 @@ project: _Underscore
|
|
|
23
23
|
client: compass-usa
|
|
24
24
|
type: profile
|
|
25
25
|
status: active
|
|
26
|
-
updated: 2026-10-
|
|
26
|
+
updated: 2026-10-18
|
|
27
27
|
owners: [jcardinal, bala, tcox, apeterson, dfranks, akhokhani, ajean, rgirish]
|
|
28
28
|
files: []
|
|
29
29
|
related:
|
|
@@ -233,6 +233,7 @@ separate, related client (see its own profile).
|
|
|
233
233
|
the ⚠ rule that **deactivation is raw bulk SQL, so no model hook ever fires on it**.
|
|
234
234
|
|
|
235
235
|
## Notes
|
|
236
|
+
- **Record numbers are Compass's own series, counted in order (TRUE-82715, 2026-10-18).** Compass checks its own order numbers for gaps; SA continues after SA139197. A gap = a failed create (check `Logs.Api` / `Logs.Issue`). [Record numbering](../../2.0/apps/_underscore/features/autoincrement-record-numbering.md).
|
|
236
237
|
- **⚠ Approval auto-cancel never fired for 926 legacy-migrated orders (TRUE-82868, 2026-10-16, not yet deployed).** Their admin approval has `dtDecision` NULL; the crons now age from `COALESCE(dtDecision, SalesOrders.dtCreated)`, reminders stop at 30 days, cancel starts after. [Approval reminder & auto-cancel crons](../../1.0/apps/worker/features/compass-manager-approval-reminder-emails.md).
|
|
237
238
|
- **⚠ SALES_ORDERS froze 2026-10-02 → 10-05 (Issue 940): closed-order delete matched the Office Depot twin SO** (dual-SO trap). Fixed in library with the per-client scope; open: higher-tier mirror fulfillments 91650/91651. Details: [closed-order rule](../../1.0/apps/library/features/netsuite-closed-order-rule.md).
|
|
238
239
|
- **⚠ SALES_ORDERS froze 2026-09-29 on ACL gaps the closed-order delete hit** (transfer-orders 312/313, entitlement-sales-orders 257, sales-order-payments 254). Fixed by `dbchanges2/Client_Compass/2026-09-29a - TransferOrderAclExpressions.sql`, then mode reset to `864000-IDLE`. Details: [closed-order rule](../../1.0/apps/library/features/netsuite-closed-order-rule.md).
|
|
@@ -36,7 +36,7 @@ OneUptime: Incoming Request monitor, DEGRADED status id `8a160361-0b6a-4cbb-ad10
|
|
|
36
36
|
|
|
37
37
|
## Gotchas
|
|
38
38
|
- **When it fires:** the order was never queued, or its push failed. Check `Core.WorkerJobs` for the action + uuid; no row = never queued (look in `Logs.Issue` for the `captureException` from `_Model_Client_TransferOrder::postPost`). Re-queue recipe: [SQS-cap gotcha](../../../2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md#-the-job-carries-only-the-uuid--never-the-order-sqs-cap-incident-2026-10-05).
|
|
39
|
-
- A listed number may be a `
|
|
39
|
+
- A listed number may be a `TR` (old: `TA`) placeholder that write-back will replace, so identify the order by uuid/id, not number.
|
|
40
40
|
- No exclusion for cancelled orders — a cancelled TO with no NetSuite id keeps it DEGRADED.
|
|
41
41
|
|
|
42
42
|
## Change history
|
|
@@ -95,7 +95,7 @@ Recipient is `DevTeam@togatech.com` while testing. `2026-09-15b` swaps it to
|
|
|
95
95
|
## ⚠ Submitted must fire on `postPut` — the POST number is a placeholder
|
|
96
96
|
|
|
97
97
|
TOGa Supply saves a transfer order with the shared-model `FIELD_AUTOINCREMENT` placeholder number
|
|
98
|
-
prefixed **`
|
|
98
|
+
prefixed **`TR`** (was `TA` before TRUE-82715, e.g. `TA100000`). The real NetSuite number arrives about **12 seconds later** on
|
|
99
99
|
the write-back PUT from `worker2/Worker/Netsuite/SalesOrder.php`, which PUTs
|
|
100
100
|
`c_netsuiteInternalSalesOrderId` and `number`. Announcing on the POST would quote `TA100000` to the
|
|
101
101
|
customer.
|
|
@@ -17,7 +17,7 @@ project: _Underscore
|
|
|
17
17
|
client: prudential
|
|
18
18
|
type: profile
|
|
19
19
|
status: active
|
|
20
|
-
updated: 2026-10-
|
|
20
|
+
updated: 2026-10-18
|
|
21
21
|
owners: ["jcardinal", "rgirish", "bala", "mhammontree"]
|
|
22
22
|
files: []
|
|
23
23
|
related:
|
|
@@ -122,7 +122,7 @@ The order-created cron branches on this id; the shipped/delivered crons branch o
|
|
|
122
122
|
features/service-request-auth-rejection-alert.md.
|
|
123
123
|
|
|
124
124
|
- **SR number race (TRUE-82209: ServiceNow bursts → 1062 `Duplicate entry 'SR…'` → 400 `EV-10`, SR lost).** Fixed by
|
|
125
|
-
|
|
125
|
+
per-client record numbers from `RecordNumbers` (TRUE-82715; SR series continues); the `usleep` stopgap in `_Model_Prudential_ServiceRequest::prePost()`
|
|
126
126
|
is removed. Load-test with a distinct contact + serial per call and `customer.uuid` set. See
|
|
127
127
|
[Record numbering](../../2.0/apps/_underscore/features/autoincrement-record-numbering.md).
|
|
128
128
|
|
|
@@ -17,11 +17,12 @@ apps:
|
|
|
17
17
|
- togadesk
|
|
18
18
|
- toga25-desk
|
|
19
19
|
- toga25-blox
|
|
20
|
+
- toga25-iq
|
|
20
21
|
project: _Underscore
|
|
21
22
|
client: true
|
|
22
23
|
type: profile
|
|
23
24
|
status: active
|
|
24
|
-
updated: 2026-10-
|
|
25
|
+
updated: 2026-10-18
|
|
25
26
|
owners: [jcardinal, kyalamarthi, tcox, rgirish, mhammontree, akhokhani]
|
|
26
27
|
files: []
|
|
27
28
|
related:
|
package/package.json
CHANGED