feat: add setup notebook and update env example for Athena
This commit is contained in:
692
docs/Athena_TEI.md
Normal file
692
docs/Athena_TEI.md
Normal file
@@ -0,0 +1,692 @@
|
||||
# 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*
|
||||
Reference in New Issue
Block a user