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.
- package/knowledge/2.0/apps/_underscore/features/per-client-database-connections.md +14 -1
- package/knowledge/2.0/apps/api2/INDEX.md +2 -0
- package/knowledge/2.0/apps/api2/features/cross-client-data-retrieval.md +120 -0
- package/knowledge/2.0/apps/api2/features/encrypted-user-uuid-auth-handoff.md +54 -0
- package/knowledge/INDEX.md +2 -1
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/cost-centers.md +115 -0
- package/knowledge/clients/compass-usa/profile.md +9 -2
- package/knowledge/registry.json +9 -0
- package/package.json +1 -1
|
@@ -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)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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) —
|
|
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
|
|
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
|
package/knowledge/registry.json
CHANGED
package/package.json
CHANGED