toga-ai 1.0.258 → 1.0.260
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +57 -2
- package/knowledge/2.0/apps/toga25-supply/features/client-configurable-fields.md +23 -1
- package/knowledge/clients/compass-canada/profile.md +8 -1
- package/knowledge/clients/quad/profile.md +9 -1
- package/knowledge/sessions/2026-06-30-talos-pricing-platform-jcardinal.md +30 -27
- package/package.json +1 -1
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
|
|
15
15
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
|
|
16
16
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql |
|
|
17
|
-
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
|
|
17
|
+
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
|
|
18
18
|
| [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
|
|
19
19
|
| [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
|
|
@@ -6,12 +6,16 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal]
|
|
9
|
+
updated: 2026-07-01
|
|
10
|
+
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Core/Surface.php
|
|
13
|
+
- _underscore/Model/Client/AclRecordScript.php
|
|
14
|
+
- _underscore/Model/Core/RecordScript.php
|
|
13
15
|
- dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql
|
|
14
16
|
- dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql
|
|
17
|
+
- dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql
|
|
18
|
+
- dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql
|
|
15
19
|
- dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
|
|
16
20
|
- dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql
|
|
17
21
|
- dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
|
|
@@ -113,6 +117,32 @@ which surfaced latent resolution gaps in `_buildBundle`/`_resolveElement`:
|
|
|
113
117
|
`_Model_<Client>_*` overrides + `Client_ApiPayloadInterceptor` hooks — the Surface layer does
|
|
114
118
|
**not** absorb business logic.
|
|
115
119
|
|
|
120
|
+
## How a surfaces scripted-API call is authorized (route-level gate)
|
|
121
|
+
|
|
122
|
+
Authorizing a surfaces script (`meta`, `meta-group`, `debug`) is a **two-layer** check. Get the
|
|
123
|
+
first layer wrong and you get a **403 `EZ-1 AUTHORIZATION`** before any element ACL runs.
|
|
124
|
+
|
|
125
|
+
1. **Route-level dispatch gate — `Client_<tenant>.AclRecordScripts`.** The surfaces dispatcher
|
|
126
|
+
authorizes each API script (route + method + phpMethod) via the **CLIENT-tier**
|
|
127
|
+
`AclRecordScripts` table (cols `uuid`, `recordScriptId` → `Core.RecordScripts`, `roleId` →
|
|
128
|
+
`Client_<tenant>.Roles`). The check is keyed on the JWT's **`id.client.roles`** (client roles),
|
|
129
|
+
**NOT** the platform `core.roles` and **NOT** the Core `AclRecordPermissions` chain.
|
|
130
|
+
`Core.RecordScripts` maps route/method/phpMethod → an ACL "record" (`recordId`). The three
|
|
131
|
+
surfaces scripts: `meta` (GET, phpMethod `meta`), `meta-group` (GET, `metaGroup`), `debug` (GET,
|
|
132
|
+
`metaDebug`). `_Model_Core_Surface::resolve/metaGroup` read the caller's roles from
|
|
133
|
+
**`id.client.roles`** (Surface.php ~lines 56/87).
|
|
134
|
+
2. **Element-level ACL (runs *inside* `meta`/`metaGroup`, AFTER the route gate passes)** — dropping
|
|
135
|
+
individual surface elements uses a **separate** client-tier table `_Model_Client_AclActionPermission`,
|
|
136
|
+
gated by each `SurfaceElement.aclActionId`. This is the "composition, not absorption" step in
|
|
137
|
+
`resolve()` step 4; it never causes the route-level 403.
|
|
138
|
+
|
|
139
|
+
**Debugging a surfaces 403 EZ-1 (hard-won):** it is the route-level gate — a missing
|
|
140
|
+
`Client_<tenant>.AclRecordScripts` grant resolved by **client role**. Do **not** chase
|
|
141
|
+
`Core.AclRecordPermissions`, `appId` scoping, or `AclLogicGroups`/`AclRecordExpressions` — all were
|
|
142
|
+
investigated and ruled out as red herrings: `appId = NULL` is the normal working pattern, and the
|
|
143
|
+
Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The gate is purely
|
|
144
|
+
"does this client role have an `AclRecordScripts` row for this `recordScriptId`?"
|
|
145
|
+
|
|
116
146
|
## Gotchas
|
|
117
147
|
|
|
118
148
|
- **A Core RecordScript needs `Client.AclRecordScripts` rows per role to be invokable — record-level
|
|
@@ -167,8 +197,33 @@ which surfaced latent resolution gaps in `_buildBundle`/`_resolveElement`:
|
|
|
167
197
|
per-role `Client.AclRecordScripts` dispatch grant. Verified via toga-db — `meta` script id 25 was
|
|
168
198
|
granted roles 1/3/4 but `meta-group` (id 27) had **none** until the grant ran. Any new surfaces
|
|
169
199
|
scripted-API method repeats this: record READ never authorizes dispatch.
|
|
200
|
+
- **The meta-group grant is seeded per-tenant, not globally — new tenants repeat the 403.** When
|
|
201
|
+
`meta-group` shipped, the `AclRecordScripts` grant was seeded **only** for `Client_Compass`
|
|
202
|
+
(recordScriptId → roles 1,3,4). `Client_Quad` and `Client_CompassCanada` had **no** grant for the
|
|
203
|
+
meta-group recordScriptId at all — their `meta` was granted to client role 1 (why `meta` worked but
|
|
204
|
+
`meta-group` 403'd for the same user/token). Same-user-different-script + same-script-different-tenant
|
|
205
|
+
is the fingerprint of this gap. Fix (2026-07-01): two `dbchanges2/Client_<tenant>/` migrations
|
|
206
|
+
granting meta-group to **client role 1** (parity with each tenant's existing `meta` grant),
|
|
207
|
+
id-agnostic (resolve `Core.RecordScripts` id by route/method/phpMethod, cross-DB read of `Core.*`
|
|
208
|
+
per the 2026-06-29c ACL pattern) and re-runnable via `NOT EXISTS`. Surface-layer DB changes ship to
|
|
209
|
+
the **beta** environment only. ⚠ **Root-cause not yet closed:** the meta-group grant should be
|
|
210
|
+
added to the `dbchanges2/Client/` **baseline (blank) seed** so newly-cut clients get it
|
|
211
|
+
automatically — otherwise the next migrated tenant hits the identical 403. Only Compass (already
|
|
212
|
+
correct), Quad, and CompassCanada are addressed. Also note **role breadth**: Compass exposes
|
|
213
|
+
surfaces to roles 1,3,4; Quad/CC got role 1 only (parity with their `meta`). If Quad/CC should
|
|
214
|
+
match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
|
|
170
215
|
|
|
171
216
|
## Change history
|
|
217
|
+
- 2026-07-01 — Documented the full route-level authorization mechanism (new section): the surfaces
|
|
218
|
+
script gate is `Client_<tenant>.AclRecordScripts` keyed on JWT `id.client.roles` (client roles,
|
|
219
|
+
not `core.roles`, not `Core.AclRecordPermissions`); element-drop is a separate
|
|
220
|
+
`_Model_Client_AclActionPermission`/`aclActionId` layer running inside `resolve()` after the route
|
|
221
|
+
gate; captured the red-herring list (`appId=NULL` is normal, Core grants all evaluate `all`/`"1"`).
|
|
222
|
+
Fixed the `meta-group` 403 EZ-1 for **Quad** and **Compass Canada** — the meta-group
|
|
223
|
+
`AclRecordScripts` grant had been seeded for Compass only; added two id-agnostic, NOT-EXISTS-guarded
|
|
224
|
+
`Client_<tenant>/` migrations granting meta-group to client role 1 (beta env only). Flagged the
|
|
225
|
+
open root-cause: add the grant to the `Client/` baseline seed, and decide role breadth (1 vs 1,3,4)
|
|
226
|
+
for Quad/CC. (apeterson)
|
|
172
227
|
- 2026-06-30 — Added `metaGroup()` (`GET /v2/surfaces/meta-group?slugs=a,b,c`): batched composition
|
|
173
228
|
over the existing per-slug `resolve()` cache, capped at `META_GROUP_MAX_SLUGS=25`, unknown slug →
|
|
174
229
|
empty bundle (chosen over a `parentSurfaceId` GROUP row post-CTO-review). Fixed latent
|
|
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-01
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/fieldsConfig/index.ts
|
|
@@ -64,6 +64,22 @@ and the `useClientFields()` hook.
|
|
|
64
64
|
`yourKey: BY_CLIENT[clientSlug] ?? DEFAULT_*`. Avoids editing all ~13 role bundles.
|
|
65
65
|
`DEFAULT_CLIENT_FIELDS` must also carry the new key (ultimate fallback).
|
|
66
66
|
|
|
67
|
+
### De-roling a client×role bundle to client-only
|
|
68
|
+
|
|
69
|
+
Some bundles were role-keyed (`{ADMIN, MANAGER}`) only for historical reasons — the roles held
|
|
70
|
+
byte-identical config. Those are being flattened to **client-only** (part of the ongoing
|
|
71
|
+
backend-driven / de-role goal). `orderViewFields` is now client-only: `COMPASS` and `COMPASSCANADA`
|
|
72
|
+
read their flat client JSON via `mergeOrderView(<client>OrderViewFields)` in
|
|
73
|
+
`src/fieldsConfig/index.ts` instead of reading `.ADMIN`/`.MANAGER` off the file, and
|
|
74
|
+
`FIELDS/COMPASSCANADA/orderViewFields.json` was flattened from `{ADMIN, MANAGER}` to a flat object.
|
|
75
|
+
|
|
76
|
+
When de-roling, watch the read path: `COMPASS`'s JSON was **already flat** but was still being read
|
|
77
|
+
via `.ADMIN` → `undefined` → the client silently fell back to `DEFAULT` and lost its
|
|
78
|
+
`salesOrderDetailsConfig` joins. This is the flat-bundle trap from the gotcha below — the fix is to
|
|
79
|
+
read the bundle itself, not a role key. `approvalViewFields` and `salesOrdersPageFields` remain
|
|
80
|
+
role-keyed (their ADMIN/MANAGER genuinely differ); fully de-roling those and removing `resolveRole`
|
|
81
|
+
is a separate pending task.
|
|
82
|
+
|
|
67
83
|
### Hydration (non-serializable bits)
|
|
68
84
|
|
|
69
85
|
Keep JSON **serializable** — strings, enums, arrays. No JSX, no functions, no Tailwind classes.
|
|
@@ -143,6 +159,12 @@ modal's chrome from the same per-client JSON, not from the component:
|
|
|
143
159
|
`vendorItemRecordViewFields` are record-modal-layout examples living under `src/layout/.../viewModel/FIELDS/`.
|
|
144
160
|
|
|
145
161
|
## Change history
|
|
162
|
+
- 2026-07-01 — De-roled `orderViewFields` from client×role to **client-only**: COMPASS/COMPASSCANADA
|
|
163
|
+
now read a flat client JSON via `mergeOrderView(...)` in `fieldsConfig/index.ts` (COMPASSCANADA's
|
|
164
|
+
JSON flattened from byte-identical `{ADMIN,MANAGER}`). Fixed a latent bug where COMPASS's
|
|
165
|
+
already-flat JSON was read via `.ADMIN` → `undefined` → silent fallback to DEFAULT (lost its
|
|
166
|
+
`salesOrderDetailsConfig` joins). `approvalViewFields`/`salesOrdersPageFields` stay role-keyed;
|
|
167
|
+
full de-role + `resolveRole` removal remains pending. Type-check clean. (apeterson)
|
|
146
168
|
- 2026-06-25 — SalesOrder action buttons render by presentation intent: `kind` (icon/text/viewLog) picks the renderer, `variant` a closed color set, `valueKey` is pure click-dispatch; a `componentRegistry` keyed by `kind` hydrates dedicated components (e.g. ViewLogButton). Added the `recordActionFields`-shape gotcha (per-persona blocks + sibling `poNumber` sub-config; walk `actionOptions` only). (apeterson)
|
|
147
169
|
- 2026-06-25 — Extended to record-modal layouts: per-mode `*ViewFields`/`*EditFields` JSON under `src/layout/.../viewModel/FIELDS/`, JSON-driven modal `header` (modalTag/labelTemplate/statusBadge) + `editItem` button gated via the Flag/Rule engine, and the flat-bundle defensive-default gotcha. (apeterson)
|
|
148
170
|
- 2026-06-23 — Documented from the `add-client-fields` skill during initial knowledge seed. (apeterson)
|
|
@@ -15,12 +15,13 @@ client: compass-canada
|
|
|
15
15
|
type: profile
|
|
16
16
|
status: active
|
|
17
17
|
updated: 2026-06-30
|
|
18
|
-
owners: [jcardinal, bala, tcox]
|
|
18
|
+
owners: [jcardinal, bala, tcox, apeterson]
|
|
19
19
|
files: []
|
|
20
20
|
related:
|
|
21
21
|
- ../compass-usa/profile.md
|
|
22
22
|
- ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
|
|
23
23
|
- ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
24
|
+
- ../../2.0/apps/_underscore/features/surface-resolver.md
|
|
24
25
|
---
|
|
25
26
|
|
|
26
27
|
## Summary
|
|
@@ -50,6 +51,12 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
50
51
|
see [Config-Driven Expedited Shipping Gating](../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md).
|
|
51
52
|
|
|
52
53
|
## Notes
|
|
54
|
+
- **Surface layer (beta):** like Quad, `Client_CompassCanada` had **no** `AclRecordScripts` dispatch
|
|
55
|
+
grant for the surfaces `meta-group` script (only `meta`, to client role 1) → `GET /v2/surfaces/meta-group`
|
|
56
|
+
403'd EZ-1 while `meta` worked. Fixed 2026-07-01 via
|
|
57
|
+
`dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql` (meta-group →
|
|
58
|
+
client role 1, id-agnostic + NOT-EXISTS-guarded, beta only). Exposes surfaces to role 1 only, vs
|
|
59
|
+
Compass USA's 1/3/4. See [Surface Resolver](../../2.0/apps/_underscore/features/surface-resolver.md).
|
|
53
60
|
- Customer language preference: `UserGlobalSettings.settingId = 2` (`en` / `fr-CA`); customer-
|
|
54
61
|
facing emails are sent in EN or FR accordingly.
|
|
55
62
|
- Assortment (product-grouping) names are served in fr-CA via the `AssortmentTranslations` sidecar
|
|
@@ -14,11 +14,12 @@ client: quad
|
|
|
14
14
|
type: profile
|
|
15
15
|
status: active
|
|
16
16
|
updated: 2026-06-30
|
|
17
|
-
owners: ["jcardinal", "bala"]
|
|
17
|
+
owners: ["jcardinal", "bala", "apeterson"]
|
|
18
18
|
files: []
|
|
19
19
|
related:
|
|
20
20
|
- ../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
21
21
|
- ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
22
|
+
- ../../2.0/apps/_underscore/features/surface-resolver.md
|
|
22
23
|
---
|
|
23
24
|
|
|
24
25
|
## Summary
|
|
@@ -48,6 +49,13 @@ Client-specific DB change-sets live in `dbchanges2/Client_Quad/`.
|
|
|
48
49
|
(`_underscore/Model/Quad/SalesOrder.php`): non-SA orders use the inline machine, `SA%` orders
|
|
49
50
|
delegate to the base `_Model_Client_SalesOrder`. Contrast Compass, which is shipped-only. See
|
|
50
51
|
[IF stage lifecycle & order status](../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
|
|
52
|
+
- **Surface layer (beta):** the surfaces `meta-group` script had **no** `AclRecordScripts` dispatch
|
|
53
|
+
grant for `Client_Quad` (only `meta` was granted, to client role 1), so `GET /v2/surfaces/meta-group`
|
|
54
|
+
403'd (EZ-1) while `meta` worked. Fixed 2026-07-01 via
|
|
55
|
+
`dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql` (grants meta-group to
|
|
56
|
+
client role 1, id-agnostic + NOT-EXISTS-guarded, beta only). Quad exposes surfaces to role 1 only,
|
|
57
|
+
vs Compass's 1/3/4 — revisit if broader roles are needed. See
|
|
58
|
+
[Surface Resolver](../../2.0/apps/_underscore/features/surface-resolver.md).
|
|
51
59
|
- Tracking data: record 318 (item-level) is currently empty for this client; their tracking
|
|
52
60
|
populates the IF/shipment level (record 317, ~98% coverage) and unit level (record 319). The
|
|
53
61
|
rebuilt views use 318 (per the Compass pattern) and will show tracking once item-level
|
|
@@ -14,40 +14,41 @@ updated: 2026-06-30
|
|
|
14
14
|
# Session: talos-pricing-platform
|
|
15
15
|
**Date:** 2026-06-30
|
|
16
16
|
**Project/Repo:** tools (1.0) + worker2 (2.0) + dbchanges2 (2.0 Team DB)
|
|
17
|
-
**Task:** Build a data-driven Talos pricing platform (Team-DB schema + tools onboarding/dashboard UI + worker2 cron automation) to replace the Excel calculator; then fix tools SSO login that was failing in production.
|
|
17
|
+
**Task:** Build a data-driven Talos pricing platform (Team-DB schema + tools onboarding/dashboard UI + worker2 cron automation) to replace the Excel calculator; then fix tools SSO login (both consumer and initiate legs) that was failing in production.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
## What WORKED
|
|
22
|
-
- **Schema (Team DB):** `dbchanges2/Team/2026-06-29a - TalosPricingTables.sql` — 9 tables
|
|
23
|
-
- **worker2 automation:** `worker2/Worker/Talos/Pricing.php` (`_Worker_Talos_Pricing`) + `worker2/Database/TalosPricingCrons.sql` — `php -l` clean.
|
|
24
|
-
- **tools UI:** nav folder + 6 pages +
|
|
25
|
-
- **SSO 500 fix:** `tools/mvc/sso/get.php` `tools_ssoFail()` — added `if (!headers_sent())` guard around `http_response_code(401)`.
|
|
26
|
-
- **SSO key sourcing:** `tools/_/app/auth.php` `
|
|
27
|
-
- **Crypto format confirmed:** `App_String::encryptWithKey/decryptWithKey` (library/app/string.php:1075-1094) = AES-256-CBC, `base64(iv . base64ciphertext)`, interoperable with 2.0 `_String`. The
|
|
28
|
-
- **
|
|
22
|
+
- **Schema (Team DB):** `dbchanges2/Team/2026-06-29a - TalosPricingTables.sql` — 9 tables, SQL-reviewed (0 critical), all 7 important review fixes applied (dtUpdated on fact tables, DECIMAL(6,4) margins, isFeature* booleans, DECIMAL(14,8) calibration factors, column comments, band-range CHECK). `dbchanges2/Team/` is the Team-DB migration folder (unqualified table names).
|
|
23
|
+
- **worker2 automation:** `worker2/Worker/Talos/Pricing.php` (`_Worker_Talos_Pricing`) + `worker2/Database/TalosPricingCrons.sql` — `php -l` clean. 3 monthly crons: ImportAwsActuals (Cost Explorer), RecomputeMargins (calibration + margin/streak/recommendation), MonthlyReport (PhpSpreadsheet xlsx + EmailTemplate). initialize() registers Team DB via `_underscore::DB_TEAM`.
|
|
24
|
+
- **tools UI:** nav folder + 6 pages + `App_Talos_Estimator` + CSS, all `php -l` clean. Onboarding get/post (live JS estimate + recommended per-band fees), pricing dashboard, usage benchmarks, cost-factors get/post.
|
|
25
|
+
- **SSO 500 fix (consumer leg):** `tools/mvc/sso/get.php` `tools_ssoFail()` — added `if (!headers_sent())` guard around `http_response_code(401)`. Root cause: `App_FrameworkIndex::render()` flushes the preloader buffer (`page.php:213`) BEFORE `body()`/`loadFile()`, so headers are already sent when any mvc page runs.
|
|
26
|
+
- **SSO key sourcing → Core.Parameters (BOTH legs):** `tools/_/app/auth.php` — `decryptHandoffValue()` (consumer) reads keys via new cached `handoffKeys()`; added public `currentApiSecret()` for the initiate leg; `const CORE_DB='db_toga2core'`. `tools/mvc/sso/initiate/get.php` now passes `'apiSecretAccessToken' => App_Auth::currentApiSecret()`. Both `php -l` clean.
|
|
27
|
+
- **Crypto format confirmed:** `App_String::encryptWithKey/decryptWithKey` (library/app/string.php:1075-1094) = AES-256-CBC, `base64(iv . base64ciphertext)`, interoperable with 2.0 `_String`. The real `?saml=` payload decoded to exactly that shape — proving SSO failure was a key/config issue, not a format bug.
|
|
28
|
+
- **"Sign in failed" root cause found + fixed:** that banner is `login/get.php` `?error=1`, set ONLY by `sso/initiate/get.php:33,38` when `App_Sso::initiate()` throws. The initiate leg still read `api_secret_access_token` from config; after it was removed from config (per earlier wrong advice), `App_Sso::initiate()` (library/app/sso.php:64-65, empty() guard) threw InvalidArgumentException → `/login?error=1`. Fixed by sourcing the secret from Core.Parameters on the initiate leg too.
|
|
29
|
+
- **Knowledge captured + pushed (4 times):** pricing platform (6 docs incl. approved ELEVATED `2.0/apps/talos/architecture.md` section), SSO fixes (`saml-sso-auth.md` + `mvc-data-access-patterns.md`), and the SSO-initiate correction (`saml-sso-auth.md` again). All PUSHED to `_main`.
|
|
29
30
|
|
|
30
31
|
## What did NOT work — DO NOT RETRY THESE
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- **
|
|
34
|
-
- **Calling `http_response_code()
|
|
35
|
-
- **Writing `[database_team]`/`[database_toga2core]`
|
|
32
|
+
- **Removing `api_secret_access_token` from `config.production.ini` `[saml]`** — broke SSO. The INITIATE leg (`sso/initiate/get.php`) reads it from config to encrypt the RelayState; empty → `App_Sso::initiate()` throws `InvalidArgumentException('...missing required parameter: apiSecretAccessToken')` → `/login?error=1` "Sign in failed". DO NOT keep the rotating secret in config; it now comes from Core.Parameters on both legs.
|
|
33
|
+
- **`App_Database::escape()`** does NOT exist — use `App_Database::sqlEscape()`.
|
|
34
|
+
- **Passing `App_Database::query(...)` inline to `buildArrayOfRows()`/`fetchOne()`** (both take `&$res`) — triggers PHP "Only variables should be passed by reference" (renders with display_errors on). Assign the query result to a variable first.
|
|
35
|
+
- **Calling `http_response_code()`/`header()`/`session_regenerate_id()` from a page loaded inside `body()`** — preloader buffer already flushed → fatals/no-ops. Guard with `headers_sent()`.
|
|
36
|
+
- **Writing `[database_team]`/`[database_toga2core]` with a guessed host** — avoided. The 2.0 Core/Team host is `production-core-cluster…us-west-2` (from config.alpha.ini `[database_prod_toga2core]`), distinct from the 1.0 prod-cluster and the 2.0 client cluster (`reader1.client.database.togahub.com` = db_true).
|
|
36
37
|
|
|
37
38
|
## Not tried yet (candidates for next session)
|
|
38
|
-
- Seeding/calibrating `TalosCostFactors` from
|
|
39
|
-
- Wiring
|
|
39
|
+
- Seeding/calibrating `TalosCostFactors` from real Langfuse aggregates (estimator ships placeholder DEFAULTS).
|
|
40
|
+
- Wiring xlsx as an email attachment — `_Email` attachment support unconfirmed (MonthlyReport writes xlsx to temp; email body has HTML table).
|
|
40
41
|
- Confirming the AWS Cost Explorer cost-allocation tag key (worker assumes `Client`).
|
|
41
|
-
- Creating the leadership `EmailTemplates` record in Client_True and replacing the placeholder `REPORT_EMAIL_TEMPLATE` UUID
|
|
42
|
-
-
|
|
42
|
+
- Creating the leadership `EmailTemplates` record in Client_True and replacing the placeholder `REPORT_EMAIL_TEMPLATE` UUID.
|
|
43
|
+
- Live end-to-end SSO verification after `[database_toga2core]` is configured + deployed.
|
|
43
44
|
|
|
44
45
|
## Current file state
|
|
45
46
|
| File | Status | Notes |
|
|
46
47
|
|------|--------|-------|
|
|
47
48
|
| dbchanges2/Team/2026-06-29a - TalosPricingTables.sql | created | 9 Talos* tables, reviewed+fixed. Apply to Team DB. |
|
|
48
|
-
| worker2/Worker/Talos/Pricing.php | created | `_Worker_Talos_Pricing`; lint clean. REPORT_EMAIL_TEMPLATE
|
|
49
|
+
| worker2/Worker/Talos/Pricing.php | created | `_Worker_Talos_Pricing`; lint clean. REPORT_EMAIL_TEMPLATE placeholder UUID. |
|
|
49
50
|
| worker2/Database/TalosPricingCrons.sql | created | 3 Core.CronJobs seeds (4th of month). Not yet inserted. |
|
|
50
|
-
| tools/_/app/nav.php | modified |
|
|
51
|
+
| tools/_/app/nav.php | modified | "Talos Pricing" folder (4 links; Cost Factors narrowed to technical personas). |
|
|
51
52
|
| tools/_/app/talos/estimator.php | created | `App_Talos_Estimator`; placeholder DEFAULT factors. |
|
|
52
53
|
| tools/mvc/talos/onboarding/get.php + post.php | created | Form + live estimate; writes TalosClients + TalosPricingBands in a txn. |
|
|
53
54
|
| tools/mvc/talos/pricing/get.php | created | Read-only margin/recommendation dashboard (db_team). |
|
|
@@ -55,21 +56,23 @@ updated: 2026-06-30
|
|
|
55
56
|
| tools/mvc/talos/factors/get.php + post.php | created | Technical-only cost-factor editor (upsert TalosCostFactors). |
|
|
56
57
|
| tools/assets/css/style.css | modified | Appended .pricing-table / .badge / form-grid / estimate-readout styles. |
|
|
57
58
|
| tools/mvc/sso/get.php | modified | headers_sent() guard in tools_ssoFail() (500→graceful 401). |
|
|
58
|
-
| tools/_/app/auth.php | modified | decryptHandoffValue()
|
|
59
|
+
| tools/_/app/auth.php | modified | decryptHandoffValue() + currentApiSecret() read tokens from Core.Parameters via db_toga2core; handoffKeys() cache; CORE_DB const. |
|
|
60
|
+
| tools/mvc/sso/initiate/get.php | modified | apiSecretAccessToken now from App_Auth::currentApiSecret() (Core.Parameters), not config. |
|
|
59
61
|
|
|
60
62
|
## Decisions made
|
|
61
|
-
- **Pricing model:** flat monthly ORG FEE (
|
|
62
|
-
- **Cost methodology:** calibrate Langfuse→AWS (monthly factor = AWS_actual / Langfuse_costCorrected; new clients use a global blended factor).
|
|
63
|
-
- **Table location:**
|
|
63
|
+
- **Pricing model:** flat monthly ORG FEE (adjustable margin lever) + per-user fee that STEPS by user-count band; band rate schedule locked at signing; on a band crossing the per-user fee auto-steps, THEN org fee is adjusted. Rejected: pure per-user banded; both-vary.
|
|
64
|
+
- **Cost methodology:** calibrate Langfuse→AWS (monthly factor = AWS_actual / Langfuse_costCorrected; new clients use a global blended factor). Always use cache-corrected Langfuse cost. Rejected: AWS-only; Langfuse-only.
|
|
65
|
+
- **Table location:** Team DB (`_underscore::DB_TEAM`). Tenant link to Core.Clients is a plain indexed column, NOT a hard FK (Team DB may be a separate instance).
|
|
64
66
|
- **Onboarding writes:** direct DB write from the 1.0 tools app (not via api2) for speed.
|
|
65
|
-
- **SSO
|
|
67
|
+
- **SSO secret source:** the rotating `API_SECRET_ACCESS_TOKEN` (+ `_PREVIOUS`) lives ONLY in `Core.Parameters`, read by BOTH SSO legs (initiate encrypts RelayState; consumer decrypts current→previous) via the `db_toga2core` connection. Config copies go stale on rotation and silently break SSO. `[saml] true_client_uuid` and the IdP URLs STAY in config. Rejected: config-stored tokens.
|
|
66
68
|
|
|
67
69
|
## Blockers
|
|
68
|
-
- **SSO login still fails until
|
|
69
|
-
-
|
|
70
|
+
- **SSO login still fails until `[database_toga2core]` is added to the tools configs AND tools is redeployed.** `App_Auth::CORE_DB='db_toga2core'` is referenced by both SSO legs, but no `[database_toga2core]` section exists in `config.production.ini` yet (developer handling creds). Template: hostname `production-core-cluster.cluster-clwbyqvxdm4q.us-west-2.rds.amazonaws.com`, username awsroot, dbname `Core`. DIAGNOSTIC: a **500 at initiate** = db_toga2core unconfigured (query against unregistered alias throws, uncaught); **/login?error=1** = empty/absent token in Core.Parameters.
|
|
71
|
+
- `Core.Parameters.API_SECRET_ACCESS_TOKEN` must be populated and match the gateway; `[saml] true_client_uuid` must remain in config (consumer leg sso/get.php:80-84 reads it).
|
|
72
|
+
- All Talos UI pages need the `[database_team]` connection (db_team → Team DB) before they render data.
|
|
70
73
|
|
|
71
74
|
## Exact next step
|
|
72
|
-
> Add
|
|
75
|
+
> Add `[database_toga2core]` (alias db_toga2core, dbname=Core, host=production-core-cluster…us-west-2) to `tools/config.production.ini` (and the dev configs you test with), confirm `Core.Parameters.API_SECRET_ACCESS_TOKEN` is set and `[saml] true_client_uuid` is still present, deploy tools, then click "Sign in with SSO" at https://tools.togatech.com — expect redirect to the IdP (not `/login?error=1`); a 500 at initiate means db_toga2core isn't wired.
|
|
73
76
|
|
|
74
77
|
---
|
|
75
78
|
_Saved by /session-save on 2026-06-30_
|
package/package.json
CHANGED