toga-ai 1.0.228 → 1.0.230
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/tools/INDEX.md +2 -0
- package/knowledge/1.0/apps/tools/features/mvc-data-access-patterns.md +77 -0
- package/knowledge/1.0/apps/tools/features/talos-pricing-ui.md +94 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +27 -1
- package/knowledge/2.0/apps/api2/features/surface-meta-option.md +8 -0
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +33 -0
- package/knowledge/2.0/apps/talos/architecture.md +30 -2
- package/knowledge/2.0/apps/talos/features/pricing-cogs-model.md +70 -34
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +63 -3
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/talos-pricing-automation.md +84 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/true/profile.md +3 -1
- package/package.json +1 -1
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Tools (1.0 Internal-Tools App) Architecture](architecture.md) | **Tools** is a standalone 1.0 (`App_`) application that houses many small internal tools behind simple interfaces, gated by Client_True staff persona. | tools/index.php, tools/_/app/framework.php, tools/_/app/frameworkindex.php, tools/assets/img/favicon/favicon.ico, tools/assets/img/favicon/favicon-32x32.png, tools/assets/img/favicon/favicon-16x16.png, tools/assets/img/favicon/apple-touch-icon.png, tools/_/app/auth.php, tools/_/app/nav.php, tools/common/header.php, tools/common/footer.php, tools/mvc/get.php, tools/mvc/_TEMPLATE/get.php, tools/docs/ADDING_A_TOOL.md |
|
|
6
6
|
| [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | The first two tools shipped in the Tools app, both under the **Developers** folder and gated to personas **Development Team** / **TOGa Technology**. | tools/mvc/developers/uuid/get.php, tools/mvc/developers/password/get.php |
|
|
7
|
+
| [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php |
|
|
7
8
|
| [Tools Persona-Gated Navigation (App_Nav)](features/persona-gated-navigation.md) | `App_Nav` is the Tools app's two-level, **persona-gated** navigation. | tools/_/app/nav.php, tools/mvc/get.php |
|
|
8
9
|
| [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini |
|
|
10
|
+
| [Talos Pricing UI (Onboarding, Dashboard, Benchmarks, Cost Factors + Estimator)](features/talos-pricing-ui.md) | The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder with four pages plus a client-side estimate engine. | tools/_/app/nav.php, tools/_/app/talos/estimator.php, tools/mvc/talos/onboarding/get.php, tools/mvc/talos/onboarding/post.php, tools/mvc/talos/pricing/get.php, tools/mvc/talos/benchmarks/get.php, tools/mvc/talos/factors/get.php, tools/mvc/talos/factors/post.php, tools/assets/css/style.css |
|
|
9
11
|
| [Deploying Tools to Elastic Beanstalk (PHP 8.5 / Amazon Linux 2023)](workflows/deploy-to-elastic-beanstalk-al2023.md) | How the **Tools** 1.0 app boots on Elastic Beanstalk running `PHP 8.5 on 64bit Amazon Linux 2023/4.13.1 (aarch64)`. | tools/.ebextensions/004_http_to_https.config, tools/.ebextensions/006_mount-s3fs.config, tools/.ebextensions/007_setup_export_cache_folders.config, tools/.ebextensions/008_setup_ldap.config, tools/.ebextensions/009_setup_phpini.config, tools/.ebextensions/020_setup_git_libraries.config, tools/.ebextensions/050_register_instance_to_shared_application_load_balancer.config, tools/ebs/git.json |
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tools MVC — Routing, CSRF & App_Database Access Patterns
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: tools
|
|
5
|
+
project: Tools
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-29
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- tools/_/app/nav.php
|
|
13
|
+
- tools/mvc/get.php
|
|
14
|
+
related:
|
|
15
|
+
- ./persona-gated-navigation.md
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ../../library/features/mvc-page-pattern-and-app-skeleton.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to
|
|
23
|
+
the Tools app — URL routing, CSRF, and DB access through `App_Database` — with the gotchas
|
|
24
|
+
that bite if you assume the legacy 1.0 behavior. Discovered while building the Talos Pricing
|
|
25
|
+
UI; these apply to any tools-app page.
|
|
26
|
+
|
|
27
|
+
## Routing
|
|
28
|
+
- `App_MVC::parseRoute` maps a URL to `mvc/<segments>/<method>.php`: **GET → `get.php`,
|
|
29
|
+
POST → `post.php`**.
|
|
30
|
+
- **Writes use `mvc/<route>/post.php`** — NOT the legacy
|
|
31
|
+
`App_FrameworkIndex::actionHandler` `targetAction → app/<x>.php` path. A form posts to the
|
|
32
|
+
same route as its GET page; the `post.php` handler does the write and redirects.
|
|
33
|
+
|
|
34
|
+
## Nav persona narrowing (gotcha)
|
|
35
|
+
`App_Nav::personasForRoute()` returns the **union** of folder + action personas. A page that
|
|
36
|
+
must be **narrower** than its folder (e.g. a technical-only editor inside a broader folder)
|
|
37
|
+
must `requireAuth()` the narrow persona set **explicitly** — do not pass
|
|
38
|
+
`personasForRoute()`, or the page inherits the wider folder grant. (See
|
|
39
|
+
`persona-gated-navigation` for the nav model.)
|
|
40
|
+
|
|
41
|
+
## CSRF
|
|
42
|
+
`App_Page::validateCrossSiteRequestForgery()` is **Origin/Referer-based**: for any non-GET
|
|
43
|
+
request the request host must equal `HTTP_HOST`. **No hidden token field is needed** — do not
|
|
44
|
+
add one expecting the framework to validate it.
|
|
45
|
+
|
|
46
|
+
## App_Database
|
|
47
|
+
- Query: `App_Database::query($sql, $alias)`.
|
|
48
|
+
- Row helpers: `buildArrayOfRows(&$res, $includeNumericIndexes)`, `fetchOne(&$res)`,
|
|
49
|
+
`fetchRow(&$res, true)`.
|
|
50
|
+
- Escaper is `App_Database::sqlEscape()` — **not** `::escape`. (Prefer parameterized queries
|
|
51
|
+
per the security rules; `sqlEscape()` is the framework escaper when needed.)
|
|
52
|
+
- Transactions: `beginTransaction` / `commitTransaction` / `rollbackTransaction`.
|
|
53
|
+
- **Connections** come from `config.*.ini` `[database_<x>]` → alias `db_<x>`. A page can read a
|
|
54
|
+
2.0 DB read-only through its own connection (e.g. `db_true → Client_True`); adding
|
|
55
|
+
`db_team → Team` is the same pattern.
|
|
56
|
+
|
|
57
|
+
### Gotcha — by-reference row helpers
|
|
58
|
+
`buildArrayOfRows`, `fetchOne`, and `fetchRow` take `$res` **by reference**. Assign the
|
|
59
|
+
`query()` result to a **variable first**, then pass it — passing the call result inline emits
|
|
60
|
+
PHP "Only variables should be passed by reference", which **renders to the page** when
|
|
61
|
+
`display_errors` is on.
|
|
62
|
+
|
|
63
|
+
```php
|
|
64
|
+
// WRONG — warning leaks to the page
|
|
65
|
+
$rows = App_Database::buildArrayOfRows(App_Database::query($sql, 'db_team'), false);
|
|
66
|
+
|
|
67
|
+
// CORRECT
|
|
68
|
+
$res = App_Database::query($sql, 'db_team');
|
|
69
|
+
$rows = App_Database::buildArrayOfRows($res, false);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Autoloader
|
|
73
|
+
`App_Foo_Bar` → `app/foo/bar.php`, **all lowercase** (per the library CLAUDE.md). E.g.
|
|
74
|
+
`App_Talos_Estimator` → `_/app/talos/estimator.php`.
|
|
75
|
+
|
|
76
|
+
## Change history
|
|
77
|
+
- 2026-06-29 — Documented tools MVC routing (GET→get.php / POST→post.php; writes via mvc/<route>/post.php, not the legacy actionHandler path), Origin/Referer CSRF (no token field), `App_Database` query/row/txn API + `sqlEscape()` name, the by-reference row-helper warning gotcha, the persona-narrowing gotcha, and the lowercase autoloader mapping. Discovered building the Talos Pricing UI. (jcardinal)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Talos Pricing UI (Onboarding, Dashboard, Benchmarks, Cost Factors + Estimator)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: tools
|
|
5
|
+
project: Tools
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-29
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- tools/_/app/nav.php
|
|
13
|
+
- tools/_/app/talos/estimator.php
|
|
14
|
+
- tools/mvc/talos/onboarding/get.php
|
|
15
|
+
- tools/mvc/talos/onboarding/post.php
|
|
16
|
+
- tools/mvc/talos/pricing/get.php
|
|
17
|
+
- tools/mvc/talos/benchmarks/get.php
|
|
18
|
+
- tools/mvc/talos/factors/get.php
|
|
19
|
+
- tools/mvc/talos/factors/post.php
|
|
20
|
+
- tools/assets/css/style.css
|
|
21
|
+
related:
|
|
22
|
+
- ./persona-gated-navigation.md
|
|
23
|
+
- ./mvc-data-access-patterns.md
|
|
24
|
+
- ../architecture.md
|
|
25
|
+
- ../../../2.0/apps/talos/architecture.md
|
|
26
|
+
- ../../../2.0/apps/talos/features/pricing-cogs-model.md
|
|
27
|
+
- ../../../2.0/apps/worker2/features/talos-pricing-automation.md
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Summary
|
|
31
|
+
|
|
32
|
+
The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder
|
|
33
|
+
with four pages plus a client-side estimate engine. Sales onboard a client and sign a
|
|
34
|
+
contract here; everything else is read-only or technical-only. All four pages read the
|
|
35
|
+
**Team DB** through a new `db_team` connection (`[database_team]` in `config.*.ini`; creds
|
|
36
|
+
added by the developer separately).
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
### Nav
|
|
41
|
+
`App_Nav::definition()` gains a **"Talos Pricing"** folder with 4 actions: Onboarding,
|
|
42
|
+
Pricing Dashboard, Usage Benchmarks, Cost Factors. Persona-gated like every other folder
|
|
43
|
+
(see `persona-gated-navigation`). **Cost Factors is technical-only** — because
|
|
44
|
+
`personasForRoute()` returns the *union* of folder+action personas, the Factors page must
|
|
45
|
+
**narrow** explicitly with its own `requireAuth([technical])` rather than trusting
|
|
46
|
+
`personasForRoute()`.
|
|
47
|
+
|
|
48
|
+
### Estimator (`App_Talos_Estimator`, `_/app/talos/estimator.php`)
|
|
49
|
+
Reads `TalosCostFactors` (with built-in fallback defaults if a key is missing) and produces
|
|
50
|
+
a live cost/user + monthly estimate and recommended per-band per-user fees. **Sales never
|
|
51
|
+
enters conversations-per-user** — that is the input they cannot know; it is derived from the
|
|
52
|
+
cost factors / usage data we already have. The same math drives the live JS estimate on the
|
|
53
|
+
Onboarding page.
|
|
54
|
+
|
|
55
|
+
### Onboarding (`onboarding/get.php` + `post.php`)
|
|
56
|
+
Sales enters name / slug / offering / # users / features / # apps / min-max margin band /
|
|
57
|
+
org fee / consecutive-months smoothing window. Live JS shows the estimate and recommended
|
|
58
|
+
per-band fees; fees are editable. **"Sign contract" locks the band schedule** and writes
|
|
59
|
+
`TalosClients` + `TalosPricingBands` in a single transaction. Write goes through
|
|
60
|
+
`mvc/talos/onboarding/post.php` (the App_MVC `post.php` convention — see
|
|
61
|
+
`mvc-data-access-patterns`).
|
|
62
|
+
|
|
63
|
+
### Pricing model surfaced in the UI
|
|
64
|
+
A flat monthly **org fee** (the adjustable margin lever) PLUS a **per-user fee that steps by
|
|
65
|
+
user-count band**. The band rate schedule is locked at signing; when user count crosses a
|
|
66
|
+
band the per-user fee auto-steps to the pre-agreed rate, then the org fee is adjusted to
|
|
67
|
+
restore margin. The per-user fee never changes outside the agreed schedule. (Full
|
|
68
|
+
methodology + COGS economics in the talos `pricing-cogs-model` doc.)
|
|
69
|
+
|
|
70
|
+
### Service offerings + cost multipliers
|
|
71
|
+
Three offerings with cost multipliers stored in `TalosCostFactors`: **CHAT = 1.0** (base),
|
|
72
|
+
**VOICE_TO_VOICE = 0.25**, **NATURAL_VOICE = 0.50**.
|
|
73
|
+
|
|
74
|
+
### Read-only pages
|
|
75
|
+
- **Pricing Dashboard** (`pricing/get.php`) — read-only per-client margin/recommendation view.
|
|
76
|
+
- **Usage Benchmarks** (`benchmarks/get.php`) — read-only Langfuse-derived benchmarks.
|
|
77
|
+
- **Cost Factors** (`factors/get.php` + `post.php`) — technical-only editor for the
|
|
78
|
+
`TalosCostFactors` key/value constants (the "technical tab").
|
|
79
|
+
|
|
80
|
+
## Gotchas
|
|
81
|
+
- **`db_team` is read-only on the dashboards** but read/write on onboarding + factors. Add
|
|
82
|
+
the `[database_team]` config section (alias `db_team`) — same pattern as `db_true → Client_True`.
|
|
83
|
+
- **Voice multiplier wording is ambiguous** — "1/4 / 1/2 the cost of chat" is flagged; full
|
|
84
|
+
voice modeling is deferred. Treat 0.25 / 0.50 as placeholders.
|
|
85
|
+
- **Narrowing personas:** do not use `personasForRoute()` for a page stricter than its folder
|
|
86
|
+
(Cost Factors). See `mvc-data-access-patterns`.
|
|
87
|
+
|
|
88
|
+
## Security
|
|
89
|
+
- `config.*.ini` already carries committed plaintext secrets (team-accepted, see architecture
|
|
90
|
+
Known issues), including a reused plaintext DB password across `config.production.ini`
|
|
91
|
+
sections — flagged for rotation. Location only; no values recorded. Do not add more secrets.
|
|
92
|
+
|
|
93
|
+
## Change history
|
|
94
|
+
- 2026-06-29 — Built the Talos Pricing UI: nav folder + 4 pages (Onboarding w/ live JS estimate and contract-signing that locks the band schedule into `TalosClients`+`TalosPricingBands` in one txn; read-only Pricing Dashboard + Usage Benchmarks; technical-only Cost Factors editor) and `App_Talos_Estimator` (reads `TalosCostFactors`, derives conversations/user so sales need not enter it). Reads Team DB via new `db_team` connection. Offerings CHAT/VOICE_TO_VOICE/NATURAL_VOICE = 1.0/0.25/0.50 (voice ambiguous, deferred). (jcardinal)
|
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
| [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 |
|
|
13
13
|
| [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 |
|
|
14
14
|
| [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 |
|
|
15
|
-
| [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-29b - SurfaceMetaPublicReadAcl.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 |
|
|
15
|
+
| [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-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 |
|
|
16
16
|
| [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 |
|
|
17
17
|
| [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 | |
|
|
@@ -11,6 +11,8 @@ owners: [jcardinal]
|
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Core/Surface.php
|
|
13
13
|
- dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
|
|
14
|
+
- dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql
|
|
15
|
+
- dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
|
|
14
16
|
- _underscore/Model/Core/SurfaceElement.php
|
|
15
17
|
- _underscore/Model/Core/Action.php
|
|
16
18
|
- _underscore/Model/Core/Vocabulary.php
|
|
@@ -88,6 +90,20 @@ resolver, not "later"). It is **not** named `debug()` — see gotchas.
|
|
|
88
90
|
|
|
89
91
|
## Gotchas
|
|
90
92
|
|
|
93
|
+
- **A Core RecordScript needs `Client.AclRecordScripts` rows per role to be invokable — record-level
|
|
94
|
+
`AclRecordPermissions` is necessary but NOT sufficient.** V2 authorizes a scripted-API call **not**
|
|
95
|
+
via `AclRecordPermissions` but via the Client-DB `AclRecordScripts` table
|
|
96
|
+
(`V2.php getRecordScriptPhpMethod: SELECT ... FROM AclRecordScripts WHERE recordScriptId=? AND
|
|
97
|
+
roleId IN(...)`). The original Surface ACL seed built the record-level `AclRecordPermissions` chain
|
|
98
|
+
(Public(1) READ on `surfaces`) but MISSED the per-role dispatch grants, so every meta call 403'd
|
|
99
|
+
(EZ-1) even though record-level READ was granted. Fix: grant the surfaces `meta` script to roles
|
|
100
|
+
Public(1)/SuperUser(3)/Base(4) and the `debug` (metaDebug) script to SuperUser(3) — mirrors the
|
|
101
|
+
existing TableView/Page `meta` scripts. The grant migration is **id-agnostic** (resolves
|
|
102
|
+
`recordScriptId` by route join, so it works on the renumbered 333 block or the originally-provisioned
|
|
103
|
+
2300 block) and **NOT EXISTS-guarded** (re-runnable). ⚠ Provisioning risk: a `BLANK_CLIENT_DATABASE`
|
|
104
|
+
snapshot may not carry these `AclRecordScripts` rows, so a newly-cut client could repeat the 403
|
|
105
|
+
unless the dated migrations replay on provision — confirm BLANK-snapshot vs migration-replay before
|
|
106
|
+
the next client is cut.
|
|
91
107
|
- **`_Model_Core_Page::meta()` (Model/Core/Page.php, ~1300 lines) is the engine being replaced** and
|
|
92
108
|
is dangerous to extend: it interpolates `LIKE '$slug'` (injection-prone), **runs INSERTs during a
|
|
93
109
|
read**, is N+1, hand-duplicates the 5-layer cascade 6× via UNION, and is uncached. The empirical
|
|
@@ -104,7 +120,11 @@ resolver, not "later"). It is **not** named `debug()` — see gotchas.
|
|
|
104
120
|
framework base `_Model` defines a NON-static `debug()` (Model.php:167) and the scripted-API
|
|
105
121
|
dispatch calls the method statically. Renamed to `metaDebug` and remapped the RecordScript
|
|
106
122
|
(`route 'debug' → phpMethod 'metaDebug'`). Vet any new scripted-API method name against the
|
|
107
|
-
`_Model` base (e.g. `debug`, and check others) before mapping it.
|
|
123
|
+
`_Model` base (e.g. `debug`, and check others) before mapping it. **Already-provisioned envs are a
|
|
124
|
+
latent fatal:** the rename landed in code + the renumbered seed, but envs provisioned before it
|
|
125
|
+
(dev.sandbox) keep the old `phpMethod='debug'` and would fatal if `GET /v2/surfaces/debug` were hit.
|
|
126
|
+
Ship an id-agnostic idempotent `UPDATE` (`SurfaceDebugPhpMethodFix.sql`) to repoint the existing
|
|
127
|
+
RecordScript on those envs.
|
|
108
128
|
- **The endpoint is `GET /v2/surfaces/meta?slug=<slug>`, not `/v2/surfaces/<slug>/meta`.** TOGA
|
|
109
129
|
RecordScripts read their args from the **query string**; the path form makes the engine parse the
|
|
110
130
|
slug as a record uuid and return 404 EV-6. Pattern for any RecordScript:
|
|
@@ -118,6 +138,12 @@ resolver, not "later"). It is **not** named `debug()` — see gotchas.
|
|
|
118
138
|
admin-CRUD sibling records stay Super-User-only (the resolver reads their tables server-side).
|
|
119
139
|
|
|
120
140
|
## Change history
|
|
141
|
+
- 2026-06-29 — Deploy fixes (round 2): grant the surfaces `meta`/`debug` RecordScripts in
|
|
142
|
+
`Client.AclRecordScripts` per role (Public/SuperUser/Base) — record-level `AclRecordPermissions`
|
|
143
|
+
alone left every meta call 403 EZ-1 because V2 dispatch authorizes via `AclRecordScripts`
|
|
144
|
+
(`SurfaceRecordScriptAcl.sql`, id-agnostic + NOT EXISTS-guarded). Shipped the id-agnostic idempotent
|
|
145
|
+
`SurfaceDebugPhpMethodFix.sql` to repoint the still-`debug` phpMethod on pre-rename envs (latent
|
|
146
|
+
fatal). Flagged BLANK-provision may not carry these dispatch grants. (jcardinal)
|
|
121
147
|
- 2026-06-29 — Deploy fixes: renamed the scripted inspector `debug()`→`metaDebug()` (name collided
|
|
122
148
|
with non-static `_Model::debug()`, fatal); corrected the endpoint to query-string form
|
|
123
149
|
`GET /v2/surfaces/meta?slug=` (path form 404'd EV-6); granted CORE Public(1) READ on the `surfaces`
|
|
@@ -50,8 +50,16 @@ traffic), behavior is **byte-for-byte unchanged** — pure data, zero extra work
|
|
|
50
50
|
them. Grant Public(1) `allowRead` on `surfaces` only (the admin-CRUD sibling records stay
|
|
51
51
|
Super-User-only — the resolver reads their tables server-side). See
|
|
52
52
|
[surface-resolver](../../_underscore/features/surface-resolver.md) gotchas for the full chain.
|
|
53
|
+
- **Record-level READ is necessary but NOT sufficient — dispatch is authorized via
|
|
54
|
+
`Client.AclRecordScripts`.** V2 (`getRecordScriptPhpMethod`) checks `AclRecordScripts` by
|
|
55
|
+
`recordScriptId` + `roleId`, not `AclRecordPermissions`. Even with Public(1) READ on `surfaces`,
|
|
56
|
+
meta 403'd until the `meta`/`debug` scripts were granted per role in `AclRecordScripts`. See
|
|
57
|
+
[surface-resolver](../../_underscore/features/surface-resolver.md) gotchas.
|
|
53
58
|
|
|
54
59
|
## Change history
|
|
60
|
+
- 2026-06-29 — Documented the second ACL layer: scripted-API dispatch is authorized via
|
|
61
|
+
`Client.AclRecordScripts` (per `recordScriptId`+`roleId`), not record `AclRecordPermissions` —
|
|
62
|
+
record READ alone still 403'd meta until the dispatch grants were seeded. (jcardinal)
|
|
55
63
|
- 2026-06-29 — Documented the meta-endpoint ACL: normal app users (CORE Public role 1) need
|
|
56
64
|
`allowRead` on the `surfaces` record or meta returns 403 EZ-1. (jcardinal)
|
|
57
65
|
- 2026-06-25 — Added the `surface=<slug>` opt-in option to the V2 engine; attaches per-record action
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
|
|
6
|
-
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql |
|
|
6
|
+
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql |
|
|
7
7
|
| [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | How to manually stand up a new 2.0 client (tenant). | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
|
|
@@ -14,6 +14,10 @@ files:
|
|
|
14
14
|
- dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql
|
|
15
15
|
- dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql
|
|
16
16
|
- dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
|
|
17
|
+
- dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql
|
|
18
|
+
- dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
|
|
19
|
+
- dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql
|
|
20
|
+
- dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql
|
|
17
21
|
- dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
|
|
18
22
|
- dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql
|
|
19
23
|
- dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql
|
|
@@ -66,6 +70,24 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
|
|
|
66
70
|
- `Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql` — Core seed + the `RecordScripts` resolve row.
|
|
67
71
|
- `Core/2026-06-29a - ItemsSurfaceSeed.sql` — the Items LIST screen seed: surfaces `items-list` (TABLE → existing `items` TableView id 9, recordId 21) + `items-list-actions` (BUTTON_BAR: refresh/columns/newItem) + Actions + ~11 Messages. Single DEFAULT bundle, no Client overrides.
|
|
68
72
|
- `Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql` — grants CORE role Public(1) `allowRead` on the `surfaces` record (so app users stop getting 403 EZ-1 on meta). **Id-agnostic**: matches the record by `route='surfaces'`, so it works whether the record id is 333 or the legacy 2300.
|
|
73
|
+
- `Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql` — id-agnostic idempotent `UPDATE` repointing the
|
|
74
|
+
surfaces `debug` RecordScript `phpMethod` from `debug`→`metaDebug` on already-provisioned envs (the
|
|
75
|
+
rename only landed in the renumbered seed; pre-rename envs would fatal on `GET /surfaces/debug`).
|
|
76
|
+
- `Core/2026-06-29d - VendorItemsSurfaceSeed.sql` — the VendorItems LIST screen seed: `vendor-items-list`
|
|
77
|
+
(TABLE → existing `vendor-items` TableView id 10, recordId 19) + `vendor-items-list-actions`
|
|
78
|
+
(BUTTON_BAR: Refresh / Columns[hidden+disabled] / New Vendor Item) + 2 Actions + 11 Messages.
|
|
79
|
+
Single DEFAULT bundle, mirrors Items. (appId supply=1.)
|
|
80
|
+
- `Core/2026-06-29e - InventorySurfaceSeed.sql` — the Inventory LIST screen, **presentation-only**:
|
|
81
|
+
`inventory-list` (TABLE, **recordId NULL + tableViewId NULL** — the page composes
|
|
82
|
+
purchase-orders/items/units via per-level fetchSlugs and has no single backing Record; carries
|
|
83
|
+
`groupByLabels` + `entityTags` in surface `config`) + `inventory-list-actions` (BUTTON_BAR:
|
|
84
|
+
Group by / Refresh). Actions anchored to `items(21)` because `Actions.recordId` is NOT NULL.
|
|
85
|
+
- `Client/2026-06-29c - SurfaceRecordScriptAcl.sql` — grants the surfaces `meta`/`debug` RecordScripts
|
|
86
|
+
in **`Client.AclRecordScripts`** per role (meta → Public(1)/SuperUser(3)/Base(4); debug →
|
|
87
|
+
SuperUser(3)). V2 authorizes scripted-API dispatch via `AclRecordScripts`, NOT record
|
|
88
|
+
`AclRecordPermissions`, so record READ alone left meta 403 EZ-1. Id-agnostic (resolves
|
|
89
|
+
`recordScriptId` by route join) + NOT EXISTS-guarded (re-runnable). See
|
|
90
|
+
[surface-resolver](../../_underscore/features/surface-resolver.md).
|
|
69
91
|
- `Client/2026-06-25a - SurfaceClientTables.sql` — the 3 Client tables + `INSERT IGNORE Languages('en','English')`.
|
|
70
92
|
- `Client/2026-06-25b - SurfaceClientSeed.sql` — `ThemeTokens` + `MessageTranslations(en)`.
|
|
71
93
|
- `Client/2026-06-25c - SurfaceClientAcl.sql` — full ACL chain for the 3 CLIENT-aclDatabase records (recordId refs updated to the 333-block renumber).
|
|
@@ -80,6 +102,12 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
|
|
|
80
102
|
or every newly provisioned client is born broken. This was appended and provisioning-tested.
|
|
81
103
|
Treat it as a CI gate (provision a throwaway client; assert the surface meta endpoint returns the
|
|
82
104
|
seed).
|
|
105
|
+
- **⚠ Unconfirmed: BLANK-provision may not carry the `AclRecordScripts` dispatch grants.** The
|
|
106
|
+
surface meta endpoint is authorized by `Client.AclRecordScripts` rows (see
|
|
107
|
+
[surface-resolver](../../_underscore/features/surface-resolver.md)), seeded by the dated
|
|
108
|
+
`SurfaceRecordScriptAcl.sql`. If a new client is cut from a BLANK snapshot rather than by replaying
|
|
109
|
+
the dated migrations, it could be born with no dispatch grants and repeat the 403 EZ-1. Confirm how
|
|
110
|
+
BLANK-snapshot vs migration-replay provisioning works before the next client is cut.
|
|
83
111
|
- **`SurfaceOverrides.value` is a stringly-typed escape hatch** — a long `CONFIG`/label override
|
|
84
112
|
would silently truncate at varchar(255); that is why `c_longValue mediumtext` exists, and the
|
|
85
113
|
resolver MUST cast strictly per `attribute` (bool/int/string/json) with a logged fallback.
|
|
@@ -95,6 +123,11 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
|
|
|
95
123
|
full 4-step ACL chain + field permissions in **each** client DB; resolve `roleId` by subselect.
|
|
96
124
|
|
|
97
125
|
## Change history
|
|
126
|
+
- 2026-06-29 — Added VendorItems + Inventory LIST seeds (`VendorItemsSurfaceSeed.sql` recordId 19 /
|
|
127
|
+
TableView 10; `InventorySurfaceSeed.sql` presentation-only, recordId+tableViewId NULL, Actions
|
|
128
|
+
anchored to items(21)). Added `SurfaceRecordScriptAcl.sql` (per-role `AclRecordScripts` dispatch
|
|
129
|
+
grants — record READ alone 403'd) and `SurfaceDebugPhpMethodFix.sql` (repoint `debug`→`metaDebug`
|
|
130
|
+
on pre-rename envs). Flagged BLANK-provision may miss the `AclRecordScripts` grants. (jcardinal)
|
|
98
131
|
- 2026-06-29 — Renumbered the reserved seed id blocks (Records 2300-2308→**333–341**, RecordFields
|
|
99
132
|
2400+→**2246–2433**) in the seed files only — known file-vs-DB drift in provisioned envs. Added
|
|
100
133
|
the Items LIST seed (`ItemsSurfaceSeed.sql`) and the id-agnostic CORE Public(1) READ ACL grant on
|
|
@@ -6,8 +6,8 @@ project: TOGa IQ
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
10
|
-
owners: [akhokhani]
|
|
9
|
+
updated: 2026-06-29
|
|
10
|
+
owners: [akhokhani, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- talos/libs/aegra-api/src/aegra_api/main.py
|
|
13
13
|
- talos/libs/aegra-api/src/aegra_api/settings.py
|
|
@@ -28,6 +28,9 @@ related:
|
|
|
28
28
|
- features/mcp-servers.md
|
|
29
29
|
- features/observability.md
|
|
30
30
|
- features/deployment.md
|
|
31
|
+
- features/pricing-cogs-model.md
|
|
32
|
+
- ../worker2/features/talos-pricing-automation.md
|
|
33
|
+
- ../../1.0/apps/tools/features/talos-pricing-ui.md
|
|
31
34
|
---
|
|
32
35
|
|
|
33
36
|
## Summary
|
|
@@ -152,6 +155,31 @@ lives (env var name, config key) but never the value itself.
|
|
|
152
155
|
user-managed** (external). Health check hits `/live`. See
|
|
153
156
|
`features/deployment.md`.
|
|
154
157
|
|
|
158
|
+
## Talos Pricing Platform (cross-framework)
|
|
159
|
+
|
|
160
|
+
A database-backed pricing/margin system replacing the Excel calculator. Three tiers:
|
|
161
|
+
|
|
162
|
+
- **System of record — Team DB (9 `Talos*` tables, dbchanges2):** TalosClients (contract
|
|
163
|
+
header), TalosPricingBands (locked per-user band schedule), TalosCostFactors (editable
|
|
164
|
+
key/value cost constants — the "technical tab"), TalosUsageMonthly + TalosUsageFeatureMonthly
|
|
165
|
+
(Langfuse import targets), TalosAwsActualsMonthly (Cost Explorer), TalosClientUserCounts
|
|
166
|
+
(headcount for band-breach), TalosCalibrationMonthly + TalosFeeRecommendations (cron-derived).
|
|
167
|
+
Keyed by `clientIdentifier` (= Langfuse `org:<slug>` tag and AWS cost tag) + `periodMonth`
|
|
168
|
+
(1st-of-month DATE), UNIQUE on those for idempotent upsert. Tenant link to `Core.Clients` is a
|
|
169
|
+
plain **indexed column, not a hard FK** (Team DB may be a separate instance). Conventions:
|
|
170
|
+
id+uuid, dtCreated/dtUpdated, InnoDB, utf8mb4_0900_ai_ci.
|
|
171
|
+
- **Sales/admin UI — tools (1.0):** onboarding (locks band schedule), dashboard, benchmarks,
|
|
172
|
+
technical-only Cost Factors editor + `App_Talos_Estimator`. Reads Team DB via `db_team`.
|
|
173
|
+
- **Automation — worker2 (2.0):** monthly crons import AWS actuals, calibrate Langfuse→AWS,
|
|
174
|
+
recompute margins/recommendations, send the leadership report. Uses `_underscore::DB_TEAM`.
|
|
175
|
+
|
|
176
|
+
**Data flow:** Langfuse usage (structure) + AWS Cost Explorer (dollars) → calibrate per client →
|
|
177
|
+
margin vs. locked band → org-fee recommendation after N consecutive out-of-band months.
|
|
178
|
+
|
|
179
|
+
**Critical rules:** Team-DB fact tables are upsert-keyed on `(clientIdentifier, periodMonth
|
|
180
|
+
[, toolCategory])` — never INSERT-only. The Core tenant link is an indexed column, not an FK.
|
|
181
|
+
Always use Langfuse's cache-corrected cost (it undercounts cache badly).
|
|
182
|
+
|
|
155
183
|
## Key decisions
|
|
156
184
|
|
|
157
185
|
- **Agent Protocol over a custom shape** — clients (TOGa Hub, TOGa View, third-party)
|
|
@@ -13,20 +13,45 @@ related:
|
|
|
13
13
|
- ../architecture.md
|
|
14
14
|
- aegra-api.md
|
|
15
15
|
- observability.md
|
|
16
|
+
- ../../worker2/features/talos-pricing-automation.md
|
|
17
|
+
- ../../../1.0/apps/tools/features/talos-pricing-ui.md
|
|
16
18
|
---
|
|
17
19
|
|
|
18
20
|
## Summary
|
|
19
21
|
|
|
20
22
|
The cost basis and pricing methodology for selling Talos (TOGa IQ) chat and
|
|
21
|
-
voice deployments.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
voice deployments. This doc captures the durable knowledge: the cost drivers,
|
|
24
|
+
measured production rates, and the pricing decisions, so anyone can re-price
|
|
25
|
+
Talos without re-deriving the model.
|
|
26
|
+
|
|
27
|
+
The COGS token economics below remain valid. **The delivery mechanism has
|
|
28
|
+
pivoted** (2026-06-29) from the per-client Excel workbook (TALOS Pricing
|
|
29
|
+
Calculator v9) to a **database-backed platform**: a Team-DB system of record +
|
|
30
|
+
an onboarding/dashboard UI in the **tools** app (1.0) + **worker2** cron
|
|
31
|
+
automation that calibrates Langfuse usage to AWS actuals each month. Why: Excel
|
|
32
|
+
can't scale and forced sales to guess inputs (esp. avg conversations/user) they
|
|
33
|
+
don't know but we already have in Langfuse. See
|
|
34
|
+
[talos-pricing-ui](../../../1.0/apps/tools/features/talos-pricing-ui.md) and
|
|
35
|
+
[talos-pricing-automation](../../worker2/features/talos-pricing-automation.md);
|
|
36
|
+
a platform architecture doc captures the cross-framework topology.
|
|
37
|
+
|
|
38
|
+
Treat the numbers below as the **April 2026 baseline**; re-measure before relying
|
|
39
|
+
on them in a later period.
|
|
40
|
+
|
|
41
|
+
## Cost methodology — calibrate Langfuse to AWS (decision, 2026-06-29)
|
|
42
|
+
|
|
43
|
+
Langfuse provides the **structure** (granular per-feature / per-user breakdown);
|
|
44
|
+
AWS provides the **absolute dollars**. Each month, per client:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
calibration_factor = AWS_actual / Langfuse_costCorrected
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Apply the factor to Langfuse's granular figures so structure comes from Langfuse,
|
|
51
|
+
dollars from AWS. New clients (no AWS history) use a **global blended factor**
|
|
52
|
+
(`SUM(aws) / SUM(langfuse)` across clients) until they have their own rolling
|
|
53
|
+
3-month factor. **Langfuse badly undercounts cache cost** (sample: $201 recorded
|
|
54
|
+
vs $1,088 corrected) — always use the **cache-corrected** Langfuse figure.
|
|
30
55
|
|
|
31
56
|
## Chat COGS model
|
|
32
57
|
|
|
@@ -111,31 +136,41 @@ approach. Per-minute drivers:
|
|
|
111
136
|
|
|
112
137
|
Adopted to **reduce client-facing price fluctuation while protecting margin**:
|
|
113
138
|
|
|
114
|
-
1. **Two-part price.** Client pays a **flat monthly org fee**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
1. **Two-part price.** Client pays a **flat monthly org fee** (the adjustable
|
|
140
|
+
margin lever) PLUS a **per-user fee that steps by user-count band**.
|
|
141
|
+
2. **Locked band schedule.** The per-user **band rate schedule** is fixed at
|
|
142
|
+
**contract signing**. When user count crosses a band, the per-user fee
|
|
143
|
+
auto-steps to the **pre-agreed band rate**; then the **org fee** is adjusted
|
|
144
|
+
to restore margin. The per-user fee **never changes outside the agreed band
|
|
145
|
+
schedule**.
|
|
146
|
+
3. **Smoothing rule.** Actual margin is tracked monthly per client. The org fee
|
|
147
|
+
change is **only recommended after** margin sits outside the min/max band for
|
|
148
|
+
a configurable number of **consecutive** months (`consecutiveMonths`,
|
|
149
|
+
per-client; default 3).
|
|
150
|
+
|
|
151
|
+
> **Pivot note (2026-06-29):** the earlier model used a *fixed* per-user fee +
|
|
152
|
+
> flat fee solved to the band midpoint. The current model makes the per-user fee
|
|
153
|
+
> a **locked band schedule** (steps with headcount) and uses the **org fee** as
|
|
154
|
+
> the margin lever. The band schedule is stored in `TalosPricingBands`, locked at
|
|
155
|
+
> signing in the tools onboarding UI.
|
|
156
|
+
|
|
157
|
+
### Delivery (current — database platform)
|
|
158
|
+
|
|
159
|
+
The model now lives across three repos (schema home + topology in the platform
|
|
160
|
+
architecture doc):
|
|
161
|
+
|
|
162
|
+
- **Team DB (9 `Talos*` tables)** — `TalosClients`, `TalosPricingBands`,
|
|
163
|
+
`TalosCostFactors`, `TalosUsageMonthly` + `TalosUsageFeatureMonthly`,
|
|
164
|
+
`TalosAwsActualsMonthly`, `TalosClientUserCounts`, `TalosCalibrationMonthly`,
|
|
165
|
+
`TalosFeeRecommendations`. (`dbchanges2/Team/...TalosPricingTables.sql`)
|
|
166
|
+
- **tools app (1.0)** — onboarding (locks the band schedule), pricing dashboard,
|
|
167
|
+
usage benchmarks, technical-only Cost Factors editor + `App_Talos_Estimator`.
|
|
168
|
+
- **worker2 (2.0)** — monthly crons: import AWS actuals, recompute calibration +
|
|
169
|
+
margins/recommendations, send leadership report.
|
|
170
|
+
|
|
171
|
+
The previous **TALOS Pricing Calculator v9** Excel workbook (Sales / Voice /
|
|
172
|
+
Technical / Actuals / Engine tabs, per-client file under the shared AI drive) is
|
|
173
|
+
**superseded** by this platform but documents the same economics.
|
|
139
174
|
|
|
140
175
|
## Gotchas / known issues
|
|
141
176
|
|
|
@@ -150,4 +185,5 @@ The workbook is a **per-client** file on the shared AI drive (`P:\Work\AI\`).
|
|
|
150
185
|
under the team shared AI drive; the path can move.
|
|
151
186
|
|
|
152
187
|
## Change history
|
|
188
|
+
- 2026-06-29 — Pricing **delivery pivoted** from the Excel calculator to a DB platform (Team DB 9 `Talos*` tables + tools 1.0 UI + worker2 crons). Cost methodology recorded as **calibrate Langfuse→AWS** (per-client factor = AWS/Langfuse_corrected; global-blended fallback for new clients; always use cache-corrected Langfuse). Methodology refined: per-user fee is now a **locked band schedule** (steps with headcount) with the **org fee** as the margin lever (was fixed per-user + midpoint-solved flat fee). COGS token economics unchanged. (jcardinal)
|
|
153
189
|
- 2026-06-29 — Initial pricing/COGS model doc: chat token + feature drivers, voice per-minute drivers, two-part flat+per-user methodology with mid-band solve and consecutive-month smoothing. Captured from TALOS Pricing Calculator v9 (replaces v8). v9 verified to reproduce v8 exactly ($0.811182/conversation, $2,071.18/mo for 50 users × 20 conversations, Document Search only). History ×7 vs ×15-exchanges quirk preserved for parity and flagged for technical review. (jcardinal)
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
| [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
|
|
9
9
|
| [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
|
|
10
10
|
| [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/, toga25-supply/src/hooks/useTableCellInteractions.ts |
|
|
11
|
-
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/fieldsConfig/index.ts |
|
|
11
|
+
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
|
|
@@ -22,6 +22,11 @@ files:
|
|
|
22
22
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx
|
|
23
23
|
- toga25-supply/src/pages/Items/ItemsPage.tsx
|
|
24
24
|
- toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx
|
|
25
|
+
- toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx
|
|
26
|
+
- toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx
|
|
27
|
+
- toga25-supply/src/pages/Inventory/Inventory.tsx
|
|
28
|
+
- toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
|
|
29
|
+
- toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts
|
|
25
30
|
- toga25-supply/src/fieldsConfig/index.ts
|
|
26
31
|
related:
|
|
27
32
|
- ../../_underscore/features/surface-resolver.md
|
|
@@ -41,7 +46,11 @@ feature flag — see below). Backend: [surface-resolver](../../_underscore/featu
|
|
|
41
46
|
- **`useFetchSurfaceMeta`** — the single fetch-once/React-Query-cached choke point for a surface's
|
|
42
47
|
meta. Loading a different *record* into the same surface reuses cached meta. Builds the URL as
|
|
43
48
|
`/surfaces/meta?slug=...` (query string), **not** `/surfaces/<slug>/meta` (the path form makes the
|
|
44
|
-
engine parse the slug as a record uuid → 404 EV-6).
|
|
49
|
+
engine parse the slug as a record uuid → 404 EV-6). Unwraps the bundle via **`extractBundle()`**,
|
|
50
|
+
which locates the bundle by its **structural fingerprint** (the `elements` array) across
|
|
51
|
+
`raw` / `raw.data` / `raw.data.<route-key>` — because V2 nests a scripted-API return value one
|
|
52
|
+
level deeper, under a route-keyed slot of `data` (e.g. `data.meta`, `data.surfaces`). On a malformed
|
|
53
|
+
payload it falls back to an empty-elements bundle so the page never crashes.
|
|
45
54
|
- **`evaluateSurfaceRule`** — the Tier-1 rule evaluator: a **frozen `all/any/none` + `{field,op,value}`
|
|
46
55
|
grammar** that **throws on an unknown op** (generalized from the SalesOrders
|
|
47
56
|
`buildPatchedTenantFields`/`evaluateEnableRule`/`resolveFlag` helpers). Evaluated client-side against
|
|
@@ -61,13 +70,14 @@ legacy pre-Surface fallback branches at the wired screens removed). Legacy per-s
|
|
|
61
70
|
|
|
62
71
|
## What's wired
|
|
63
72
|
|
|
64
|
-
Login + sales-orders list + the sales-order modal + the **Items
|
|
73
|
+
Login + sales-orders list + the sales-order modal + the **Items**, **VendorItems**, and
|
|
74
|
+
**Inventory** LIST screens. The SalesOrder action
|
|
65
75
|
bar reproduces `orderViewFields.json` approve/deny/approvalWorkflow/viewLog/editOrder by delegating
|
|
66
76
|
to the existing `onActionClick`/`onOpenLog` handlers. TABLE-type surfaces stay on the existing
|
|
67
77
|
TableView pipeline via `tableViewSlug` (see [meta-driven-table-data](meta-driven-table-data.md)) —
|
|
68
78
|
the Surface layer references the TableView, never replaces it.
|
|
69
79
|
|
|
70
|
-
## Per-screen migration recipe (proven on SalesOrders + Items)
|
|
80
|
+
## Per-screen migration recipe (proven on SalesOrders + Items + VendorItems + Inventory)
|
|
71
81
|
|
|
72
82
|
The screen-by-screen full-app refactor follows one recipe:
|
|
73
83
|
1. Seed the screen's surface(s) in a `Core/*SurfaceSeed.sql` — a TABLE surface that references the
|
|
@@ -82,8 +92,51 @@ Keep TABLE rendering on the existing TableView pipeline; only the chrome (action
|
|
|
82
92
|
moves to Surface. A screen with no client/role/lang variance ships a single DEFAULT bundle (no
|
|
83
93
|
Client overrides), as Items did.
|
|
84
94
|
|
|
95
|
+
**VendorItems (screen 2)** followed the recipe exactly: `vendor-items-list` (TABLE → TableView 10,
|
|
96
|
+
recordId 19) + `vendor-items-list-actions` (Refresh / Columns[hidden+disabled] / New Vendor Item),
|
|
97
|
+
page+view-model rewired to resolve title+buttons from the bundle, dropped the
|
|
98
|
+
`useClientFields`/`vendorItemsPageFields` dependency, deleted `vendorItemsPageFields.json` + its
|
|
99
|
+
`fieldsConfig/index.ts` entry.
|
|
100
|
+
|
|
101
|
+
**Inventory (screen 3) — presentation-only, by decision.** Only the page title, header buttons
|
|
102
|
+
(Group by / Refresh), the Group-by modal copy, and the entity icon/color tag map moved to Surface.
|
|
103
|
+
The `inventoryGroupings` `groupings[].levels[]` topology
|
|
104
|
+
(`fetchSlug`/`role`/`additionalDataSlug`/`urlParamKey` driving `GenericNestedTables`) is **data-fetch
|
|
105
|
+
wiring, not presentation**, so per the config/logic boundary it **stays as JSON**. The
|
|
106
|
+
`inventory-list` surface carries `groupByLabels` + `entityTags` in its surface `config`, with
|
|
107
|
+
**recordId+tableViewId NULL** (the page has no single backing Record — it composes
|
|
108
|
+
purchase-orders/items/units via per-level fetchSlugs). Labels + `entityTags` were stripped from both
|
|
109
|
+
DEFAULT and NYCHH `inventoryGroupings.json` (groupings kept) and marked `@deprecated`/optional in the
|
|
110
|
+
`InventoryGroupingsConfig` type.
|
|
111
|
+
|
|
112
|
+
**The config/logic line for migration:** move presentation (titles, button labels, icons, colors,
|
|
113
|
+
modal copy, visibility/enable) to Surface; **leave data-fetch wiring (fetch slugs, roles, URL params,
|
|
114
|
+
nested-table topology) in JSON/code.** Not every screen reduces to a generic `SurfaceSection` swap —
|
|
115
|
+
see SalesOrders below.
|
|
116
|
+
|
|
117
|
+
## SalesOrders remaining migration — bespoke, NOT a generic SurfaceSection swap (deferred)
|
|
118
|
+
|
|
119
|
+
The SalesOrders detail migration is the plan's explicitly-deferred "blessed escape hatch"/approval
|
|
120
|
+
work — it is bespoke-component + business-logic work, not a generic swap. `SalesOrderSummaryGrid` is
|
|
121
|
+
fully tenant-data-driven through three **bespoke renderers**: `detailSection` (blox `DetailSection` +
|
|
122
|
+
custom `renderField`), `locationCard` (`OrderSectionCard`, structured contact data), and `totalsCard`
|
|
123
|
+
(blox `BaseTotals` **with client-specific recurring-lease business logic**:
|
|
124
|
+
`_totalLease !== 0 → recurringKey` fields). The generic `SurfaceSection` (label/value) cannot
|
|
125
|
+
represent `BaseTotals`/`OrderSectionCard`, and the early-seeded order-details/ship-to/bill-to/
|
|
126
|
+
order-summary SECTION surfaces do **not** cleanly map onto the grid's arbitrary client-keyed section
|
|
127
|
+
model. A faithful migration needs a **registered-renderer approach** keyed off surface element config
|
|
128
|
+
(`detailSection`/`locationCard`/`totalsCard` → their components), with the lease logic staying in
|
|
129
|
+
code/`_underscore`, plus reconciling those seeds and the Compass approval-modal state machine.
|
|
130
|
+
**Deferred to a dedicated design-first session.**
|
|
131
|
+
|
|
85
132
|
## Gotchas
|
|
86
133
|
|
|
134
|
+
- **Scripted-API GET return values land under `data.<scriptRoute>`, not `data` directly.** V2 nests a
|
|
135
|
+
scripted-API return value inside the envelope under a route-keyed slot of `data` (e.g. `data.meta`,
|
|
136
|
+
`data.surfaces`), one level deeper than a normal data fetch. The bundle hook unwraps this with
|
|
137
|
+
`extractBundle()` by structural fingerprint (the `elements` array). Consumers (`SurfaceSection`,
|
|
138
|
+
`SurfaceActionBar`) are hardened to bail to `null` unless `elements` is a real array, so a malformed
|
|
139
|
+
payload never crashes the page. (Root cause of the "s.elements is not iterable" runtime crash.)
|
|
87
140
|
- **No feature flag, no fallback.** Surface is the enforced path; a missing/broken surface bundle
|
|
88
141
|
does not silently fall back to legacy JSON — that path is deleted per screen as it migrates.
|
|
89
142
|
- **Endpoint is query-string form** — `GET /v2/surfaces/meta?slug=...`, not `/surfaces/<slug>/meta`
|
|
@@ -94,6 +147,13 @@ Client overrides), as Items did.
|
|
|
94
147
|
treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
|
|
95
148
|
|
|
96
149
|
## Change history
|
|
150
|
+
- 2026-06-29 — Migrated VendorItems (screen 2, recipe-exact) and Inventory (screen 3,
|
|
151
|
+
**presentation-only by decision** — fetch topology stays JSON; only title/buttons/modal copy/entity
|
|
152
|
+
tags moved). Fixed the "s.elements is not iterable" crash: `extractBundle()` now finds the bundle by
|
|
153
|
+
its `elements`-array fingerprint across `raw`/`raw.data`/`raw.data.<route-key>` (V2 nests
|
|
154
|
+
scripted-API returns under `data.<scriptRoute>`), with an empty-bundle fallback + hardened
|
|
155
|
+
SurfaceSection/SurfaceActionBar guards. Recorded SalesOrders detail as deferred bespoke
|
|
156
|
+
(registered-renderer + lease logic + approval-modal) work. (jcardinal)
|
|
97
157
|
- 2026-06-29 — DECISION: Surface is now enforced, not flagged — removed `VITE_SURFACE_ENABLED`/
|
|
98
158
|
`SURFACE_ENABLED` (deleted `featureFlag.ts` + all gating/legacy-fallback branches). Fixed
|
|
99
159
|
`useFetchSurfaceMeta` to the query-string endpoint `/surfaces/meta?slug=` (path form 404'd EV-6).
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
| [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
|
|
19
19
|
| [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
|
|
20
20
|
| [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
|
|
21
|
+
| [Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)](features/talos-pricing-automation.md) | The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and tools `talos-pricing-ui` docs for the other halves). | worker2/Worker/Talos/Pricing.php, worker2/Database/TalosPricingCrons.sql |
|
|
21
22
|
| [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` (file `Worker/Team/Sprint.php`) is the engine behind TOGA's internal **development-sprint process and reporting**. | worker2/Worker/Team/Sprint.php |
|
|
22
23
|
| [Teams Meeting Transcript Export](features/teams-transcript-export.md) | `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for Teams meeting transcripts produced by a set of organizers, classifies ea | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
|
|
23
24
|
| [VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)](features/vapi-webhook-handler.md) | `_Worker_Vapi` ([worker2/Worker/Vapi.php](worker2/Worker/Vapi.php)) is the **PHP side of the AI-BDR call loop** — the webhook that receives VAPI's end-of-call r | worker2/Worker/Vapi.php, worker2/Worker/Ai/Bdr/Vapi.php |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-29
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Talos/Pricing.php
|
|
13
|
+
- worker2/Database/TalosPricingCrons.sql
|
|
14
|
+
related:
|
|
15
|
+
- ./creating-worker-actions.md
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ../../talos/architecture.md
|
|
18
|
+
- ../../talos/features/pricing-cogs-model.md
|
|
19
|
+
- ../../../1.0/apps/tools/features/talos-pricing-ui.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model`
|
|
25
|
+
and tools `talos-pricing-ui` docs for the other halves). `_Worker_Talos_Pricing` is an
|
|
26
|
+
**abstract** worker class with three monthly cron actions that turn raw usage/cost data
|
|
27
|
+
into per-client margin tracking and a leadership report. All state lives in the **Team DB**
|
|
28
|
+
(9 `Talos*` tables — schema home is the platform architecture doc); the worker reads/writes
|
|
29
|
+
it via `_underscore::DB_TEAM`.
|
|
30
|
+
|
|
31
|
+
This replaces the Excel "Actuals" tab as the monthly recompute mechanism. The COGS token
|
|
32
|
+
economics are unchanged — only the delivery/automation changed.
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
`initialize()` registers the Team DB connection via `_underscore::DB_TEAM` (same pattern as
|
|
37
|
+
`_Worker_Team_Transcripts`) so every action has it. The three actions are registered as
|
|
38
|
+
`Core.CronJobs` rows (no code wiring — see `creating-worker-actions`), fired on the **4th of
|
|
39
|
+
the month**, sequenced 06:00 / 07:00 / 08:00 Central so each step's inputs are present
|
|
40
|
+
before the next runs.
|
|
41
|
+
|
|
42
|
+
### 1. ImportAwsActuals (06:00)
|
|
43
|
+
Calls AWS Cost Explorer `GetCostAndUsage` per client, filtered by `LINKED_ACCOUNT` or a
|
|
44
|
+
cost-allocation **tag** (assumed key `Client` — open item, confirm). Upserts one row per
|
|
45
|
+
`(clientIdentifier, periodMonth)` into `TalosAwsActualsMonthly`. The AWS Cost Explorer SDK
|
|
46
|
+
is vendored in `worker2/vendor`.
|
|
47
|
+
|
|
48
|
+
### 2. RecomputeMargins (07:00)
|
|
49
|
+
The core calibration step. For each client/month:
|
|
50
|
+
- **Calibration factor** = `AWS_actual / Langfuse_costCorrected` (write to
|
|
51
|
+
`TalosCalibrationMonthly`, `DECIMAL(14,8)`). Always use Langfuse's **cache-corrected**
|
|
52
|
+
cost — Langfuse badly undercounts cache cost (sample: $201 recorded vs $1,088 corrected).
|
|
53
|
+
- **New clients** (no AWS history) use a **global blended factor** = `SUM(aws) / SUM(langfuse)`
|
|
54
|
+
across all clients, until they have their own rolling-3-month factor.
|
|
55
|
+
- Apply the factor to Langfuse's granular per-feature/per-user structure so **structure comes
|
|
56
|
+
from Langfuse, absolute dollars from AWS**.
|
|
57
|
+
- Compute per-client margin vs. the client's min/max band, update the consecutive out-of-band
|
|
58
|
+
**streak**, and only emit a `TalosFeeRecommendations` row once the streak ≥ the client's
|
|
59
|
+
configured `consecutiveMonths`.
|
|
60
|
+
|
|
61
|
+
### 3. MonthlyReport (08:00)
|
|
62
|
+
Builds an `.xlsx` with **PhpSpreadsheet** (vendored at
|
|
63
|
+
`_underscore/Component/Library/PhpOffice` — first use in worker2) and sends a templated
|
|
64
|
+
leadership email via `_Model_Client_EmailTemplate::send(...)` to the `True` client,
|
|
65
|
+
rendering `{var}` placeholders through AWS SES.
|
|
66
|
+
|
|
67
|
+
## Gotchas / open items
|
|
68
|
+
- **Idempotent upsert key** — every fact table is keyed `(clientIdentifier, periodMonth)`
|
|
69
|
+
(usage-by-feature adds `toolCategory`), with a UNIQUE on those columns, so re-running a
|
|
70
|
+
cron is safe.
|
|
71
|
+
- **`REPORT_EMAIL_TEMPLATE` is a placeholder UUID** — needs a real `EmailTemplates` record in
|
|
72
|
+
`Client_True` before MonthlyReport sends.
|
|
73
|
+
- **`_Email` attachment support unconfirmed** — verify before relying on the xlsx attachment.
|
|
74
|
+
- **AWS cost-tag key assumed `Client`** — confirm against the actual cost-allocation tag.
|
|
75
|
+
- **Importer contract (other dev):** `TalosUsageMonthly` + `TalosUsageFeatureMonthly` are the
|
|
76
|
+
Langfuse → MySQL targets. Map the Langfuse org tag `org:True` → `clientIdentifier` `"True"`;
|
|
77
|
+
upsert on `(clientIdentifier, periodMonth[, toolCategory])`.
|
|
78
|
+
|
|
79
|
+
## Security
|
|
80
|
+
- Hardcoded AWS SES SMTP credentials live in `_underscore/Email.php` (pre-existing, not
|
|
81
|
+
introduced here) — flagged for rotation. Location only; no values recorded.
|
|
82
|
+
|
|
83
|
+
## Change history
|
|
84
|
+
- 2026-06-29 — Built `_Worker_Talos_Pricing` (abstract) with 3 monthly crons — ImportAwsActuals (Cost Explorer), RecomputeMargins (calibrate Langfuse→AWS, rolling-3mo + global-blended fallback, margin/streak/recommendation), MonthlyReport (PhpSpreadsheet xlsx + templated SES email to True). Registers Team DB via `_underscore::DB_TEAM`; crons are `Core.CronJobs` rows on the 4th, 06/07/08:00 CT. Replaces the Excel "Actuals" recompute. (jcardinal)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -12,12 +12,12 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
12
12
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
13
13
|
- **test** (Test) — 12 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
|
|
14
14
|
- **toga** (TOGa) — 2 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
|
|
15
|
-
- **tools** (Tools) —
|
|
15
|
+
- **tools** (Tools) — 7 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
|
|
16
16
|
|
|
17
17
|
## 2.0 framework
|
|
18
18
|
|
|
19
19
|
- **_underscore** (_Underscore) _(framework core)_ — 17 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
20
|
-
- **worker2** (Worker) —
|
|
20
|
+
- **worker2** (Worker) — 22 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
21
21
|
- **api2** (API) — 7 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
22
22
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
23
23
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
package/package.json
CHANGED