Quotation · Illustration

Quotation Illustration — How the Numbers Are Built

The year-by-year Account Value, Surrender Value and Death Benefit projection shown on a Quotation's Illustration screen and its downloadable PDF — exactly how SPLI_Illustration_Generate computes every figure, and what it deliberately does not do.

Endpoint: GET /api/quotations/{id}/illustration Scenarios: Low / Medium / High Max projection: 30 policy years Scope: Quotation-stage only

The one rule that governs this engine: before computing a single number, it walks every projection year and checks that a real mortality rate, a real admin charge, and real growth-rate assumptions are all configured. If even one policy year is missing a rate, the whole illustration is blocked with the exact reason — naming the missing year and age — rather than silently falling back to a guessed or zero value.

This reproduces a real client illustration sample to the cent (Year 1 @ 3%: (54,600 − 1,387.86) × 1.03 = 54,808.50) — the formula below is not an approximation of that sample, it is that sample's formula.

01 Blocking-First

The Calculation Chain

Every year is validated before any year is computed — a missing rate in Year 17 blocks the whole illustration up front, not a silent gap discovered only when the reader scrolls that far down the table.

flowchart TD
  classDef startEnd fill:#7c3aed,stroke:#4c1d95,color:#ffffff,stroke-width:1px;
  classDef process fill:#eef0f6,stroke:#94a0c2,color:#1b2033,stroke-width:1px;
  classDef decision fill:#fdf1de,stroke:#92400e,color:#1b2033,stroke-width:1px;
  classDef success fill:#e4f7ee,stroke:#047857,color:#052e21,stroke-width:1px;
  classDef fail fill:#fde8ed,stroke:#9f1239,color:#3f0a17,stroke-width:1px;

  A(["Request Illustration for a Quotation"]):::startEnd --> B{"Low/Medium/High rates
+ DOB, gender, smoker
all configured?"}:::decision B -- "No" --> B1(["Blocked -- names exactly
what's missing"]):::fail B -- "Yes" --> C["Pre-validate EVERY policy year
(1..min(Term, 30))"]:::process C --> D{"Age band, mortality rate
& admin charge exist
for every year?"}:::decision D -- "No" --> D1(["Blocked -- names the exact
year and age missing a rate"]):::fail D -- "Yes" --> E["Compute year by year:
Mortality Premium, Admin Charge,
Allocation, compound growth ×3"]:::process E --> F["Surrender Value = Account Value
× (1 − Surrender Charge %)"]:::process F --> G(["Illustration table + chart + PDF"]):::success
Two full passes, not oneThe first pass only validates (every year's rate exists); the second pass only computes. A year 25 config gap is caught before year 1's numbers are ever built, not discovered mid-loop.
Capped at 30 yearsMaxYear = MIN(PolicyDurationYears, 30) — a real, deliberate cap regardless of how long the policy term itself is.
Single Premium is realFor a SinglePremium quotation, Contribution/Admin Charge/Allocation are nonzero only in Year 1 — but Mortality Premium and compounding continue every year, since the account still exists and still ages.
02 Exact Arithmetic

Per-Year Formula

Applied once per policy year, per rate scenario. Every input is a real configured value — nothing here is a hard-coded percentage.

LineFormulaReal source
Mortality Premium(CoverageAmount / 1000) × RatePer1000SumAssuredtblProductPlan_RatingTable, banded by age/gender/smoker
Admin ChargeYearContribution × AdminChargePct / 100tblProductPlan_ChargeSchedule (ChargeType='AdminCharge')
Investment AllocationYearContribution − Admin Charge—
Account Value(PriorAccountValue + Allocation − Mortality Premium) × (1 + Rate)Rate = Low / Medium / High, one column per scenario
Surrender Charge %configured, else 30% / 20% / 10% / 0% for years 1 / 2 / 3 / 4+tblProductPlan_SurrenderCharge
Surrender ValueAccount Value × (1 − Surrender Charge %)Same declining schedule the real Policy Surrender engine reads
Death BenefitCoverageAmountFlat every year, same across all three scenarios
03 Never Invented

Three Growth Scenarios

Low/Medium/High are per-Plan admin-configured assumptions — not a regulatory formula, not a hard-coded default.

Low
Configured %
Conservative assumption
Medium
Configured %
Central assumption
High
Configured %
Optimistic assumption
One row per PlantblProductPlan_IllustrationAssumption holds exactly one Low/Medium/High triplet per Plan — all three must be set, or the whole illustration blocks.
Not a live market feedThese are the insurer's own approved projection assumptions, set once per plan — not pulled from any market data source, and not tied to any fund's real historical return.
04 Read This Before Trusting a Number

Honest Limits

What this engine is, and what it deliberately is not.

This is a projection, not a real fund valuation

The Account Value here is a simulated compounding at an assumed rate — it never reads tblPolicy_FundUnit, tblFundUnitTransaction, or any real NAV. It has no branch on Product Type or IsFundLinked at all: the exact same simulation runs for every plan, fund-linked or not. For a policy's real current fund value (Units × today's actual NAV), see IFundService.GetPolicyFundValueAsync — the engine the Policy Surrender calculation reuses.

Quotation-stage only

The only caller is QuotationService.GenerateIllustrationAsync, reading exclusively from tblQuotation. There is no equivalent for an already-issued Policy — a policyholder cannot pull a fresh illustration against their real, current account.

05 For Engineers

API & Reference

ObjectPurpose
SPLI_Illustration_GenerateThe full engine described above
GET /api/quotations/{id}/illustrationTable + chart data, IllustrationResponse / IllustrationYearResponse
GET /api/quotations/{id}/illustration/pdfDownloadable PDF — refuses (400, real reason) rather than generating a PDF for a blocked illustration
tblProductPlan_IllustrationAssumptionLow/Medium/High rate configuration, per Plan
tblProductPlan_RatingTableAge/gender/smoker-banded mortality rate per 1000 sum assured
tblProductPlan_ChargeScheduleAdmin Charge % per policy year (ChargeType='AdminCharge')
tblProductPlan_SurrenderChargeDeclining surrender-charge schedule, same table Policy Surrender reads