Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions actuarial-module/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM python:3.12-slim
WORKDIR /app
COPY . .
EXPOSE 8094
CMD ["python", "main.py"]
150 changes: 150 additions & 0 deletions actuarial-module/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""
Actuarial Module (Python)

Provides actuarial calculations for insurance pricing, reserving, and capital modeling.
Integrates with: Postgres, Redis, Kafka

Calculations:
- Loss ratio analysis by product line
- IBNR (Incurred But Not Reported) reserves
- Chain-ladder development factors
- Risk margin calculation (Cost of Capital method)
- Solvency capital requirement (SCR) under NAICOM RBS
"""

import json
import math
from http.server import HTTPServer, BaseHTTPRequestHandler
from datetime import datetime
from typing import Dict, List


def calculate_loss_ratio(earned_premium: float, incurred_claims: float) -> Dict:
"""Calculate loss ratio and classify profitability."""
if earned_premium == 0:
return {"error": "earned_premium cannot be zero"}

loss_ratio = incurred_claims / earned_premium
combined_ratio = loss_ratio + 0.30 # Assume 30% expense ratio

classification = "profitable"
if combined_ratio > 1.0:
classification = "unprofitable"
elif combined_ratio > 0.95:
classification = "marginal"

return {
"loss_ratio": round(loss_ratio, 4),
"expense_ratio": 0.30,
"combined_ratio": round(combined_ratio, 4),
"classification": classification,
"underwriting_result": round(earned_premium * (1 - combined_ratio), 2),
}


def calculate_ibnr(paid_claims: List[List[float]]) -> Dict:
"""Chain-ladder IBNR estimation from claims triangle."""
if not paid_claims or len(paid_claims) < 2:
return {"ibnr_estimate": 0, "method": "chain_ladder", "note": "Insufficient data"}

# Simplified chain-ladder
development_factors = []
for col in range(len(paid_claims[0]) - 1):
sum_curr = sum(row[col + 1] for row in paid_claims if col + 1 < len(row))
sum_prev = sum(row[col] for row in paid_claims if col < len(row) and col + 1 < len(row))
if sum_prev > 0:
development_factors.append(round(sum_curr / sum_prev, 4))

# Ultimate claims for most recent year
latest = paid_claims[-1][-1] if paid_claims[-1] else 0
cumulative_factor = 1.0
for f in development_factors:
cumulative_factor *= f

ultimate = latest * cumulative_factor
ibnr = ultimate - latest

return {
"ibnr_estimate": round(max(ibnr, 0), 2),
"development_factors": development_factors,
"cumulative_factor": round(cumulative_factor, 4),
"ultimate_claims": round(ultimate, 2),
"method": "chain_ladder",
}


def calculate_scr(assets: float, liabilities: float, premium_volume: float) -> Dict:
"""Simplified Solvency Capital Requirement per NAICOM RBS."""
# NAICOM minimum capital: ₦3B for life, ₦3B for non-life
minimum_capital = 3_000_000_000

# Risk charges (simplified)
market_risk = assets * 0.08
underwriting_risk = premium_volume * 0.15
credit_risk = assets * 0.03
operational_risk = premium_volume * 0.05

# Diversification benefit (-20%)
gross_scr = market_risk + underwriting_risk + credit_risk + operational_risk
diversification = gross_scr * 0.20
net_scr = gross_scr - diversification

available_capital = assets - liabilities
solvency_ratio = available_capital / net_scr if net_scr > 0 else 0

return {
"scr": round(net_scr, 2),
"available_capital": round(available_capital, 2),
"solvency_ratio": round(solvency_ratio, 4),
"meets_minimum": available_capital >= minimum_capital,
"minimum_capital": minimum_capital,
"risk_breakdown": {
"market_risk": round(market_risk, 2),
"underwriting_risk": round(underwriting_risk, 2),
"credit_risk": round(credit_risk, 2),
"operational_risk": round(operational_risk, 2),
"diversification_benefit": round(-diversification, 2),
},
"status": "adequate" if solvency_ratio >= 1.5 else "warning" if solvency_ratio >= 1.0 else "breach",
}


class ActuarialHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
self._respond(200, {"status": "healthy", "service": "actuarial-module"})
elif self.path == "/api/v1/products":
self._respond(200, {"products": ["motor", "health", "life", "home", "marine", "travel"]})
else:
self._respond(404, {"error": "not found"})

def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length)) if length > 0 else {}

if self.path == "/api/v1/loss-ratio":
result = calculate_loss_ratio(body.get("earned_premium", 0), body.get("incurred_claims", 0))
self._respond(200, result)
elif self.path == "/api/v1/ibnr":
result = calculate_ibnr(body.get("claims_triangle", []))
self._respond(200, result)
elif self.path == "/api/v1/scr":
result = calculate_scr(body.get("assets", 0), body.get("liabilities", 0), body.get("premium_volume", 0))
self._respond(200, result)
else:
self._respond(404, {"error": "not found"})

def _respond(self, code: int, data):
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(data).encode())

def log_message(self, format, *args):
pass


if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 8100), ActuarialHandler)
print("Actuarial Module starting on :8100")
server.serve_forever()
13 changes: 13 additions & 0 deletions agent-commission-management/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o service .

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/service .
EXPOSE 8090
CMD ["./service"]
2 changes: 2 additions & 0 deletions agent-commission-management/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module agent-commission-management
go 1.22.0
87 changes: 87 additions & 0 deletions agent-commission-management/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

import (
"encoding/json"
"log"
"math"
"net/http"
"time"
)

// Agent Commission Management Service
// Calculates, tracks, and pays agent commissions based on tiered structures.
// Integrates with: TigerBeetle (payments), Kafka, Postgres, Redis
//
// Commission Tiers:
// - New Agent (0-6 months): 8% motor, 12% health, 10% life
// - Standard (6-24 months): 10% motor, 15% health, 12% life
// - Senior (24+ months): 12% motor, 18% health, 15% life
// - Override bonus: 2% on team production for team leads

type CommissionTier struct {
Name string
Motor float64
Health float64
Life float64
Home float64
}

var tiers = map[string]CommissionTier{
"new": {Name: "New Agent", Motor: 0.08, Health: 0.12, Life: 0.10, Home: 0.06},
"standard": {Name: "Standard", Motor: 0.10, Health: 0.15, Life: 0.12, Home: 0.08},
"senior": {Name: "Senior", Motor: 0.12, Health: 0.18, Life: 0.15, Home: 0.10},
}

func calculateCommission(premium float64, product string, tier string) float64 {
t, ok := tiers[tier]
if !ok { t = tiers["new"] }
rates := map[string]float64{"motor": t.Motor, "health": t.Health, "life": t.Life, "home": t.Home}
rate := rates[product]
if rate == 0 { rate = 0.08 }
return math.Round(premium*rate*100) / 100
}

func handleHealth(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "healthy", "service": "agent-commission-management"})
}

func handleCalculate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
AgentID string `json:"agent_id"`
Premium float64 `json:"premium"`
Product string `json:"product"`
Tier string `json:"tier"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
commission := calculateCommission(req.Premium, req.Product, req.Tier)
json.NewEncoder(w).Encode(map[string]interface{}{
"agent_id": req.AgentID, "premium": req.Premium, "product": req.Product,
"tier": req.Tier, "commission": commission, "rate": commission / req.Premium,
"payment_date": time.Now().AddDate(0, 0, 15).Format("2006-01-02"),
})
}

func handlePayoutSummary(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"period": time.Now().Format("2006-01"),
"total_payable": 12500000, "agents_due": 342, "avg_payout": 36549,
"top_earner": 285000, "pending_approval": 15,
})
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", handleHealth)
mux.HandleFunc("/api/v1/calculate", handleCalculate)
mux.HandleFunc("/api/v1/payout-summary", handlePayoutSummary)
port := ":8099"
log.Printf("Agent Commission Management starting on %s", port)
log.Fatal(http.ListenAndServe(port, mux))
}
13 changes: 13 additions & 0 deletions batch-processing-engine/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o service .

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/service .
EXPOSE 8090
CMD ["./service"]
2 changes: 2 additions & 0 deletions batch-processing-engine/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module batch-processing-engine
go 1.22.0
89 changes: 89 additions & 0 deletions batch-processing-engine/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
)

// Batch Processing Engine
// Handles large-scale async operations: bulk payments, mass notifications,
// batch KYC reviews, commission payouts, policy renewals.
// Integrates with: Kafka, Temporal, Postgres, Redis

type BatchJob struct {
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
TotalItems int `json:"total_items"`
Processed int `json:"processed"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
StartedAt time.Time `json:"started_at"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
}

var (
jobs = make(map[string]*BatchJob)
jobsMu sync.RWMutex
)

func handleHealth(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "healthy", "service": "batch-processing-engine"})
}

func handleCreateBatch(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Type string `json:"type"`
Items int `json:"items"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Items > 10000 {
http.Error(w, "Max 10,000 items per batch", http.StatusBadRequest)
return
}
job := &BatchJob{
ID: fmt.Sprintf("BATCH-%d", time.Now().UnixNano()),
Type: req.Type, Status: "processing",
TotalItems: req.Items, StartedAt: time.Now(),
}
jobsMu.Lock()
jobs[job.ID] = job
jobsMu.Unlock()

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(job)
}

func handleGetBatch(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
jobsMu.RLock()
job, ok := jobs[id]
jobsMu.RUnlock()
if !ok {
http.Error(w, "Batch not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(job)
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", handleHealth)
mux.HandleFunc("/api/v1/batch", handleCreateBatch)
mux.HandleFunc("/api/v1/batch/status", handleGetBatch)

port := ":8092"
log.Printf("Batch Processing Engine starting on %s", port)
log.Fatal(http.ListenAndServe(port, mux))
}
13 changes: 13 additions & 0 deletions claims-adjudication-engine/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o service .

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/service .
EXPOSE 8090
CMD ["./service"]
2 changes: 2 additions & 0 deletions claims-adjudication-engine/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module claims-adjudication-engine
go 1.22.0
Loading