toga-ai 1.0.277 → 1.0.279

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-06-18
10
- owners: ["dfranks"]
10
+ owners: ["dfranks", "jcardinal"]
11
11
  files:
12
12
  - _underscore/Database.php
13
13
  - _underscore/ApiRequest.php
@@ -62,6 +62,17 @@ Engine InnoDB, `utf8mb4_unicode_ci`.
62
62
 
63
63
  None — uniform across clients; only the `<Id>` suffix differs.
64
64
 
65
+ ## Related shared clusters (not per-client)
66
+
67
+ Alongside the three per-client aliases and the shared **Core** cluster, a dedicated shared
68
+ **Cache** cluster was added (2026-07-07): `Core.Databases` id **145**, `_underscore` alias
69
+ `DB_CACHE`, `CACHE_DATABASE_ID = 145`. It is modeled like Core (a single shared cluster, **not**
70
+ per-client), region-aware (1=us-east-1, 2=us-west-2, 3=eu-west-1) with per-region `DatabaseHosts`
71
+ (`hostCluster` DevOps-TBD). It exists to keep cache churn off Core and backs the api2
72
+ [multi-client data retrieval](../../api2/features/cross-client-data-retrieval.md) engine. Boot
73
+ registration is region-aware in `api2/Controller/Index.php`; the alias const is in `api2/_.php`.
74
+ `Records.ttlCache` (TINYINT UNSIGNED, default 15) governs cache TTL.
75
+
65
76
  ## Gotchas / known issues
66
77
 
67
78
  - **The "logs DB write trap" (local dev).** A laptop usually imports only `Client_<Id>`, not
@@ -82,6 +93,8 @@ None — uniform across clients; only the `<Id>` suffix differs.
82
93
 
83
94
  ## Change history
84
95
 
96
+ - 2026-07-07 — Noted the new shared **Cache** cluster (`Databases` id 145, alias `DB_CACHE`,
97
+ region-aware) added for the api2 cross-client retrieval engine. (jcardinal)
85
98
  - 2026-06-18 — Documented after the `Logs_Growrk`/`Logs_Aig` local 500s while testing the
86
99
  NetSuite TOGa Supply wrappers; created the missing local log schemas structure-only. (dfranks)
87
100
 
