Files
palladium/docs/Athena_TEI.md

19 KiB
Raw Permalink Blame History

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:

{
  "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:

{
  "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):

{
  "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:

{
  "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/
{
  "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:

{
  "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:

{
  "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:

{
  "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/
{
  "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
{
  "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:

{
  "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/
{
  "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:

{
  "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:

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:

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:

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