toga-ai 1.0.227 → 1.0.229
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 +5 -0
- 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/nycdoe/INDEX.md +1 -0
- package/knowledge/clients/nycdoe/features/hold-status-sync.md +127 -0
- package/knowledge/clients/nycdoe/features/servicenow-integration.md +6 -0
- 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
|
|
@@ -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
|
@@ -5,14 +5,14 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
7
|
- **library** (Library) _(framework core)_ — 10 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
|
-
- **worker** (Worker) —
|
|
8
|
+
- **worker** (Worker) — 11 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **togadesk** (TOGa Desk) — 8 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
|
|
10
10
|
- **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
|
|
11
11
|
- **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
|
|
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
|
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
+
| [NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)](features/hold-status-sync.md) | 1.0 | DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and **stay held** — holds are SLA-bearing in both systems. | worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/send_request_item_updates.php, library/app/model/togadesk/repairorder.php, library/app/api/nycdoev2.php |
|
|
5
6
|
| [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/asnprocessor/manufacturer.php, library/app/asnprocessor/apple.php, library/app/asnprocessor/lenovo.php, library/app/asnprocessor/lexmark.php, library/app/asnprocessor/acer.php, library/app/edi.php |
|
|
6
7
|
| [New York City Department of Education](profile.md) | 1.0 | NYC DOE (New York City Department of Education) is a TOGA client whose entire integration runs in the **1.0 worker tier** (~30 cron scripts under `worker/crons/ | |
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: worker
|
|
5
|
+
project: Worker
|
|
6
|
+
client: nycdoe
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-29
|
|
10
|
+
owners: [mhammontree]
|
|
11
|
+
files:
|
|
12
|
+
- worker/crons/sync/nycdoe/send_ticket_updates.php
|
|
13
|
+
- worker/crons/sync/nycdoe/process_tickets.php
|
|
14
|
+
- worker/crons/sync/nycdoe/send_request_item_updates.php
|
|
15
|
+
- library/app/model/togadesk/repairorder.php
|
|
16
|
+
- library/app/api/nycdoev2.php
|
|
17
|
+
related:
|
|
18
|
+
- servicenow-integration.md
|
|
19
|
+
- ../profile.md
|
|
20
|
+
- ../../../1.0/apps/worker/architecture.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and
|
|
26
|
+
**stay held** — holds are SLA-bearing in both systems. This doc covers the bidirectional
|
|
27
|
+
hold-status sync for DOE Incidents (INC) and Request Items (RITM), the authoritative
|
|
28
|
+
business rule that governs it, and the TRUE-79922 fix that made hold persist. It is the
|
|
29
|
+
status-sync companion to the broader [ServiceNow / ASN integration](servicenow-integration.md)
|
|
30
|
+
doc; the underlying crons and `App_Api_NYCDOEV2` plumbing are documented there.
|
|
31
|
+
|
|
32
|
+
> DOE "tickets" are **`repair_orders`** rows (`App_Model_TogaDesk_RepairOrder`,
|
|
33
|
+
> `db_togadesk` / legacy `TOGaDeskSupport`, **clientid = 16**) — NOT the generic `tickets`
|
|
34
|
+
> table / `class.ticket.php`. All four DOE crons filter to client 16 / `NYCDOETickets`, so
|
|
35
|
+
> hold behavior here is **NYCDOE-only**; no other client is affected.
|
|
36
|
+
|
|
37
|
+
## The authoritative business rule (SME-confirmed — contact-center)
|
|
38
|
+
|
|
39
|
+
1. **A DOE hold persists until MANUALLY set to a new status.** Nothing auto-reverts a hold.
|
|
40
|
+
2. **TOGaDesk is the AUTHORITATIVE system for releasing a hold.** A manual change *in
|
|
41
|
+
ServiceNow* does NOT release a TOGaDesk hold — the outbound cron **re-asserts On Hold**
|
|
42
|
+
onto SNOW every run until the hold is cleared in TOGaDesk.
|
|
43
|
+
3. Scheduling metadata (ETA / assigned technician) is **separate from the hold STATE** — an
|
|
44
|
+
on-site visit can be scheduled weeks out (e.g. summer break) **without releasing the
|
|
45
|
+
hold**. ETA syncs *inside* the hold payload.
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
### Status model (TOGaDesk side)
|
|
50
|
+
- There are **six `HOLD_*` status constants** on `App_Model_TogaDesk_RepairOrder`. All bucket
|
|
51
|
+
to main status **`Hold`** via `App_Model_TogaDesk_RepairOrder::statusDisplayMain()`.
|
|
52
|
+
- `Hold` has **statusRank 6**, beating `Work in Progress` (rank 5) in the downgrade-guard
|
|
53
|
+
added by TRUE-76812 — so a downstream sync cannot silently demote a held order.
|
|
54
|
+
|
|
55
|
+
### Status field (ServiceNow side)
|
|
56
|
+
- The SNOW **native `state` field** (human-readable: `On Hold`, `In Progress`, `Assigned`,
|
|
57
|
+
`Resolved`, …) is the **SLA-bearing, authoritative status**. Read/write this for hold.
|
|
58
|
+
- The custom **`u_status_task`** field is a *separate concept* with a different vocabulary;
|
|
59
|
+
it can lag/disagree with `state` (observed: native `state="In Progress"` while
|
|
60
|
+
`u_status_task="Open"`). **Never use `u_status_task` for hold detection.**
|
|
61
|
+
- Numeric `state` codes seen on the wire: INC `On Hold` = **3**, `In Progress` = **2**;
|
|
62
|
+
RITM `On Hold` = **8**, the scheduled-update state = **"-14"**.
|
|
63
|
+
|
|
64
|
+
### Outbound (TOGaDesk → ServiceNow)
|
|
65
|
+
- `send_ticket_updates.php` (INC) pushes the hold as `state:3`. The "Assigned → In Progress"
|
|
66
|
+
auto-start block is **guarded** so it never pushes `state:2` when the local repair order is
|
|
67
|
+
in any `HOLD_*` status (this is what caused the self-clobber — see Change history).
|
|
68
|
+
- `send_request_item_updates.php` (RITM) pushes `state:"8"` (On Hold) and carries a
|
|
69
|
+
defensive `HOLD_*` guard on the scheduled-update path so it can't push `state:"-14"` while
|
|
70
|
+
held.
|
|
71
|
+
|
|
72
|
+
### Inbound (ServiceNow → TOGaDesk)
|
|
73
|
+
- `process_tickets.php` determines local status from the native **`$data->state`** field
|
|
74
|
+
(NOT `u_status_task`): after the `u_status_task` switch — before the completion-preserve
|
|
75
|
+
step and the `statusRank` downgrade-guard — if native `state` is `On Hold`
|
|
76
|
+
(case-insensitive) it sets `$newStatus = STATUS_HOLD`. Both inbound "Assigned-force"
|
|
77
|
+
`patchINC(state:2)` blocks are guarded against a local hold.
|
|
78
|
+
|
|
79
|
+
### RITM "ETA-in-hold"
|
|
80
|
+
- `send_request_item_updates.php` syncs `scheduled_date` (ETA) to ServiceNow **inside the
|
|
81
|
+
hold block, in the same payload as `state:"8"`**, when a held order has an ETA + assigned
|
|
82
|
+
technician. This mirrors the INC cron, which already sends `u_eta` while holding — keeping
|
|
83
|
+
scheduling metadata flowing while the hold STATE stays asserted.
|
|
84
|
+
|
|
85
|
+
## Reference facts (DOE sync debugging)
|
|
86
|
+
|
|
87
|
+
- **SNOW request/response logs:** logged to the legacy `Logs` schema, **`API` table**
|
|
88
|
+
(`App_Model_Logs_API`, `db_logs`, `_databaseNameOverride 'Logs'`). `requestPayload` /
|
|
89
|
+
`responsePayload` are queryable CHAR columns — the primary debugging avenue. Filter
|
|
90
|
+
`endpoint LIKE '%nycd3%'`; the custom DOE endpoint base is **`/api/nycd3/`**. The
|
|
91
|
+
state:3↔state:2 oscillation in TRUE-79922 was proven from this table (INC2074899,
|
|
92
|
+
INC2185183 showed `state:3` then `state:2` pushed seconds apart in one run).
|
|
93
|
+
- **`NYCDOETickets` (`App_Model_Common_NYCDOETicket`, `db_common` / legacy `Common`)** stores
|
|
94
|
+
the inbound SNOW record in `rawData`, a **`FIELDTYPE_BLOBSTORAGE`** field (S3 bucket
|
|
95
|
+
`asifiles`, key `prod/Common/NYCDOETickets/rawData/{id}`) — **NOT a queryable DB column.**
|
|
96
|
+
- **Worker prod code cannot run on a Windows dev box.** The DB/S3 data layer fetches EC2
|
|
97
|
+
instance-metadata (IMDSv2) credentials and fails off-EC2. Investigate with the toga DB MCP
|
|
98
|
+
and the `Logs.API` table instead of running the crons locally.
|
|
99
|
+
|
|
100
|
+
## Gotchas / known issues
|
|
101
|
+
|
|
102
|
+
- **The self-clobber (root cause #1 of TRUE-79922):** an unguarded "Assigned → In Progress"
|
|
103
|
+
auto-start fought the hold push — `state:3` then `state:2` pushed seconds apart, every run,
|
|
104
|
+
so SNOW never stayed On Hold. Any future auto-start/auto-status block on these crons MUST
|
|
105
|
+
short-circuit when the local RO is in a `HOLD_*` status.
|
|
106
|
+
- **The wrong-field read (root cause #2 of TRUE-79922):** reading `u_status_task` instead of
|
|
107
|
+
native `state` meant a hold set on the SNOW side never reached TOGaDesk. Hold detection is
|
|
108
|
+
on native `state` only.
|
|
109
|
+
- Holds are **never auto-released** — if a SNOW user manually changes status, the outbound
|
|
110
|
+
cron correctly re-asserts On Hold; that is intended, not a bug. Clear the hold in TOGaDesk.
|
|
111
|
+
|
|
112
|
+
## Change history
|
|
113
|
+
- 2026-06-29 — TRUE-79922: fixed DOE holds never persisting (reverted to In Progress, broke
|
|
114
|
+
SLA reporting both sides). Two coupled defects: outbound self-clobber (`send_ticket_updates.php`
|
|
115
|
+
unguarded Assigned→In-Progress pushing `state:2` over a held `state:3`) and inbound
|
|
116
|
+
wrong-field read (`process_tickets.php` read `u_status_task` instead of native `state`).
|
|
117
|
+
Guarded all Assigned-force blocks (INC outbound + both inbound) against local `HOLD_*`,
|
|
118
|
+
added native-`state`="On Hold"→`STATUS_HOLD` inbound mapping, and a RITM scheduled-update
|
|
119
|
+
`HOLD_*` guard. Built RITM ETA-in-hold sync (`scheduled_date` in the same `state:"8"`
|
|
120
|
+
payload). Recorded the SME business rule (hold persists until manual; TOGaDesk authoritative
|
|
121
|
+
for release). (mhammontree)
|
|
122
|
+
|
|
123
|
+
## Related docs
|
|
124
|
+
- [NYCDOE ServiceNow / ASN Integration](servicenow-integration.md) — full integration map,
|
|
125
|
+
crons, schedules, and `App_Api_NYCDOEV2` plumbing.
|
|
126
|
+
- [NYC DOE client profile](../profile.md)
|
|
127
|
+
- [Worker (1.0) architecture](../../../1.0/apps/worker/architecture.md)
|
|
@@ -36,6 +36,7 @@ files:
|
|
|
36
36
|
- library/app/edi.php
|
|
37
37
|
related:
|
|
38
38
|
- ../profile.md
|
|
39
|
+
- hold-status-sync.md
|
|
39
40
|
- ../../../1.0/apps/worker/architecture.md
|
|
40
41
|
---
|
|
41
42
|
|
|
@@ -230,6 +231,11 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
|
|
|
230
231
|
of the consumer query; `php -l` every touched file.
|
|
231
232
|
|
|
232
233
|
## Change history
|
|
234
|
+
- 2026-06-29 — Outbound status sync now keeps DOE holds authoritative (TRUE-79922). Hold
|
|
235
|
+
detection moved to the SNOW native `state` field (not `u_status_task`); Assigned-force
|
|
236
|
+
blocks guarded against local `HOLD_*` to stop the `state:3`↔`state:2` self-clobber; RITM
|
|
237
|
+
now syncs ETA inside the On-Hold payload. Split into dedicated
|
|
238
|
+
[hold-status-sync.md](hold-status-sync.md). (mhammontree)
|
|
233
239
|
- 2026-06-19 — Investigated (read-only) the replacement-PO-suffix duplicate-fulfillment bug: PO-scoped serial dedupe defeated by per-manufacturer suffix-format variance (`-RPL`/`-REPL`/`-RPLE`/`-N`/`_N`); confirmed live case WR260150152/V60095WK (ROs 772308+801064) and ~14 systemic pairs across Lenovo+Lexmark. Refined business rule #6; added OPEN-BUG gotcha + adjacent anomalies (numeric-PO↔WR cluster, "NA"/integer serials). No code changes. (mhammontree)
|
|
234
240
|
- 2026-06-10 — Documented the NYC DOE ServiceNow/ASN integration (ticket mirror, dual ASN ingestion, NetSuite freeze pipeline, EDI surface, dedupeKey gotchas). (mhammontree)
|
|
235
241
|
|
package/package.json
CHANGED