@@ -3,6 +3,8 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
6
+ | [Multi-Client (Cross-Client) Data Retrieval](features/cross-client-data-retrieval.md) | A single authenticated V2 GET listing can return records across **many** clients (designed for 1000+) that the caller is entitled to, honoring **each target cli | api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php, api2/_.php, _underscore/Model/Cache/Table.php, _underscore/Model/Cache/Tables/Client.php, dbchanges2/Core/2026-06-30b - CacheClusterRegistrationAndRecordTtl.sql, dbchanges2/Cache/2026-06-30a - MultiClientCacheTables.sql |
7
+ | [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
6
8
  | [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple languages without forking the schema or breaking English consume | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql |
7
9
  | [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
8
10
  | [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
@@ -0,0 +1,120 @@
1
+ ---
2
+ title: Multi-Client (Cross-Client) Data Retrieval
3
+ framework: "2.0"
4
+ repo: api2
5
+ project: API
6
+ client: shared
7
+ type: feature
8
+ status: draft
9
+ updated: 2026-07-07
10
+ owners: [jcardinal]
11
+ files:
12
+ - api2/Component/Api/CrossClient/CrossClient.php
13
+ - api2/Component/Api/V2/V2.php
14
+ - api2/Controller/Index.php
15
+ - api2/_.php
16
+ - _underscore/Model/Cache/Table.php
17
+ - _underscore/Model/Cache/Tables/Client.php
18
+ - dbchanges2/Core/2026-06-30b - CacheClusterRegistrationAndRecordTtl.sql
19
+ - dbchanges2/Cache/2026-06-30a - MultiClientCacheTables.sql
20
+ related:
21
+ - ./encrypted-user-uuid-auth-handoff.md
22
+ - ../../_underscore/features/per-client-database-connections.md
23
+ - ../../_underscore/features/acl-permission-chain.md
24
+ ---
25
+
26
+ ## What it is
27
+
28
+ A single authenticated V2 GET listing can return records across **many** clients (designed for
29
+ 1000+) that the caller is entitled to, honoring **each target client's own ACL**. It is triggered
30
+ by a non-empty `client` query-string option on a GET listing; without that option the ordinary
31
+ single-client path is byte-for-byte unchanged.
32
+
33
+ Because client DBs live on **different clusters**, a cross-DB JOIN/UNION is impossible. The feature
34
+ is a **scatter-gather** engine: it fans out to each entitled client's own V2 API, merges the
35
+ streams, and caches the result per-user on a dedicated Cache cluster.
36
+
37
+ > **Status: code-complete + reviewer-hardened, NOT yet runtime-tested.** Result rows are stored as
38
+ > a JSON `rowData` column (deliberate v1 deviation from real typed columns). Phases 4 (single-record
39
+ > cross-client), 5 (worker2 page-ahead prefetch), and 7 (live load test / verify `_Query`/`_Database`
40
+ > method names against a running instance) are open. Per-client token hardening (§0) is an
41
+ > owner-accepted deferred risk.
42
+
43
+ ## How it works
44
+
45
+ Entry point is `_Component_Api_CrossClient` (`api2/Component/Api/CrossClient/CrossClient.php`),
46
+ delegated to from a guard at the **top** of the `ACTION__LIST` case in
47
+ `_Component_Api_V2::processRoutePairs()`: when `isCrossClientRequest($httpOptions)` is true (a
48
+ non-empty `client` option), it calls `handleListing()`, merges result rows into `$outData` and meta
49
+ into `$this->response->meta`, sets status 200, and breaks. The guard is **inert** on ordinary
50
+ single-client GETs.
51
+
52
+ 1. **resolveScope** — the caller's entitled clients resolve via the `Users_Clients` bridge
53
+ (`Users.homeClientUserId` maps one home user to per-client user records across tenant DBs); each
54
+ target yields a minted per-client identity.
55
+ 2. **queryHash** — SHA-256 of the normalized query; the cache key.
56
+ 3. **Build lock** — a named `GET_LOCK` (`tablecache:<clientId>:<userId>:<hash>`) prevents duplicate
57
+ concurrent builds.
58
+ 4. **Fan out** — a two-phase concurrent HTTP handshake per client (see
59
+ [encrypted-user-uuid-auth-handoff](./encrypted-user-uuid-auth-handoff.md)): Phase A mints an
60
+ access token, Phase B issues the authenticated GET.
61
+ 5. **Watermark k-way merge** — client streams are merged into a globally-correct sort order using a
62
+ watermark (`safeRowCount`/`keyBeyond`); a `deepen` loop pulls more from lagging streams, capped
63
+ by a 50-iteration backstop.
64
+ 6. **Cache build/serve** — rows are cached on the Cache cluster; `servePage` emits a keyset page.
65
+
66
+ ### Total order + keyset pagination (V2 query builder, Phase 0a/0b)
67
+
68
+ Cross-client global sort correctness requires each client's stream to be a **total order**:
69
+ - **0a** — the LIST query builder always appends a **primary-key ASC tiebreaker** to `ORDER BY`
70
+ (also improves single-client determinism).
71
+ - **0b** — a new keyset/seek pagination mode runs alongside offset mode; it engages **only** when
72
+ the caller passes `seekValue` + `seekId` options. It injects a seek `WHERE` predicate (V2.php
73
+ ~line 3782) and switches `LIMIT` from offset to `recordsPerPage` (~line 3861). The composite
74
+ unique sort key is `(sortfield, clientId, primaryKeyId)`.
75
+
76
+ ### Query-string forwarding (load-bearing gotcha)
77
+
78
+ V2 parses list options (`fields`/`where`/`sort`/`join`/`group`…) from the **raw** query-string
79
+ values into structured `$httpOptions` — re-serializing the parsed array does **not** round-trip.
80
+ The fan-out therefore forwards the caller's **original** `$_SERVER['QUERY_STRING']` through an
81
+ explicit **allowlist** of forwardable keys (`fields, where, sort, group, distinct, join, ojoin,
82
+ depth, calcDepth, _`), re-stamping `recordsPerPage`, `seekValue`/`seekId`, and a fresh
83
+ `transactionId` per client. The allowlist (not a denylist) prevents internal flags like
84
+ `surface`/`debug` from leaking into sub-requests.
85
+
86
+ ### Cache cluster
87
+
88
+ A new dedicated **Cache** cluster (Core-style shared cluster, `Databases` id **145**, alias
89
+ `DB_CACHE`, `CACHE_DATABASE_ID = 145`) keeps cache churn off Core. It is region-aware
90
+ (1=us-east-1, 2=us-west-2, 3=eu-west-1) with per-region `DatabaseHosts`; `hostCluster` values are
91
+ DevOps TBD. Boot registration lives in `api2/Controller/Index.php`; the alias const in `api2/_.php`.
92
+ See [per-client-database-connections](../../_underscore/features/per-client-database-connections.md)
93
+ for how it sits among the shared/per-client clusters.
94
+
95
+ ## Key rules
96
+
97
+ - **Triggered only by a non-empty `client` option** — single-client GETs are unaffected.
98
+ - **Each target client's ACL is honored** via a minted per-client identity, not a global aggregate
99
+ view. This preserves ACL fidelity across tenants.
100
+ - **Every outbound sub-request needs its own globally-unique `transactionId`** — the API enforces
101
+ uniqueness; it is the load-bearing idempotency/audit key across Core + Client logs.
102
+ - **Forward the original query string, not the parsed options** — parsed `$httpOptions` do not
103
+ round-trip through re-serialization.
104
+ - `MAX_CONCURRENT_CLIENT_FETCHES = 25` bounds fan-out concurrency (PHP-FPM has no in-process
105
+ concurrency, so transport is an HTTP pool via `curl_multi`).
106
+
107
+ ## Gotchas
108
+
109
+ - **Cross-client output row shape must match single-client.** Served rows are cast to `(object)` in
110
+ `servePage` so payload interceptors reading `$row->prop` don't throw — the single-client path
111
+ emits `(object)$outRow`.
112
+ - **The X-Cross-Client / X-Cross-User custom-header transport was a dead stub** — the V2 engine
113
+ never reads such headers. The real transport is the two-phase encrypted-UUID auth handshake.
114
+ - **`curl_multi` busy-spin guard** — both multi loops `usleep(100)` when
115
+ `curl_multi_select() === -1`.
116
+
117
+ ## Change history
118
+ - 2026-07-07 — Initial capture: scatter-gather cross-client retrieval engine (orchestrator, watermark
119
+ k-way merge, keyset pagination Phase 0a/0b, `client`-option delegation, Cache cluster id 145).
120
+ Code-complete + reviewer-hardened, not yet runtime-tested. (jcardinal)
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)
3
+ framework: "2.0"
4
+ repo: api2
5
+ project: API
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-07
10
+ owners: [jcardinal]
11
+ files:
12
+ - api2/Component/Api/CrossClient/CrossClient.php
13
+ related:
14
+ - ./cross-client-data-retrieval.md
15
+ ---
16
+
17
+ ## What it is
18
+
19
+ `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**:
20
+ given an encrypted `{client, user}` UUID pair, it mints a fresh access token for that client+user
21
+ without the caller holding the target client's credentials. It is what the scatter-gather
22
+ [cross-client data retrieval](./cross-client-data-retrieval.md) engine uses to authenticate each
23
+ per-client sub-request.
24
+
25
+ ## How it works
26
+
27
+ - The encrypted `{client, user}` pairs are produced during **normal** auth: `/auth/api` and
28
+ `/auth/login` emit them as `data.clients[].encrypted.{client, user}`, encrypted with
29
+ `KEY_API_SECRET_REFRESH_TOKEN`.
30
+ - The route reads `$httpPayload->client` and `$httpPayload->user` (the encrypted UUIDs) from the
31
+ **JSON body**, decrypts them against the API secret rotation keys, and mints an access token.
32
+ - The token is returned at `response data.tokens.access`.
33
+
34
+ ### Two-phase fan-out handshake (as used by cross-client retrieval)
35
+
36
+ - **Phase A** — concurrent `POST /auth/encrypted-user-uuid` with the minted `{client, user}`
37
+ encrypted-UUID pair in the JSON body → read `data.tokens.access`.
38
+ - **Phase B** — concurrent authenticated `GET` with `Authorization: Bearer <token>`.
39
+ - Every request (both phases) carries its own globally-unique `transactionId` (query string); the
40
+ API enforces uniqueness.
41
+
42
+ ## Key rules
43
+
44
+ - **Identity source of truth is the `Users_Clients` bridge** — `Users.homeClientUserId` maps one
45
+ home user to its per-client user records across tenant DBs; that is where the per-client UUIDs
46
+ come from.
47
+ - **Never store or log the encrypted UUID values** — they are credentials; document where they live
48
+ (`data.clients[].encrypted`), never the value.
49
+ - The encrypted pairs are encrypted with `KEY_API_SECRET_REFRESH_TOKEN` (rotation keys); decryption
50
+ tolerates the rotation set.
51
+
52
+ ## Change history
53
+ - 2026-07-07 — Documented `/auth/encrypted-user-uuid` as the cross-client/SSO identity-handoff route
54
+ and its two-phase use by the cross-client fan-out engine. (jcardinal)
@@ -6,6 +6,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 11 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 13 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
+ - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
9
10
  - **togadesk** (TOGa Desk) — 8 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
10
11
  - **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
11
12
  - **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
@@ -18,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
18
19
 
19
20
  - **_underscore** (_Underscore) _(framework core)_ — 22 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
20
21
  - **worker2** (Worker) — 27 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
21
- - **api2** (API) — 7 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
22
+ - **api2** (API) — 9 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
22
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
23
24
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
24
25
  - **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
@@ -3,6 +3,7 @@
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
5
  | [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
6
+ | [Cost Centers — Unit Locations, numeric-only policy](features/cost-centers.md) | 2.0 | A Compass "cost center" — the value a user picks in commerce and that lands on an order — is **not** a `CostCenters` row. | toga2-commerce/src/pages/Cart/api/CartApi.ts, worker1.5/crons/toga2/compass/import_locations.php, _underscore/Model/Compass/SalesOrder.php, api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-06 - RemoveNonNumericCostCenters.sql |
6
7
  | [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql |
7
8
  | [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
8
9
  | [Compass MITS PO Transmission to Vendors](features/mits-po-transmission-to-vendors.md) | 2.0 | The 1.0 worker cron `2_transmit_mits_purchase_orders_to_vendors.php` transmits Compass PurchaseOrders to their vendors (Office Depot, Strategic Systems, Compass | worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, library/app/client/compass.php |
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Cost Centers — Unit Locations, numeric-only policy
3
+ framework: "2.0"
4
+ project: _Underscore
5
+ client: compass-usa
6
+ type: client-feature
7
+ status: active
8
+ updated: 2026-07-06
9
+ owners: [bala]
10
+ files:
11
+ - toga2-commerce/src/pages/Cart/api/CartApi.ts
12
+ - worker1.5/crons/toga2/compass/import_locations.php
13
+ - _underscore/Model/Compass/SalesOrder.php
14
+ - api2/Component/Api/V2/V2.php
15
+ - dbchanges2/Client_Compass/2026-07-06 - RemoveNonNumericCostCenters.sql
16
+ related:
17
+ - ../profile.md
18
+ - ../../../2.0/apps/toga2-commerce/features/cart-page-config-architecture.md
19
+ ---
20
+
21
+ ## Summary
22
+ A Compass "cost center" — the value a user picks in commerce and that lands on an order — is
23
+ **not** a `CostCenters` row. In `Client_Compass` it is a **Unit-level Location**
24
+ (`locationTypeId = 1`) identified by its **`c_erpSystemEntityId`** custom field. The
25
+ `CostCenters` table (and the `Users_CostCenters` / `Locations_CostCenters` bridges,
26
+ `SalesOrders.costCenterId`, `Users.defaultCostCenterId`) are **empty / vestigial** for Compass
27
+ and unused. As of **2026-07-06**, Compass policy is **numeric-only cost centers**: users may only
28
+ select cost centers whose `c_erpSystemEntityId` is all digits, all existing non-numeric ones are
29
+ soft-deactivated, and the import will never re-create or re-activate a non-numeric one.
30
+
31
+ This is US Compass only (`Client_Compass`). Compass Canada is explicitly out of scope.
32
+
33
+ ## How it works
34
+
35
+ ### What a cost center actually is
36
+ - A cost center = a **Unit Location** in `Client_Compass.Locations` (`locationTypeId = 1`),
37
+ keyed by the custom field **`c_erpSystemEntityId`**.
38
+ - `c_erpEntityId` is a **different** field on the Unit row (the unit id) — do not confuse the
39
+ two. At checkout the chosen cost-center value is written to **`SalesOrders.c_erpEntityId`** as a
40
+ **plain string, not a foreign key**. `_underscore/Model/Compass/SalesOrder.php` exposes a
41
+ `_costCenter` computed field.
42
+ - The `CostCenters` table is empty for Compass; nothing reads
43
+ `Users_CostCenters` / `Locations_CostCenters` / `SalesOrders.costCenterId` /
44
+ `Users.defaultCostCenterId`.
45
+
46
+ ### The commerce dropdown
47
+ - Built by **`fetchCostCenter()`** in
48
+ `toga2-commerce/src/pages/Cart/api/CartApi.ts`, which calls
49
+ `GET /locations` filtered on `c_erpSystemEntityId LIKE %search% AND isActive = 1`, and displays
50
+ each as `"{c_erpSystemEntityId} - {name}"`.
51
+ - It funnels **both** the cart checkout and the New-User form, so a single filter in
52
+ `fetchCostCenter` covers both entry points.
53
+ - **api2 does no cost-center validation** — the FE is the only selection gate.
54
+
55
+ ### Where the values come from (import)
56
+ - `worker1.5/crons/toga2/compass/import_locations.php` reads an SFTP file and sets
57
+ `c_erpSystemEntityId` on Unit rows from **column 16**.
58
+ - Repeated imports have massively **duplicated** cost-center rows (~730× per value).
59
+
60
+ ### Numeric-only enforcement (2026-07-06) — three layers
61
+ 1. **Front-end selection block** — in `fetchCostCenter`, on the **search path**
62
+ (`useSearchOptions === true`), returned locations are filtered to numeric-only cost centers
63
+ with `/^[0-9]+$/` on `c_erpSystemEntityId.trim()`. The non-search / display path is left
64
+ untouched (so an already-saved value still renders). Done client-side because api2 rejects a
65
+ server-side numeric filter (see gotcha) and api2 was out of scope.
66
+ 2. **Data cleanup (one-time, reversible)** —
67
+ `dbchanges2/Client_Compass/2026-07-06 - RemoveNonNumericCostCenters.sql` runs
68
+ `UPDATE Locations SET isActive = 0` for every active row whose `c_erpSystemEntityId` is
69
+ non-empty and does **not** match `^[0-9]+$`. **Soft delete (isActive=0), not a hard delete.**
70
+ ~401,719 active rows across 1,185 distinct non-numeric codes. They then drop out of the
71
+ commerce lookup automatically via its existing `isActive = 1` filter.
72
+ 3. **Import durability guard** — in `import_locations.php`, in **both** Unit branches
73
+ (new-create and existing-update), after `$isActiveValue` is computed it is forced to `0` when
74
+ the cost center is non-empty and not `ctype_digit`, so future imports never re-activate or
75
+ create active non-numeric cost centers.
76
+
77
+ ## Gotchas
78
+ - **api2 cannot do a server-side numeric filter.** The V2 query-string where-parser
79
+ (`api2/Component/Api/V2/V2.php` → `parseOptionsWhere`) **whitelists operators and rejects
80
+ `regexp` with a 500**. Any numeric/regex filtering must happen client-side (or via a new
81
+ operator in api2, which was out of scope here).
82
+ - **Two fields, easy to swap.** `c_erpSystemEntityId` is the cost center (what users pick);
83
+ `c_erpEntityId` is the unit id and the value that orders record on
84
+ `SalesOrders.c_erpEntityId`.
85
+ - **Second, possibly-live copy of the import cron not yet guarded.** A second copy exists at
86
+ `worker/crons/toga2/compass/DOA_Scripts/import_locations.php` (appears disabled / "DOA"). It
87
+ did **not** receive the numeric guard — **confirm whether it is live** before trusting the
88
+ guard to be complete.
89
+ - Non-numeric cost centers include both number+alphabet combos (e.g. `10007A`, `224GT`) and
90
+ letters-only codes (e.g. `EAGLE`, `TAMU`). All are retired in favor of numeric-only.
91
+
92
+ ## Safety verification (before deactivation, prod Client_Compass)
93
+ Deactivation was proven safe against prod data:
94
+ - **0** users (active or inactive, via `Users.locationId`), **0** sales orders / SO items /
95
+ purchase orders / item fulfillments / item receipts / transfer orders / inventory adjustments /
96
+ tickets / contacts / child locations reference these non-numeric Unit locations.
97
+ - Only **1,180** `Locations_Addresses` links point at them (harmless, left intact).
98
+ - Only **2** sales orders ever used a non-numeric cost-center value (stored as string on
99
+ `SalesOrders.c_erpEntityId`, unaffected by deactivating the Location).
100
+ - **0** active users have a non-numeric `c_erpEntityId`, so no cost-center display goes blank.
101
+ - Proven end-to-end in dev-sandbox: after the migration, active non-numeric rows = **0** (all
102
+ 1,185 codes incl. the 725 number+alphabet combos) while pure-numeric stayed untouched at
103
+ **440,005** active rows.
104
+
105
+ ## Scope constraints (from the developer)
106
+ - Do **not** change anything in `api2`.
107
+ - Do **not** hard-delete — use `isActive = 0` (reversible).
108
+ - **US Compass only** (`Client_Compass`); Compass Canada is out of scope.
109
+
110
+ ## Change history
111
+ - 2026-07-06 — Established numeric-only cost-center policy across three layers: FE `fetchCostCenter`
112
+ search-path filter (`/^[0-9]+$/`), one-time `RemoveNonNumericCostCenters.sql` soft-deactivation
113
+ (~401,719 rows / 1,185 codes), and an `import_locations.php` guard forcing `isActive=0` on
114
+ non-`ctype_digit` cost centers in both Unit branches. Documented that a Compass cost center is a
115
+ Unit Location keyed by `c_erpSystemEntityId` (not the vestigial `CostCenters` table). (bala)
@@ -8,16 +8,18 @@ apps:
8
8
  - toga25-supply
9
9
  - toga2-commerce
10
10
  - worker
11
+ - worker1.5
11
12
  - dbchanges2
12
13
  project: _Underscore
13
14
  client: compass-usa
14
15
  type: profile
15
16
  status: active
16
- updated: 2026-06-30
17
+ updated: 2026-07-06
17
18
  owners: [jcardinal, bala, tcox]
18
19
  files: []
19
20
  related:
20
21
  - features/asn-to-item-fulfillment.md
22
+ - features/cost-centers.md
21
23
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
22
24
  - ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
23
25
  ---
@@ -34,7 +36,9 @@ separate, related client (see its own profile).
34
36
  - **2.0:** `_underscore` backend, prod schema `Client_Compass` (worker link `db_compass`).
35
37
  Client-specific model overrides live under `_underscore/Model/Compass/`.
36
38
  - **1.0:** worker crons under `worker/crons/toga2/compass/` handle email-based imports and
37
- notifications.
39
+ notifications. A separate **`worker1.5`** repo (also App_/1.0-style) runs the SFTP location
40
+ import (`worker1.5/crons/toga2/compass/import_locations.php`) that seeds Unit Locations /
41
+ cost centers.
38
42
  - Storefront: compass.togacommerce.com / compass.togahub.com. The storefront app repo is
39
43
  **`toga2-commerce`** (2.0, consumes api2).
40
44
 
@@ -56,6 +60,9 @@ separate, related client (see its own profile).
56
60
  ## Key features (this client)
57
61
  - [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) — the
58
62
  `_Model_Compass_AdvanceShippingNotice::postPost` handler.
63
+ - [Cost Centers (Unit Locations, numeric-only)](features/cost-centers.md) — what a Compass
64
+ "cost center" actually is (a Unit Location keyed by `c_erpSystemEntityId`), and the
65
+ numeric-only selection/import/data policy (2026-07-06).
59
66
 
60
67
  ## Notes
61
68
  - **Order status is shipped-only (2026-06-30).** Compass imports all IF stages
@@ -41,6 +41,15 @@
41
41
  "role": "app",
42
42
  "dependsOn": []
43
43
  },
44
+ {
45
+ "repo": "worker1.5",
46
+ "project": "Worker 1.5",
47
+ "framework": "1.0",
48
+ "role": "app",
49
+ "dependsOn": [
50
+ "library"
51
+ ]
52
+ },
44
53
  {
45
54
  "repo": "toga2-supply",
46
55
  "project": "TOGa Supply",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.277",
3
+ "version": "1.0.279",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",