Policy Surrender — Full Lifecycle
How a Surrender Service Request actually moves through LifeX today: automatic eligibility analysis, a server-computed calculation dispatched by real product type, a genuine maker≠checker approval gate, a final recalculation, and settlement — fund redemption, a Finance payable, and the policy status change, in that order.
The one rule that governs this flow: the browser never sends a financial figure and the server never trusts one it was sent. Eligibility, Fund Value, Surrender Charge and the Net Surrender Value are all computed fresh — once at Submit as a quote, and again at Approval as the figure that actually settles — and the checker approving a request can never be the same person who submitted it.
Surrender reuses the existing after-sales Service Request framework and the existing generic Workflow engine rather than introducing either a second request type or a second approval engine — see Database & API Reference for exactly what was reused versus newly built.
The Full Lifecycle
From the maker selecting Surrender in the New Service Request screen through to the policy actually changing status — nothing here is skipped or short-circuited.
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 gate fill:#cffafe,stroke:#0e7490,color:#083344,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(["Maker selects Surrender"]):::startEnd --> B["Automatic Eligibility Check
policy status, pending claim,
conflicting request, KYC, IBAN,
min. duration"]:::process B --> C{"Mandatory checks pass?"}:::decision C -- "No" --> C1(["Blocked before submission"]):::fail C -- "Yes" --> D["Automatic Calculation
Fund Value / Cash Value / Not Applicable"]:::process D --> E["Submit — Service Request +
Surrender Transaction created atomically
Status: Submitted"]:::process E --> F["SURRENDER_MCA Workflow"]:::gate F --> G{"Checker decision"}:::decision G -- "Reject" --> G1(["Status: Rejected
no policy / fund / finance effect"]):::fail G -- "Approve" --> H{"Checker ≠ Maker?"}:::gate H -- "Same person" --> H1(["Blocked: cannot approve
own submission"]):::fail H -- "Different, authorized user" --> I["Final Recalculation
(never trusts the quoted figure)"]:::process I --> J{"Matches quote within SAR 1.00?"}:::decision J -- "Yes" --> K["Settlement"]:::process J -- "No, differs" --> L(["Status: Approved
awaiting explicit settlement"]):::fail L -. "Checker: Confirm & Settle" .-> K K --> M["Redeem fund units at current NAV
(fund-linked types only)"]:::process M --> N["Post SurrenderPayable voucher
GL 5202 / 2102"]:::process N --> O["Policy.Status → Surrendered"]:::process O --> Z(["Service Request: Completed"]):::success
SPLI_ServiceRequest_CreateSurrender) — a failure partway rolls back both rather than leaving an orphan, a real bug hit and fixed while building this.Policy.Status by itself — only the settlement step at the very end does, and only after the figure has been recalculated fresh.ServiceRequestService.ApproveSurrenderAsync separately checks the workflow transaction's own submitter and blocks a match.Eligibility Checks
Every check is a real, live read against the policy, its claims, and its customer — never a static rule. A failed mandatory check blocks Submit with the exact reason shown.
| Code | Check | Real source |
|---|---|---|
| PolicyStatus | Policy is currently Active | tblPolicy.Status |
| NoConflictingTransaction | No other Surrender already pending on this policy | SPLI_ServiceRequest_HasPendingSurrender |
| NoPendingClaim | No claim on this policy outside Rejected / Repudiated / Closed | IClaimService.GetAllAsync |
| KycVerified | Policyholder's KYC status is Verified (Individual only) | tblCustomer.KycStatus |
| IbanPresent | A bank IBAN exists for the payable's reference | tblPolicy.IBAN |
| MinimumDuration | Policy year has reached the plan's configured minimum, if any | tblProductPlan.MinSurrenderPolicyYear |
Calculation by Product Type
The engine dispatches on the policy's real ProductTypeCode. A product type with no configured cash-value rule shows Configuration Required — it never invents a rate.
| Product Type | Basis | Status today |
|---|---|---|
| Term | No cash value by design | Not Applicable |
| Whole Life | Guaranteed / Special Surrender Value % · tblProductPlan_SurrenderValue | Config Required |
| Endowment | Guaranteed / Special Surrender Value, Bonuses · tblProductPlan_SurrenderValue | Config Required |
| Savings | Units × current NAV, real fund holdings | Live |
| Unit-Linked | Units × current NAV, real fund holdings | Live |
| Retirement | Units × current NAV where fund-linked | Live |
IFundService.GetPolicyFundValueAsync — the same NAV engine every other fund-linked request type already uses, not a separate calculation.tblProductPlan_SurrenderCharge schedule for the current policy year — the same table the Illustration engine reads.Surrender Reversal
SurrenderReversal is its own, pre-existing Service Request type — explicitly out of scope for this build and left untouched.
| What it does | Guard | Effect |
|---|---|---|
| SPLI_Policy_ReverseSurrender | Policy must currently be Surrendered, else it raises an error | Flips Policy.Status back to Active — nothing else |
Reversal only ever flipped the status back, on the old assumption that forward Surrender "only flips Status, with no fund/finance side-effect." That assumption is now false: this build's Surrender settlement genuinely redeems fund units and posts a real SurrenderPayable voucher. Reversing a surrender completed through the new engine leaves the policy Active again with its fund units still at zero and the payable still sitting in Finance — a real gap, not yet closed.
Database & API Reference
What's genuinely new versus what this build reused as-is.
| Object | Kind | |
|---|---|---|
| tblPolicy_Transaction | Surrender Transaction header (TransTypeCode='Surrender', already seeded, never used until now) | Reused |
| tblProductPlan_SurrenderCharge | Declining surrender-charge schedule | Reused |
| IFundService | Fund valuation & unit redemption | Reused |
| IWorkflowService | Generic maker-checker engine | Reused |
| tblProductPlan_SurrenderValue | Whole Life / Endowment cash-value config, governed via PRICING_MCA | New |
| tblPolicy_TransactionCalculation | Full calculation snapshot per attempt (Quotation / Final) | New |
| SPLI_Fin_RecordSurrenderPayable | GL 5202 (Expense) / 2102 (Payable) | New |
| SURRENDER_MCA | Workflow definition, default stage: Operations Manager | New |
| POST /api/service-requests/surrender | Submit | New |
| POST .../surrender/approve · /reject · /settle | Maker≠checker lifecycle | New |
Known Gaps
Flagged deliberately, not discovered later.
The calculation framework and config schema (tblProductPlan_SurrenderValue) are live and governed, but no plan has rates entered yet — every calculation for these two types correctly returns "Surrender Value Configuration Required" until your actuarial team supplies real Guaranteed/Special Surrender Value percentages.
See Surrender Reversal above — reversing a settled surrender does not restore fund units or reverse the posted payable.
A real Unit-Linked policy was submitted, blocked on self-approval, approved by a different authorized user, and settled — fund units redeemed to zero, a real SurrenderPayable voucher posted, policy flipped to Surrendered only at that point.