Files
palladium/docs/Athena_TEI.md

693 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TEI Tool
The **TEI (Total Economic Impact) Tool** provides a configurable financial calculator for building client-specific business cases. It attaches to an existing Proposal or Engagement, inheriting client context (company name, industry, etc.) without redundant data entry, and exposes a REST API consumed by Streamlit calculators and LLM report-generation pipelines.
---
## Overview
Two parent objects define a TEI calculation:
- **TEIReport** (admin-configured template) — defines the field schema, analysis period, and discount rate. Created once per study type (e.g., "Amazon Connect 2026"). Multiple tools share one report.
- **TEITool** (one per client opportunity) — holds the actual values, financial summary, and version history. Inherits `name`, `description`, `owner`, `subscriber`, `proposal`, `engagement`, and `is_active` from {py:class}`core.models.BaseTool`.
The tool lifecycle is: **create → seed values → edit values → calculate → save version → export**.
---
## Data Model
| Model | Role |
|-------|------|
| `TEIReport` | Admin template: field schema + financial parameters |
| `TEIReportField` | One field definition (benefit or cost) on a report |
| `TEITool` | A specific calculation attached to a Proposal/Engagement |
| `TEIFieldValue` | Current value for one field, one year (or null for non-annual) |
| `TEIFinancialSummary` | Fixed-schema rollup (1-to-1 with a tool); written by `/calculate/` |
| `TEIVersion` | Immutable JSON snapshot of values + summary at a point in time |
**Public identifiers:** `TEIReport`, `TEITool`, and `TEIVersion` are addressed by a 12-character short UUID (`public_id`) in all API URLs. `TEIReportField` is addressed by its integer PK under its parent report's `public_id`.
---
## API Base URL and Authentication
```
/api/v1/tei/
```
All endpoints require `IsAuthenticated`. The error envelope is always:
```json
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"details": [...]
}
}
```
### Permissions
| Operation | Admin | Consultant | Viewer |
|-----------|:-----:|:----------:|:------:|
| Create/edit/delete Reports and Fields | Yes | No | No |
| Create TEITool | Yes | Yes | No |
| Edit values, trigger calculation, save version | Yes | Yes | No |
| View tools, values, summary, versions, export | Yes | Yes | Yes |
| Delete TEITool | Yes | Yes (own only) | No |
Data isolation follows the parent Proposal/Engagement — if a user cannot see the Proposal, they cannot see the attached TEI tool.
---
## Reports (admin)
Reports are read-only for consultants; admins manage them.
### List / Create
```
GET /api/v1/tei/reports/
POST /api/v1/tei/reports/
```
**Query params (GET):** `status` (`draft` | `active` | `archived`), `vendor`
**POST body:**
```json
{
"name": "Amazon Connect 2026",
"vendor": "AWS",
"version": "1.0",
"description": "Based on Forrester TEI February 2026",
"analysis_period_years": 3,
"discount_rate": "0.10",
"status": "draft"
}
```
**Response shape (list item):**
```json
{
"id": "Ab3Cd5Ef7Gh9",
"name": "Amazon Connect 2026",
"vendor": "AWS",
"version": "1.0",
"description": "...",
"analysis_period_years": 3,
"discount_rate": "0.1000",
"status": "active",
"field_count": 12,
"instance_count": 3,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}
```
### Detail / Update / Delete
```
GET /api/v1/tei/reports/{report_id}/
PUT /api/v1/tei/reports/{report_id}/
PATCH /api/v1/tei/reports/{report_id}/
DELETE /api/v1/tei/reports/{report_id}/
```
**Business rules:**
- `DELETE` is only allowed when `status = draft` and no tools exist (`instance_count = 0`).
- `analysis_period_years` cannot be changed if any tools reference this report — raises `409 MODEL_HAS_INSTANCES`.
---
## Report Fields (admin)
Fields define what the calculator collects. They are nested under their parent report.
### List / Add
```
GET /api/v1/tei/reports/{report_id}/fields/
POST /api/v1/tei/reports/{report_id}/fields/
```
**Query params (GET):** `table` (`benefits` | `costs`), `category`
**POST body:**
```json
{
"table": "benefits",
"field_key": "ai_resolution_efficiency",
"label": "AI-Driven Contact Resolution Efficiency",
"description": "Labor savings from AI-powered self-service",
"field_type": "currency",
"category": "AI Resolution",
"default_value": null,
"is_annual": true,
"risk_adjustment": "0.20",
"sort_order": 1,
"is_required": true,
"source_notes": "Forrester TEI 2026 — $64.3M over 3 years"
}
```
`field_type` choices: `currency`, `percentage`, `integer`, `decimal`, `text`
Adding a field to a report that already has tools will back-fill `TEIFieldValue` rows (null values) for every existing tool.
### Update / Delete
```
PUT /api/v1/tei/reports/{report_id}/fields/{field_id}/
PATCH /api/v1/tei/reports/{report_id}/fields/{field_id}/
DELETE /api/v1/tei/reports/{report_id}/fields/{field_id}/?confirm=true
```
**Protected fields** — once any tool has values for a field, these attributes are immutable (raises `409 PROTECTED_FIELD`):
- `field_key`
- `field_type`
- `is_annual`
Always mutable: `label`, `description`, `default_value`, `risk_adjustment`, `sort_order`, `category`, `source_notes`.
`DELETE` requires `?confirm=true` and cascades all `TEIFieldValue` rows for that field across every tool. Historical version snapshots are unaffected (they are stored as JSON).
### Reorder
```
PATCH /api/v1/tei/reports/{report_id}/fields/reorder/
```
```json
{
"field_order": [
{"id": 1, "sort_order": 1},
{"id": 2, "sort_order": 2}
]
}
```
---
## Tools
A tool is one TEI calculation attached to one Proposal or Engagement.
### List / Create
```
GET /api/v1/tei/tools/
POST /api/v1/tei/tools/
```
**Query params (GET):** `status`, `report` (report `public_id`), `proposal` (PK), `engagement` (PK)
**POST body:**
```json
{
"report": "Ab3Cd5Ef7Gh9",
"proposal": 42,
"name": null,
"status": "draft"
}
```
Supply exactly one of `proposal` or `engagement`. `name` defaults to the report name when omitted.
**Side effects on create:**
- Creates `TEIFieldValue` rows for every field in the report (populated from `default_value`, or empty string).
- For annual fields, creates one row per year (1 through `analysis_period_years`).
- Creates an empty `TEIFinancialSummary` record.
A duplicate active tool for the same report + proposal/engagement raises `409 DUPLICATE_INSTANCE`.
### Detail / Update / Delete
```
GET /api/v1/tei/tools/{tool_id}/
PUT /api/v1/tei/tools/{tool_id}/
PATCH /api/v1/tei/tools/{tool_id}/
DELETE /api/v1/tei/tools/{tool_id}/?confirm=true
```
Only `name` and `status` are mutable via `PUT`/`PATCH`. `DELETE` requires `?confirm=true` and cascades all values, versions, and summary.
**Tool detail response:**
```json
{
"id": "Xy1Za2Bc3De4",
"report": {
"id": "Ab3Cd5Ef7Gh9",
"name": "Amazon Connect 2026",
"vendor": "AWS",
"version": "1.0",
"analysis_period_years": 3,
"discount_rate": "0.1000"
},
"opportunity": {
"id": "...",
"name": "Acme Corp CX Transformation",
"proposal_id": "...",
"client": {
"id": "...",
"name": "Acme Corporation",
"short_name": "Acme"
}
},
"engagement": null,
"name": "Amazon Connect 2026",
"status": "in_progress",
"current_version": 2,
"summary": {
"net_present_value": "14200000.00",
"roi_percentage": "289.8000",
"total_benefits_pv": "19100000.00",
"total_costs_pv": "4900000.00"
},
"created_date": "2025-01-20T14:30:00Z",
"modified_date": "2025-02-03T09:15:00Z"
}
```
---
## Values
### Get all values
```
GET /api/v1/tei/tools/{tool_id}/values/
```
**Query params:** `table` (`benefits` | `costs`), `category`
Annual fields return a `years` object; non-annual fields return a flat `value`:
```json
{
"tool_id": "Xy1Za2Bc3De4",
"report": "Amazon Connect 2026",
"values": [
{
"id": 1,
"field_key": "ai_resolution_efficiency",
"label": "AI-Driven Contact Resolution Efficiency",
"table": "benefits",
"category": "AI Resolution",
"field_type": "currency",
"is_annual": true,
"risk_adjustment": "0.20",
"years": {
"1": {"value": "12500000.00", "risk_adjustment": null, "notes": ""},
"2": {"value": "24800000.00", "risk_adjustment": null, "notes": ""},
"3": {"value": "27000000.00", "risk_adjustment": "0.25", "notes": "Phase 3 risk"}
}
},
{
"id": 2,
"field_key": "legacy_termination",
"label": "Legacy Solution Termination Fees",
"table": "costs",
"category": "Migration",
"field_type": "currency",
"is_annual": false,
"risk_adjustment": null,
"value": "1200000.00",
"notes": "Confirmed by procurement"
}
]
}
```
### Bulk update
```
PUT /api/v1/tei/tools/{tool_id}/values/
```
```json
{
"values": [
{"field_key": "ai_resolution_efficiency", "year": 1, "value": "12500000.00", "risk_adjustment": null, "notes": null},
{"field_key": "ai_resolution_efficiency", "year": 2, "value": "24800000.00", "risk_adjustment": null, "notes": "60% containment by month 18"},
{"field_key": "legacy_termination", "year": null, "value": "1200000.00", "risk_adjustment": null, "notes": null}
]
}
```
- `year` must be `null` for non-annual fields, `1..N` for annual fields.
- Validation raises `400 INVALID_FIELD_KEY`, `400 INVALID_YEAR`, or `400 TYPE_MISMATCH` on bad input.
- Does **not** auto-recalculate — call `/calculate/` explicitly.
- Returns the same shape as `GET /values/`.
### Update single value
```
PATCH /api/v1/tei/tools/{tool_id}/values/{field_key}/?year=1
```
```json
{
"value": "13000000.00",
"risk_adjustment": "0.15",
"notes": "Revised after benchmarking call"
}
```
`?year` is required for annual fields; omit (or pass `year=null`) for non-annual fields.
---
## Calculation
```
POST /api/v1/tei/tools/{tool_id}/calculate/
```
No request body. Uses current stored values. Persists result in `TEIFinancialSummary` (upsert).
**Response:**
```json
{
"total_benefits_pv": "19100000.00",
"total_costs_pv": "4900000.00",
"net_present_value": "14200000.00",
"roi_percentage": "289.8000",
"payback_period_months": 8,
"total_benefits_nominal": "22300000.00",
"total_costs_nominal": "5400000.00",
"benefits_year_1": "5200000.00",
"benefits_year_2": "9800000.00",
"benefits_year_3": "7300000.00",
"costs_year_1": "3800000.00",
"costs_year_2": "900000.00",
"costs_year_3": "700000.00",
"discount_rate": "0.1000",
"calculated_at": "2025-02-03T09:15:00Z"
}
```
---
## Summary
```
GET /api/v1/tei/tools/{tool_id}/summary/
```
Returns the stored `TEIFinancialSummary` in the same shape as the `/calculate/` response. Returns `404 SUMMARY_NOT_CALCULATED` if `/calculate/` has never been called.
---
## Versions
Versions are **immutable** snapshots. They cannot be updated or deleted via the API.
### List
```
GET /api/v1/tei/tools/{tool_id}/versions/
```
Returns headline summary only (NPV + ROI) per version item.
### Save new version
```
POST /api/v1/tei/tools/{tool_id}/versions/
```
```json
{
"date": "2025-02-03",
"note": "Updated with actuals from finance team. Containment revised to 24%."
}
```
**Side effects:**
1. Auto-triggers `/calculate/` to ensure the summary is current.
2. Snapshots all current `TEIFieldValue` rows as JSON into `values_snapshot`.
3. Snapshots current `TEIFinancialSummary` as JSON into `summary_snapshot`.
4. Increments `tool.current_version`.
Returns the created version (without full snapshots — use the detail endpoint to retrieve those).
### Version detail
```
GET /api/v1/tei/tools/{tool_id}/versions/{version_number}/
```
Returns full `values_snapshot` and `summary_snapshot`.
---
## Export (LLM payload)
```
GET /api/v1/tei/tools/{tool_id}/export/
```
Returns everything needed for LLM report generation in one payload. Auto-recalculates before building the response.
**Response shape:**
```json
{
"export_date": "2025-02-03T09:30:00Z",
"client": {
"name": "Acme Corporation",
"short_name": "Acme",
"industry": "Financial Services",
"size": "enterprise"
},
"opportunity": {
"name": "CX Transformation",
"stage": "proposal"
},
"engagement": null,
"report": {
"name": "Amazon Connect 2026",
"vendor": "AWS",
"version": "1.0",
"analysis_period_years": 3,
"discount_rate": "0.10"
},
"benefits": [
{
"field_key": "ai_resolution_efficiency",
"label": "AI-Driven Contact Resolution Efficiency",
"category": "AI Resolution",
"risk_adjustment": "0.20",
"source_notes": "Forrester TEI 2026 — $64.3M over 3 years",
"years": {
"1": {"nominal": "12500000.00", "risk_adjusted": "10000000.00"},
"2": {"nominal": "24800000.00", "risk_adjusted": "19840000.00"},
"3": {"nominal": "27000000.00", "risk_adjusted": "20250000.00"}
},
"total_nominal": "64300000.00",
"total_risk_adjusted": "50090000.00",
"present_value": "41200000.00"
}
],
"costs": [
{
"field_key": "connect_licensing",
"label": "Amazon Connect Licensing & Usage",
"category": "Platform",
"source_notes": "Per-minute pricing model",
"years": {
"1": {"value": "2000000.00"},
"2": {"value": "2200000.00"},
"3": {"value": "2400000.00"}
},
"total_nominal": "6600000.00",
"present_value": "5490000.00"
}
],
"summary": {
"total_benefits_pv": "19100000.00",
"total_costs_pv": "4900000.00",
"net_present_value": "14200000.00",
"roi_percentage": "289.80",
"payback_period_months": 8
},
"versions": [
{"version_number": 2, "date": "2025-02-03", "note": "Actuals from finance team"},
{"version_number": 1, "date": "2025-01-15", "note": "Initial Forrester defaults"}
]
}
```
Non-annual cost fields appear under `"years": {"1": {"value": "..."}}` (treated as Year 1).
---
## Cross-Tool Rollup
```
GET /api/v1/tei/summary/
```
**Query params:** `status`, `vendor`, `min_npv`
Returns aggregate NPV across all calculated tools plus per-tool headline rows. Only includes tools where `/calculate/` has been run at least once.
---
## Jupyter Notebook Workflow
A typical notebook session using the TEI API:
```python
import requests
BASE = "https://athena.example.com/api/v1/tei"
HEADERS = {"Authorization": "Token <your-token>"}
TOOL_ID = "Xy1Za2Bc3De4" # TEITool public_id
# 1. Load tool metadata and client context
tool = requests.get(f"{BASE}/tools/{TOOL_ID}/", headers=HEADERS).json()
# 2. Load current values (benefits + costs)
values_resp = requests.get(f"{BASE}/tools/{TOOL_ID}/values/", headers=HEADERS).json()
values = values_resp["values"]
# 3. Update values (customize for the client)
updated_rows = [
{"field_key": "ai_resolution_efficiency", "year": 1, "value": "11000000.00"},
{"field_key": "ai_resolution_efficiency", "year": 2, "value": "23000000.00"},
{"field_key": "ai_resolution_efficiency", "year": 3, "value": "25500000.00"},
{"field_key": "legacy_termination", "year": None, "value": "950000.00"},
]
requests.put(
f"{BASE}/tools/{TOOL_ID}/values/",
headers=HEADERS,
json={"values": updated_rows},
)
# 4. Recalculate
summary = requests.post(f"{BASE}/tools/{TOOL_ID}/calculate/", headers=HEADERS).json()
print(f"NPV: {summary['net_present_value']}, ROI: {summary['roi_percentage']}%")
# 5. Save a version snapshot
requests.post(
f"{BASE}/tools/{TOOL_ID}/versions/",
headers=HEADERS,
json={"date": "2025-02-03", "note": "Notebook scenario — conservative containment"},
)
# 6. Get the full LLM-ready export
export = requests.get(f"{BASE}/tools/{TOOL_ID}/export/", headers=HEADERS).json()
# export["benefits"], export["costs"], export["summary"] are all populated
```
### Finding the tool_id
If you know the Proposal PK or Engagement PK:
```python
tools = requests.get(
f"{BASE}/tools/",
headers=HEADERS,
params={"proposal": 42, "status": "in_progress"},
).json()
tool_id = tools["results"][0]["id"]
```
If the tool doesn't exist yet, create it first:
```python
new_tool = requests.post(
f"{BASE}/tools/",
headers=HEADERS,
json={"report": "Ab3Cd5Ef7Gh9", "proposal": 42},
).json()
tool_id = new_tool["id"]
```
---
## Calculation Logic Reference
### Benefit risk adjustment
```
risk_adjusted_value = nominal_value × (1 risk_adjustment)
```
`risk_adjustment` comes from the `TEIFieldValue` instance override if set, otherwise from `TEIReportField.risk_adjustment`, otherwise 0. Costs are **never** risk-adjusted.
### Present value discounting
Each annual value is discounted to today:
```
PV = value / (1 + discount_rate) ^ year
```
where `year` is 1, 2, … N and `discount_rate` comes from the `TEIReport`. Non-annual (one-time) values are treated as Year 1.
### Summary calculations
```
total_benefits_nominal = sum of all risk-adjusted benefit values (all years)
total_costs_nominal = sum of all cost values (all years)
total_benefits_pv = Σ PV(risk_adjusted_benefit, year) for all benefit fields
total_costs_pv = Σ PV(cost, year) for all cost fields
net_present_value = total_benefits_pv total_costs_pv
roi_percentage = (net_present_value / total_costs_pv) × 100
```
`roi_percentage` is `null` when `total_costs_pv` is 0.
### Payback period
Annual values are prorated evenly across 12 months. One-time (non-annual) values land in month 1 as a lump sum. The payback month is the first month where cumulative risk-adjusted benefits ≥ cumulative costs. Returns `null` if never achieved within `analysis_period_years`.
### Edge cases
| Scenario | Behaviour |
|----------|-----------|
| Null/blank field value | Treated as 0 |
| All benefits zero | NPV = total_costs_pv, ROI = null if costs are also 0 |
| All costs zero | NPV = total_benefits_pv, ROI = null (division by zero) |
| `risk_adjustment = 1.0` | Benefit is zeroed out (fully excluded) |
| `risk_adjustment = 0.0` | Full nominal value used |
| Non-annual field | Folded into Year 1 for NPV and payback |
All arithmetic uses Python `Decimal` to avoid floating-point drift. Values are stored as strings and cast at calculation time.
---
## Error Codes
| HTTP | Code | When |
|------|------|------|
| 400 | `VALIDATION_ERROR` | Missing required field, wrong type |
| 400 | `INVALID_FIELD_KEY` | `field_key` not defined in the tool's report |
| 400 | `INVALID_YEAR` | Year missing for annual field, or present for non-annual, or out of range |
| 400 | `TYPE_MISMATCH` | Value cannot be parsed as the field's `field_type` |
| 401 | `AUTHENTICATION_REQUIRED` | Missing or invalid token |
| 403 | `PERMISSION_DENIED` | Role does not allow this operation |
| 404 | `NOT_FOUND` | Resource does not exist |
| 404 | `SUMMARY_NOT_CALCULATED` | `GET /summary/` before any `/calculate/` run |
| 409 | `DUPLICATE_INSTANCE` | Creating a second active tool for the same report + proposal/engagement |
| 409 | `FIELD_KEY_EXISTS` | `field_key` already exists within the report |
| 409 | `PROTECTED_FIELD` | Changing `field_key`, `field_type`, or `is_annual` when values exist |
| 409 | `MODEL_HAS_INSTANCES` | Deleting a report or changing `analysis_period_years` when tools exist |
| 422 | `CALCULATION_ERROR` | Calculation failed unexpectedly |
---
*TEI Tool — Athena*