toga-ai 1.0.278 → 1.0.280
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/1.0/apps/togadesk/INDEX.md +1 -1
- package/knowledge/1.0/apps/togadesk/features/field-service-dispatch.md +27 -1
- 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 +1 -1
- package/knowledge/clients/nycdoe/features/hold-status-sync.md +64 -4
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [TOGa Desk Architecture](architecture.md) | TOGa Desk is the staff-facing support desk **and field-service platform** (analysts work at `/desk/`). | desk/index.php, desk/includes/loader.php, desk/config.php, desk/includes/controllers/actions.php, desk/includes/controllers/data.php, desk/includes/controllers/modals.php, desk/includes/classes/class.app.php, desk/includes/classes/class.ticket.php, desk/api/index.php, desk/api/resources/tickets.php, crons/tickets.php |
|
|
6
6
|
| [Email-to-Ticket Intake (crons/tickets.php)](features/email-to-ticket-intake.md) | TOGa Desk ingests support email into tickets through a cron-driven IMAP poller (`crons/tickets.php`) plus a postfix pipe variant (`crons/pipe.php`). | crons/tickets.php, crons/tickets_prod.php, crons/pipe.php, desk/includes/classes/class.ticket.php |
|
|
7
|
-
| [Field-Service Dispatch (central / repair orders)](features/field-service-dispatch.md) | The **central** subsystem is TOGa Desk's field-service dispatch domain: repair-order lifecycle, technician scheduling, onsite vs depot service, parts, and shipm | desk/includes/controllers/actions/central/, desk/includes/classes/class.repair.php, desk/includes/classes/class.repairhistory.php, desk/_/browser/datatable/central.php, desk/template/pages/central.php, desk/template/pages/central/view.php, desk/template/modals/central/addTracking.php, library/app/model/togadesk/repairordertracking.php, library/app/api/carrier/ups.php |
|
|
7
|
+
| [Field-Service Dispatch (central / repair orders)](features/field-service-dispatch.md) | The **central** subsystem is TOGa Desk's field-service dispatch domain: repair-order lifecycle, technician scheduling, onsite vs depot service, parts, and shipm | desk/includes/controllers/actions/central/, desk/includes/classes/class.repair.php, desk/includes/classes/class.repairhistory.php, desk/_/browser/datatable/central.php, desk/template/pages/central.php, desk/template/pages/central/view.php, desk/template/modals/central/addTracking.php, library/app/model/togadesk/repairorder.php, library/app/model/togadesk/repairordertracking.php, library/app/api/carrier/ups.php |
|
|
8
8
|
| [Ticket Email Notifications (notifications table)](features/notifications.md) | Which TOGa Desk emails fire for a given client is driven **entirely by data**, not code: the `TOGaDeskSupport.notifications` table holds one row per `(clientid, | desk/includes/classes/class.notification.php, crons/tickets.php, crons/tickets_prod.php |
|
|
9
9
|
| [REST API (RPC-over-POST) & API-Key Auth](features/rest-api.md) | TOGa Desk exposes a programmatic API at `desk/api/`. | desk/api/index.php, desk/api/resources/tickets.php, desk/api/resources/assets.php, desk/api/resources/authenticate.php, desk/includes/classes/class.apikey.php, desk/includes/functions.php |
|
|
10
10
|
| [SMB Contract Editing & the clientMspId Corruption Trap](features/smb-contract-editing.md) | The SMB contracts page (`/desk/?route=toga/smbcontracts&togaClientId=<id>`) edits `TOGA_*.SMBContracts` rows via a modal. | desk/template/modals/toga/smbcontracts/smbContract.php, desk/includes/controllers/modals/toga/smbcontracts/smbContract.php, desk/includes/controllers/actions/toga/smbcontracts/smbContract.php, desk/includes/controllers/actions/toga/smbcontracts/edit.php |
|
|
@@ -6,7 +6,7 @@ project: TOGa Desk
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-07
|
|
10
10
|
owners: ["jcardinal", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- desk/includes/controllers/actions/central/
|
|
@@ -16,6 +16,7 @@ files:
|
|
|
16
16
|
- desk/template/pages/central.php
|
|
17
17
|
- desk/template/pages/central/view.php
|
|
18
18
|
- desk/template/modals/central/addTracking.php
|
|
19
|
+
- library/app/model/togadesk/repairorder.php
|
|
19
20
|
- library/app/model/togadesk/repairordertracking.php
|
|
20
21
|
- library/app/api/carrier/ups.php
|
|
21
22
|
related:
|
|
@@ -90,6 +91,26 @@ always confirm the client arm before changing shared central logic.
|
|
|
90
91
|
`controllers/actions/central/*.php` (the default fallback). Check both when tracing an action.
|
|
91
92
|
- Status recomputation is centralized in `updateStatus()` — write paths that bypass it can leave
|
|
92
93
|
an order in an inconsistent state.
|
|
94
|
+
- **`updateStatus()` / `qqStatus()` silently overwrite status via a raw `UPDATE`** (no
|
|
95
|
+
`repair_order_history` / `repair_order_notes` row). `qqStatus()`
|
|
96
|
+
(`library/app/model/togadesk/repairorder.php` ~L356) is a computed SQL `CASE` that
|
|
97
|
+
recomputes `repair_orders.status` from technician / dispatch / ETA / parts data on every
|
|
98
|
+
`updateStatus()` call (fired by `scheduleDispatch`, `technicianOnsite`, `technicianEnRoute`,
|
|
99
|
+
`awaitingDispatch`, `reworkAwaitingScheduling`, and part flows). If a status is not
|
|
100
|
+
explicitly gated inside `qqStatus()`, it will be recomputed away with no audit trail — this
|
|
101
|
+
is **shared, client-agnostic** logic (no tenant conditional), so a change hits every
|
|
102
|
+
repair-order client.
|
|
103
|
+
- **Hold states must be gated in `qqStatus()` or they are dropped.** There are six `HOLD_*`
|
|
104
|
+
constants; `qqStatus()` has hold gates that collapse recognized holds to base `STATUS_HOLD`.
|
|
105
|
+
TRUE-80060 fixed both `ONSITE_REPAIR`/`ONSITE_SERVICE`-branch gates (~L389, ~L429) recognizing
|
|
106
|
+
only 2 of the 6. **KNOWN REMAINING GAP:** the `DEPOT_REPAIR` branch (~L799) has **no hold gate
|
|
107
|
+
at all**, so a depot order set to any hold is still silently recomputed out of hold — a
|
|
108
|
+
follow-up ticket is needed. (DOE surfacing + full detail:
|
|
109
|
+
[NYCDOE Hold-Status Sync](../../../../clients/nycdoe/features/hold-status-sync.md).)
|
|
110
|
+
- **The hold sub-reason is lost from `repair_orders.status` after a recompute.** `qqStatus()`
|
|
111
|
+
collapses all six hold variants to base `HOLD`; the specific sub-reason persists only in
|
|
112
|
+
`repair_order_notes.newstatus`. Badges/reports/filters reading `repair_orders.status`
|
|
113
|
+
directly cannot distinguish the variants.
|
|
93
114
|
- Cross-database context: some central data controllers switch the DB link to a client database
|
|
94
115
|
(`App_Model_Client::changeDbLinkToClientDatabase()`).
|
|
95
116
|
- **UPS carrier-account dropdown is NOT honored by the shipment API call.** In
|
|
@@ -105,6 +126,11 @@ always confirm the client arm before changing shared central logic.
|
|
|
105
126
|
account-location map noted under *UPS shipment / carrier-account billing*.
|
|
106
127
|
|
|
107
128
|
## Change history
|
|
129
|
+
- 2026-07-07 — TRUE-80060: documented the shared `qqStatus()`/`updateStatus()` status-recompute
|
|
130
|
+
hold gates (in `library/app/model/togadesk/repairorder.php`) — silent raw-`UPDATE` overwrite,
|
|
131
|
+
hold states must be gated or dropped, the six-hold-variant collapse to base `STATUS_HOLD`, and
|
|
132
|
+
the untouched `DEPOT_REPAIR`-branch gap (follow-up needed). Added `repairorder.php` to files.
|
|
133
|
+
(mhammontree)
|
|
108
134
|
- 2026-06-15 — documented UPS Prepare Shipment flow: CARRIER_ACCOUNTS dropdown vs hardcoded
|
|
109
135
|
SHIPPER_NUMBER, and the gotcha that the dropdown selection is never forwarded to the UPS API
|
|
110
136
|
(always bills Agilant). Agilant account migrated 135FV5 → 8696XA. (mhammontree)
|
|
@@ -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
|
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
19
19
|
|
|
20
20
|
- **_underscore** (_Underscore) _(framework core)_ — 22 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 27 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
|
-
- **api2** (API) —
|
|
22
|
+
- **api2** (API) — 9 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
24
24
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
25
25
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: nycdoe
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-07
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/sync/nycdoe/send_ticket_updates.php
|
|
@@ -25,9 +25,24 @@ related:
|
|
|
25
25
|
DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and
|
|
26
26
|
**stay held** — holds are SLA-bearing in both systems. This doc covers the bidirectional
|
|
27
27
|
hold-status sync for DOE Incidents (INC) and Request Items (RITM), the authoritative
|
|
28
|
-
business rule that governs it, and the
|
|
29
|
-
status-sync companion to the broader
|
|
30
|
-
doc; the underlying crons and
|
|
28
|
+
business rule that governs it, and the two independent defects that let holds silently
|
|
29
|
+
revert. It is the status-sync companion to the broader
|
|
30
|
+
[ServiceNow / ASN integration](servicenow-integration.md) doc; the underlying crons and
|
|
31
|
+
`App_Api_NYCDOEV2` plumbing are documented there.
|
|
32
|
+
|
|
33
|
+
> **Two distinct hold-revert paths — different repos, mechanisms, and revert targets.**
|
|
34
|
+
> A held DOE order could be knocked out of hold by *either* of two unrelated code paths;
|
|
35
|
+
> both had to be closed:
|
|
36
|
+
> - **TRUE-79922 (worker crons):** the SNOW-side `state` 3⇄2 oscillation — an outbound
|
|
37
|
+
> self-clobber pushing `state:2` over a held `state:3`, plus an inbound wrong-field read
|
|
38
|
+
> — reverted the order to **"In Progress"**. Fixed; see *Change history* + the sections
|
|
39
|
+
> below. (Still pending deploy at time of writing.)
|
|
40
|
+
> - **TRUE-80060 (`library` shared model):** the local `qqStatus()` recompute silently
|
|
41
|
+
> demoting the order to a scheduling status (**"Orders Assigned / Awaiting Scheduling"**,
|
|
42
|
+
> `ORDER_ASSIGNED_AWAITING_SCHEDULING`) whenever `updateStatus()` ran. Fixed; see
|
|
43
|
+
> *Local status recompute* below. This defect is in a **shared, client-agnostic** model
|
|
44
|
+
> method — surfaced by DOE but affecting every TogaDesk repair-order client; the durable
|
|
45
|
+
> mechanism lives in [Field-Service Dispatch](../../../1.0/apps/togadesk/features/field-service-dispatch.md).
|
|
31
46
|
|
|
32
47
|
> DOE "tickets" are **`repair_orders`** rows (`App_Model_TogaDesk_RepairOrder`,
|
|
33
48
|
> `db_togadesk` / legacy `TOGaDeskSupport`, **clientid = 16**) — NOT the generic `tickets`
|
|
@@ -82,6 +97,26 @@ doc; the underlying crons and `App_Api_NYCDOEV2` plumbing are documented there.
|
|
|
82
97
|
technician. This mirrors the INC cron, which already sends `u_eta` while holding — keeping
|
|
83
98
|
scheduling metadata flowing while the hold STATE stays asserted.
|
|
84
99
|
|
|
100
|
+
### Local status recompute (`qqStatus` / `updateStatus`) — TRUE-80060
|
|
101
|
+
- `App_Model_TogaDesk_RepairOrder::qqStatus()` is a computed SQL `CASE` expression that
|
|
102
|
+
`App_Model_TogaDesk_RepairOrder::updateStatus()` writes to `repair_orders.status`. It
|
|
103
|
+
recomputes status from technician / dispatch / ETA / parts data. Both live in the shared
|
|
104
|
+
model `library/app/model/togadesk/repairorder.php` (`qqStatus` ~L356, `updateStatus` ~L181)
|
|
105
|
+
— **no tenant conditional**, so this path is not DOE-specific.
|
|
106
|
+
- `updateStatus()` is called from the togadesk central actions `scheduleDispatch`,
|
|
107
|
+
`technicianOnsite`, `technicianEnRoute`, `awaitingDispatch`, `reworkAwaitingScheduling`
|
|
108
|
+
(and the part flows). Any of these recomputes `repair_orders.status` via `qqStatus()`.
|
|
109
|
+
- `qqStatus()` has two "hold gates" that short-circuit the recompute when an order is held.
|
|
110
|
+
Before TRUE-80060 they recognized only **2 of the 6** `HOLD_*` constants
|
|
111
|
+
(`HOLD_AWAITING_APPROVAL`, `HOLD_OTHER`); the base `STATUS_HOLD` and the other three
|
|
112
|
+
(`HOLD_AWAITING_CUSTOMER_APPROVAL`, `HOLD_ADDITIONAL_INFO_NEEDED`,
|
|
113
|
+
`HOLD_AWAITING_CUSTOMER_CONTACT`) fell through and were silently recomputed into a
|
|
114
|
+
scheduling / WIP status. The fix (`repairorder.php` ~L389 and ~L429) expanded **both**
|
|
115
|
+
gates to all six `HOLD_*` constants, still collapsing to base `STATUS_HOLD` (matching the
|
|
116
|
+
pre-existing behavior of the two that already worked).
|
|
117
|
+
- The revert is **silent** — `updateStatus()` writes via a raw `UPDATE` with **no**
|
|
118
|
+
`repair_order_history` / `repair_order_notes` entry.
|
|
119
|
+
|
|
85
120
|
## Reference facts (DOE sync debugging)
|
|
86
121
|
|
|
87
122
|
- **SNOW request/response logs:** logged to the legacy `Logs` schema, **`API` table**
|
|
@@ -108,8 +143,33 @@ doc; the underlying crons and `App_Api_NYCDOEV2` plumbing are documented there.
|
|
|
108
143
|
on native `state` only.
|
|
109
144
|
- Holds are **never auto-released** — if a SNOW user manually changes status, the outbound
|
|
110
145
|
cron correctly re-asserts On Hold; that is intended, not a bug. Clear the hold in TOGaDesk.
|
|
146
|
+
- **Diagnosing which writer dropped a hold:** `repair_order_history`
|
|
147
|
+
(`repairid, userid, dtStamp, note`) records *user-driven* status changes. A hold reverting
|
|
148
|
+
with **no** history row between the hold and the revert = a raw-`UPDATE` writer
|
|
149
|
+
(`updateStatus`/`qqStatus`), **not** the note/reply path (`class.repair.php::addNotes` syncs
|
|
150
|
+
replies to SNOW but never writes `repair_orders.status`) and **not** the worker inbound cron
|
|
151
|
+
(`process_tickets.php` contains no reference to `ORDER_ASSIGNED_AWAITING_SCHEDULING`). This
|
|
152
|
+
is how TRUE-80060 was pinned to `qqStatus()` rather than the SNOW round-trip.
|
|
153
|
+
- **KNOWN REMAINING GAP (needs a follow-up ticket):** both `qqStatus()` hold gates live inside
|
|
154
|
+
the `type IN (ONSITE_REPAIR, ONSITE_SERVICE)` branch. The `DEPOT_REPAIR` branch
|
|
155
|
+
(`repairorder.php` from ~L799) has **no hold gate at all**, so a `DEPOT_REPAIR` order set to
|
|
156
|
+
any hold status is still silently recomputed out of hold by `updateStatus()` — the same
|
|
157
|
+
class of bug, untouched by TRUE-80060.
|
|
158
|
+
- `qqStatus()` collapses all recognized hold sub-statuses to base `HOLD` in
|
|
159
|
+
`repair_orders.status`; the specific hold **sub-reason persists only in
|
|
160
|
+
`repair_order_notes.newstatus`**. Any status badge / report / filter reading
|
|
161
|
+
`repair_orders.status` directly cannot distinguish the six hold variants after a recompute.
|
|
111
162
|
|
|
112
163
|
## Change history
|
|
164
|
+
- 2026-07-07 — TRUE-80060: closed the **second** DOE hold-revert path — the local
|
|
165
|
+
`qqStatus()` recompute (in the shared model `library/app/model/togadesk/repairorder.php`)
|
|
166
|
+
silently demoting held orders to `ORDER_ASSIGNED_AWAITING_SCHEDULING` whenever
|
|
167
|
+
`updateStatus()` ran. Root cause: both `qqStatus()` hold gates recognized only 2 of the 6
|
|
168
|
+
`HOLD_*` constants; expanded both (~L389, ~L429) to all six, collapsing to base
|
|
169
|
+
`STATUS_HOLD`. Separate from TRUE-79922 (different repo/mechanism/revert target). Documented
|
|
170
|
+
the raw-`UPDATE` silent-revert, the `repair_order_history`-gap debugging technique, and the
|
|
171
|
+
untouched `DEPOT_REPAIR`-branch gap (follow-up ticket needed). php -l clean; php-reviewer 0
|
|
172
|
+
critical / 0 blocking. (mhammontree)
|
|
113
173
|
- 2026-06-29 — TRUE-79922: fixed DOE holds never persisting (reverted to In Progress, broke
|
|
114
174
|
SLA reporting both sides). Two coupled defects: outbound self-clobber (`send_ticket_updates.php`
|
|
115
175
|
unguarded Assigned→In-Progress pushing `state:2` over a held `state:3`) and inbound
|
package/package.json
CHANGED