# 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 "} 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*