| Field | Value |
|---|---|
| Module Name | Exposure Rating |
| Version | 1.0.0 |
| Status | ✅ Implemented |
| Last Updated | December 30, 2025 |
| Author | Actuarial Development Team |
| System | Acentre Reinsurance & Actuarial System |
The Exposure Rating Module has been successfully integrated into the Acentre Reinsurance & Actuarial System. This module implements a deterministic exposure curve methodology for pricing Excess of Loss (XL) reinsurance layers, complementing the existing frequency-severity simulation approach and providing dual-method pricing capabilities.
| Feature | Status | Description |
|---|---|---|
| Core Exposure Curve Engine | ✅ Complete | Swiss Re exposure curve methodology with G(x) function |
| Rate on Line (ROL) Calculator | ✅ Complete | Poisson-weighted factor approach |
| Layer-by-Layer Calculation | ✅ Complete | Multi-layer XL pricing support |
| Class Curve Mapping | ✅ Complete | 22 default insurance class mappings |
| Interactive Curve Visualizer | ✅ Complete | Real-time Chart.js visualization |
| Results Storage & History | ✅ Complete | Full audit trail with metadata |
| Simulation Comparison | ✅ Complete | Side-by-side exposure vs simulation results |
| Excel Profile Integration | ✅ Complete | Works with Data Cleaning module outputs |
| Responsive UI | ✅ Complete | Consistent styling with Data Cleaning module |
| Layer | Technology | Version |
|---|---|---|
| Backend Framework | Laravel | 10.x |
| Frontend Framework | Vue 3 | 3.x |
| Bridge | Inertia.js | Latest |
| CSS Framework | Tailwind CSS | 3.x |
| Charts | Chart.js | 4.x |
| Math Library | MathPHP | 2.x |
| Icons | Font Awesome | 6.x |
| Date Formatting | Moment.js | 2.x |
┌─────────────────────────────────────────────────────────────────────────┐
│ EXPOSURE RATING MODULE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ FRONTEND (Vue 3) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ ExposureRating.vue (Main Page) │ │
│ │ ├── Tab Navigation (Calculate | Results | History) │ │
│ │ ├── Progress Steps Indicator │ │
│ │ ├── LayerResultsTable.vue │ │
│ │ ├── CurveVisualizer.vue │ │
│ │ ├── ClassCurveEditor.vue │ │
│ │ └── ComparisonView.vue │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ Inertia.js Bridge │
│ │ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ BACKEND (Laravel) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ ExposureRatingController.php │ │
│ │ ├── index() - Load page with scenarios, reconciliations │ │
│ │ ├── calculate() - Execute exposure rating calculation │ │
│ │ ├── getResults() - Retrieve calculation results │ │
│ │ ├── compare() - Compare with simulation results │ │
│ │ ├── getCurveData() - Get curve visualization data │ │
│ │ ├── saveCurveConfig() - Save curve parameters │ │
│ │ └── saveClassCurve() - Save class curve mappings │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ SERVICES │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ ExposureCurveService.php - Core G(x) calculations │ │
│ │ ExposureRatingCalculator.php - Layer loss calculations │ │
│ │ ROLCalculator.php - Rate on Line computation │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ MODELS │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ ExposureCurveConfig.php - Curve parameters (c4, c5, c6, c7)│ │
│ │ ClassCurveParameter.php - Class-to-curve mappings │ │
│ │ ExposureRatingResult.php - Calculation results storage │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Scenario │ │ Reconciliation │ │ Class Curve │
│ Selection │────▶│ (Excel Data) │────▶│ Mapping │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Layer Results │◀────│ Exposure │◀────│ G(x) Curve │
│ & ROL/LOL │ │ Calculator │ │ Calculation │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ XL Rates │ │ Comparison │
│ Update │────▶│ with Sim │
└─────────────────┘ └─────────────────┘
The exposure rating approach uses the Swiss Re Exposure Curve methodology:
G(x) = MAX(MIN(LN((((g-1)*b)+((1-b*g)*b^x))/(1-b))/LN(b*g), 1), 0)
Where:
- x = Damage ratio (Layer boundary / Sum Insured), bounded [0, 1]
- b = exp(C4 + C5 * c * (1 + c))
- g = exp((C6 + C7 * c) * c)
- c = Class-specific curve parameter
| Constant | Default Value | Description |
|---|---|---|
| C4 | 3.1 | Base curvature parameter |
| C5 | -0.15 | Class sensitivity for b |
| C6 | 0.78 | Base scaling parameter |
| C7 | 0.12 | Class sensitivity for g |
| Exposure Factor | 0.25 | Expected loss ratio |
For each layer defined by (lower bound, upper bound):
Layer Loss = (G(upper_ratio) - G(lower_ratio)) × Net_Premium × Exposure_Factor
Where:
- Upper ratio = min(upper_bound / Net_SI, 1)
- Lower ratio = min(lower_bound / Net_SI, 1)
Uses Poisson-weighted factor approach:
Factor = Σ(k+1) × P(X=k) for k = 0 to 4+
ROL = LOL / Factor
Poisson Probabilities (where λ = LOL):
- P(0) = exp(-λ)
- P(1) = λ × exp(-λ)
- P(2) = (λ² × exp(-λ)) / 2!
- P(3) = (λ³ × exp(-λ)) / 3!
- P(4+) = 1 - Σ P(k) for k=0 to 3
Stores curve configuration parameters.
CREATE TABLE exposure_curve_configs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
c4 DECIMAL(10,6) DEFAULT 3.1,
c5 DECIMAL(10,6) DEFAULT -0.15,
c6 DECIMAL(10,6) DEFAULT 0.78,
c7 DECIMAL(10,6) DEFAULT 0.12,
exposure_factor DECIMAL(10,6) DEFAULT 0.25,
user_id BIGINT UNSIGNED NULL,
is_default BOOLEAN DEFAULT FALSE,
description TEXT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);Stores class-to-curve parameter mappings.
CREATE TABLE class_curve_parameters (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
class_name VARCHAR(255) NOT NULL,
curve_parameter_c DECIMAL(10,4) NOT NULL,
description TEXT NULL,
user_id BIGINT UNSIGNED NULL,
is_default BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);Stores calculation results with full audit trail.
CREATE TABLE exposure_rating_results (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
scenario_id BIGINT UNSIGNED NOT NULL,
reconciliation_id BIGINT UNSIGNED NULL,
user_id BIGINT UNSIGNED NOT NULL,
calculation_name VARCHAR(255) NULL,
layer_number TINYINT NOT NULL,
layer_limit DECIMAL(20,2),
layer_excess DECIMAL(20,2),
total_expected_loss DECIMAL(20,2),
loss_on_line DECIMAL(10,6),
rate_on_line DECIMAL(10,6),
exposure_rate DECIMAL(10,6),
calculation_metadata JSON NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (scenario_id) REFERENCES scenarios(id) ON DELETE CASCADE,
FOREIGN KEY (reconciliation_id) REFERENCES reconciliations(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_scenario_layer (scenario_id, layer_number),
INDEX idx_user_created (user_id, created_at)
);Added exposure rate columns:
ALTER TABLE xl_rates ADD COLUMN exposure_l1_rate DECIMAL(10,6) NULL;
ALTER TABLE xl_rates ADD COLUMN exposure_l2_rate DECIMAL(10,6) NULL;
ALTER TABLE xl_rates ADD COLUMN exposure_l3_rate DECIMAL(10,6) NULL;
ALTER TABLE xl_rates ADD COLUMN exposure_l4_rate DECIMAL(10,6) NULL;
ALTER TABLE xl_rates ADD COLUMN exposure_l5_rate DECIMAL(10,6) NULL;
ALTER TABLE xl_rates ADD COLUMN exposure_l6_rate DECIMAL(10,6) NULL;| File | Purpose |
|---|---|
2025_12_30_100000_create_exposure_curve_configs_table.php |
Creates curve configs table |
2025_12_30_100001_create_class_curve_parameters_table.php |
Creates class curve mappings table |
2025_12_30_100002_create_exposure_rating_results_table.php |
Creates results storage table |
2025_02_06_124240_add_exposure_rates_to_xl_rates_table.php |
Adds exposure columns to XL rates |
app/
├── Http/
│ ├── Controllers/
│ │ └── ExposureRatingController.php # Main controller (463 lines)
│ └── Requests/
│ └── ExposureRatingRequest.php # Form validation
├── Models/
│ ├── ExposureCurveConfig.php # Curve config model (75 lines)
│ ├── ClassCurveParameter.php # Class curve model (131 lines)
│ └── ExposureRatingResult.php # Results model (97 lines)
└── Services/
└── ExposureRating/
├── ExposureCurveService.php # Core G(x) calculations (242 lines)
├── ExposureRatingCalculator.php # Layer calculations (389 lines)
└── ROLCalculator.php # ROL computation (155 lines)
Location: app/Http/Controllers/ExposureRatingController.php
Methods:
| Method | Route | HTTP | Description |
|---|---|---|---|
index() |
/exposure-rating |
GET | Load main page with data |
calculate() |
/exposure-rating/calculate |
POST | Execute calculation |
getResults() |
/exposure-rating/results |
POST | Get results for scenario |
compare() |
/exposure-rating/compare |
POST | Compare with simulation |
getCurveData() |
/exposure-rating/curve-data |
GET | Get curve visualization data |
saveCurveConfig() |
/exposure-rating/curve-config |
POST | Save curve parameters |
saveClassCurve() |
/exposure-rating/class-curve |
POST | Save class curve mapping |
Key Implementation:
public function index()
{
$userId = auth()->id();
return Inertia::render('ExposureRating', [
'scenarios' => Scenario::where('user_id', $userId)->with('XLrates')->get(),
'reconciliations' => Reconciliation::whereHas('excell', fn($q) =>
$q->where('user_id', $userId)
)->with('excell')->get(),
'curveConfigs' => ExposureCurveConfig::forUser($userId)->get(),
'classCurves' => ClassCurveParameter::getAllCurvesForUser($userId),
'previousResults' => ExposureRatingResult::forUser($userId)->latest()->take(10)->get(),
'defaultCurves' => ClassCurveParameter::DEFAULT_CLASS_CURVES,
]);
}Location: app/Services/ExposureRating/ExposureCurveService.php
Purpose: Core exposure curve calculations implementing the Swiss Re methodology.
Key Methods:
class ExposureCurveService
{
private float $c4;
private float $c5;
private float $c6;
private float $c7;
public function __construct(
float $c4 = 3.1,
float $c5 = -0.15,
float $c6 = 0.78,
float $c7 = 0.12
) {
$this->c4 = $c4;
$this->c5 = $c5;
$this->c6 = $c6;
$this->c7 = $c7;
}
// Calculate b parameter: b = EXP(C4 + C5 * c * (1 + c))
public function calculateB(float $c): float
{
$exponent = $this->c4 + $this->c5 * $c * (1 + $c);
return exp($exponent);
}
// Calculate g parameter: g = EXP((C6 + C7 * c) * c)
public function calculateG(float $c): float
{
$exponent = ($this->c6 + $this->c7 * $c) * $c;
return exp($exponent);
}
// Calculate G(x) function - core exposure curve
public function calculateGFunction(float $x, float $b, float $g): float
{
if ($x <= 0) return 0.0;
if ($x >= 1) return 1.0;
// Core formula implementation
$term1 = ($g - 1) * $b;
$term2 = (1 - $b * $g) * pow($b, $x);
$numerator = ($term1 + $term2) / (1 - $b);
$denominator = log($b * $g);
$result = log($numerator) / $denominator;
return max(min($result, 1), 0);
}
// Generate curve data points for visualization
public function generateCurveData(float $c, int $points = 100): array;
// Create service from configuration model
public static function fromConfig(ExposureCurveConfig $config): self;
}Location: app/Services/ExposureRating/ExposureRatingCalculator.php
Purpose: Main calculation engine that processes profile data and calculates layer losses.
Key Methods:
class ExposureRatingCalculator
{
private ExposureCurveService $curveService;
private ROLCalculator $rolCalculator;
private float $exposureFactor;
// Main calculation from profile data
public function calculateFromProfile(
$profileData,
Scenario $scenario,
array $classCurveMap = [],
?int $userId = null
): array
{
// Extract layers from scenario
$layers = $this->extractLayersFromScenario($scenario);
// Process each row
foreach ($profileData as $row) {
$subClass = $row['SUB_CLASS'];
$netSi = $row['NET_SI'];
$netPrem = $row['NET_PREM'];
// Get curve parameter for class
$c = $classCurveMap[$subClass] ?? ClassCurveParameter::getCurveForClass($subClass);
// Calculate b and g
$b = $this->curveService->calculateB($c);
$g = $this->curveService->calculateG($c);
// Calculate layer losses
$layerLosses = $this->calculateLayerLosses($netSi, $netPrem, $layers, $b, $g);
}
return $this->aggregateResults($results, $layers);
}
// Extract layer configuration from scenario
public function extractLayersFromScenario(Scenario $scenario): array
{
$layers = [];
$cumulative = $scenario->deductible;
for ($i = 1; $i <= 6; $i++) {
$limitKey = "l{$i}_limit";
if (!empty($scenario->$limitKey)) {
$limit = $this->parseNumber($scenario->$limitKey);
$layers[] = [
'name' => "Layer {$i}",
'number' => $i,
'lower' => $cumulative,
'upper' => $cumulative + $limit,
'limit' => $limit,
];
$cumulative += $limit;
}
}
return $layers;
}
// Store calculation results to database
public function storeResults(
Scenario $scenario,
array $results,
int $userId,
?Reconciliation $reconciliation = null,
?string $calculationName = null
): void;
}Location: app/Services/ExposureRating/ROLCalculator.php
Purpose: Calculates Rate on Line from Loss on Line using Poisson-weighted factors.
Key Methods:
class ROLCalculator
{
// Basic ROL calculation
public function calculate(float $lol): float
{
if ($lol <= 0) return 0.0;
$lambda = $lol;
// Calculate Poisson probabilities
$p0 = exp(-$lambda);
$p1 = $lambda * exp(-$lambda);
$p2 = (pow($lambda, 2) * exp(-$lambda)) / 2;
$p3 = (pow($lambda, 3) * exp(-$lambda)) / 6;
$p4Plus = max(0, 1 - ($p0 + $p1 + $p2 + $p3));
// Weighted factor
$factor = (1 * $p0) + (2 * $p1) + (3 * $p2) + (4 * $p3) + (5 * $p4Plus);
return $factor > 0 ? $lol / $factor : 0.0;
}
// ROL with confidence interval
public function calculateWithConfidence(float $lol, float $confidenceLevel = 0.95): array
{
$rol = $this->calculate($lol);
$z = $this->getZScore($confidenceLevel);
$standardError = sqrt($rol * (1 - min($rol, 0.99)) / max(1, $lol * 100));
return [
'rol' => $rol,
'lower_bound' => max(0, $rol - $z * $standardError),
'upper_bound' => $rol + $z * $standardError,
'confidence_level' => $confidenceLevel,
];
}
// Get factor breakdown for debugging
public function getFactorBreakdown(float $lol): array;
}class ExposureCurveConfig extends Model
{
protected $fillable = [
'name', 'c4', 'c5', 'c6', 'c7', 'exposure_factor',
'user_id', 'is_default', 'description',
];
protected $casts = [
'c4' => 'float',
'c5' => 'float',
'c6' => 'float',
'c7' => 'float',
'exposure_factor' => 'float',
'is_default' => 'boolean',
];
public function user() { return $this->belongsTo(User::class); }
public function scopeForUser($query, $userId) {
return $query->where('user_id', $userId)->orWhere('is_default', true);
}
public static function getDefaultConfig() {
return static::where('is_default', true)->first() ?? new static([
'name' => 'Default',
'c4' => 3.1,
'c5' => -0.15,
'c6' => 0.78,
'c7' => 0.12,
'exposure_factor' => 0.25,
'is_default' => true,
]);
}
}Includes 22 default class-to-curve mappings:
class ClassCurveParameter extends Model
{
public const DEFAULT_CLASS_CURVES = [
'ALL RISKS' => 3.0,
'FIRE DOMESTIC (HOC)' => 1.5,
'FIRE DOMESTIC' => 1.5,
'FIRE INDUSTRIAL' => 3.0,
'FIRE CONSEQUENTIAL LOSSES' => 3.0,
'INDUSTRIAL ALL RISKS' => 3.0,
'CONTRACTORS ALL RISKS' => 3.0,
'ERECTION ALL RISKS' => 3.0,
'CONTRACTORS PLANT AND MACHINERY' => 3.0,
'MACHINERY BREAKDOWN' => 3.0,
'ELECTRONIC EQUIPMENT' => 3.0,
'MARINE CARGO' => 2.5,
'MARINE HULL' => 2.5,
'MOTOR COMMERCIAL' => 2.0,
'MOTOR PRIVATE' => 1.5,
'BURGLARY' => 2.0,
'MONEY' => 2.0,
'FIDELITY GUARANTEE' => 2.0,
'PUBLIC LIABILITY' => 2.5,
'EMPLOYERS LIABILITY' => 2.5,
'PROFESSIONAL INDEMNITY' => 2.5,
'WORKMEN COMPENSATION' => 2.0,
];
public static function getCurveForClass(string $className, ?int $userId = null): float
{
// Normalize class name
$normalizedName = strtoupper(trim($className));
// Try database first, then fall back to defaults
// Returns 3.0 as ultimate fallback
}
public static function getAllCurvesForUser(?int $userId = null): array
{
// Merge default curves with user-specific curves
}
}class ExposureRatingResult extends Model
{
protected $fillable = [
'scenario_id', 'reconciliation_id', 'user_id', 'calculation_name',
'layer_number', 'layer_limit', 'layer_excess', 'total_expected_loss',
'loss_on_line', 'rate_on_line', 'exposure_rate', 'calculation_metadata',
];
protected $casts = [
'layer_limit' => 'float',
'layer_excess' => 'float',
'total_expected_loss' => 'float',
'loss_on_line' => 'float',
'rate_on_line' => 'float',
'exposure_rate' => 'float',
'calculation_metadata' => 'array',
];
public function scenario() { return $this->belongsTo(Scenario::class); }
public function reconciliation() { return $this->belongsTo(Reconciliation::class); }
public function user() { return $this->belongsTo(User::class); }
public function scopeForScenario($query, $scenarioId) {
return $query->where('scenario_id', $scenarioId)->orderBy('layer_number');
}
public function scopeForUser($query, $userId) {
return $query->where('user_id', $userId);
}
}resources/js/
├── Pages/
│ └── ExposureRating.vue # Main page (~718 lines)
└── Components/
└── ExposureRating/
├── LayerResultsTable.vue # Results table (188 lines)
├── CurveVisualizer.vue # Chart component (347 lines)
├── ClassCurveEditor.vue # Class editor (232 lines)
└── ComparisonView.vue # Comparison component
Layout: Uses MainLayout for consistent navigation sidebar.
Features:
- 3-tab navigation (Calculate, Results, History)
- Visual progress steps indicator
- Scenario dropdown selection
- Reconciliation dropdown showing Data Cleaning process names
- Real-time calculation results
- History table with previous calculations
Key Template Structure:
<template>
<Head title="Exposure Rating" />
<Loader v-if="showLoader" />
<MainLayout>
<!-- Tab Navigation -->
<nav class="isolate flex divide-x divide-gray-200 rounded-lg shadow-2xl">
<button @click="currentTab = 'calculate'">
<i class="fas fa-calculator mr-2"></i>Calculate
</button>
<button @click="currentTab = 'results'">
<i class="fas fa-chart-bar mr-2"></i>Results
</button>
<button @click="currentTab = 'history'">
<i class="fas fa-history mr-2"></i>History
</button>
</nav>
<!-- Progress Steps -->
<ol class="flex items-center justify-between">
<li>Step 1: Select Scenario</li>
<li>Step 2: Select Data</li>
<li>Step 3: View Results</li>
</ol>
<!-- Tab Content -->
<LayerResultsTable :results="results?.summary" />
<CurveVisualizer />
<ClassCurveEditor :curves="classCurves" />
<ComparisonView :comparison="comparison" />
</MainLayout>
</template>Key Script Logic:
<script setup>
import { ref, computed } from 'vue';
import { useForm, Head } from '@inertiajs/vue3';
import MainLayout from '@/Layouts/MainLayout.vue';
import Loader from '@/Components/Loader.vue';
import moment from 'moment';
// Props from controller
const props = defineProps({
scenarios: Array,
reconciliations: Array,
curveConfigs: Array,
classCurves: Object,
previousResults: Array,
defaultCurves: Object,
});
// State
const currentTab = ref('calculate');
const results = ref(null);
// Form
const form = useForm({
scenario_id: null,
reconciliation_id: null,
calculation_name: '',
class_curves: {},
});
// Display reconciliation with Data Cleaning process name
const getReconciliationDisplayName = (recon) => {
if (recon.excell?.import_name) {
return recon.excell.import_name; // Shows "Test 3", "With Morph", etc.
}
if (recon.excell?.original_name) {
return recon.excell.original_name;
}
return `Reconciliation #${recon.id}`;
};
// Submit calculation
const calculateExposure = () => {
form.post(route('exposure.calculate'), {
onSuccess: (page) => {
results.value = page.props.flash?.results;
currentTab.value = 'results';
},
});
};
</script>Purpose: Displays layer-by-layer calculation results with ROL/LOL metrics.
Features:
- Dark theme (
bg-secondary) matching Data Cleaning module - Layer number badges with primary color
- Color-coded ROL values (green < 10%, yellow 10-20%, red > 20%)
- Export button
- Totals row with summary statistics
- Quick stats footer
Template Structure:
<template>
<div class="bg-secondary rounded-lg shadow-2xl overflow-hidden">
<!-- Header -->
<div class="px-6 py-4 border-b border-gray-700">
<h3 class="text-lg font-semibold text-white">
<i class="fas fa-table text-primary"></i>
Layer Results Summary
</h3>
</div>
<!-- Results Table -->
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-gray-800/50">
<tr>
<th>Layer</th>
<th>Limit</th>
<th>Excess</th>
<th>Expected Loss</th>
<th>LOL</th>
<th>ROL</th>
</tr>
</thead>
<tbody>
<tr v-for="(data, layerName) in results">
<td>
<span class="rounded-full bg-primary/20 text-primary">
{{ getLayerNumber(layerName) }}
</span>
</td>
<td>{{ formatNumber(data.limit) }}</td>
<td>{{ formatNumber(data.excess) }}</td>
<td>{{ formatNumber(data.total_expected_loss) }}</td>
<td>{{ formatPercentage(data.loss_on_line) }}</td>
<td :class="getRolColor(data.rate_on_line)">
{{ formatPercentage(data.rate_on_line) }}
</td>
</tr>
</tbody>
</table>
</div>
</template>Purpose: Interactive Chart.js visualization of exposure curves.
Features:
- Real-time curve updates with slider
- Parameter input for curve value (c)
- Display of calculated b and g parameters
- Dark themed chart with grid styling
- Educational info section explaining the curve
Key Implementation:
import { Chart, registerables } from 'chart.js';
Chart.register(...registerables);
const curveParameter = ref(3.0);
const chartCanvas = ref(null);
let chart = null;
const updateCurve = async () => {
const response = await axios.get(route('exposure.curve.data'), {
params: { c: curveParameter.value }
});
curveParams.value = response.data.parameters;
chart.data.datasets[0].data = response.data.points.map(p => ({
x: p.x,
y: p.y
}));
chart.update();
};Purpose: Edit curve parameters per insurance class.
Features:
- White card with shadow (contrasts with dark sections)
- Visual progress bars showing relative curve values
- Add/Remove class functionality
- Modal for adding new classes
- Quick stats (class count, average, range)
Purpose: Side-by-side comparison of exposure vs simulation results.
Features:
- Dark theme consistent with other components
- Comparison table with difference calculations
- Color-coded differences
- Chart.js bar chart visualization
File: routes/web.php
use App\Http\Controllers\ExposureRatingController;
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])
->group(function () {
// Exposure Rating Routes
Route::get('/exposure-rating', [ExposureRatingController::class, 'index'])
->name('exposurerating');
Route::post('/exposure-rating/calculate', [ExposureRatingController::class, 'calculate'])
->name('exposure.calculate');
Route::post('/exposure-rating/results', [ExposureRatingController::class, 'getResults'])
->name('exposure.results');
Route::post('/exposure-rating/compare', [ExposureRatingController::class, 'compare'])
->name('exposure.compare');
Route::get('/exposure-rating/curve-data', [ExposureRatingController::class, 'getCurveData'])
->name('exposure.curve.data');
Route::post('/exposure-rating/curve-config', [ExposureRatingController::class, 'saveCurveConfig'])
->name('exposure.curve.save');
Route::post('/exposure-rating/class-curve', [ExposureRatingController::class, 'saveClassCurve'])
->name('exposure.class.curve.save');
});File: resources/js/Layouts/MainLayout.vue
Menu item added to sidebar navigation:
const menuItems = [
// ... other items
{
name: 'Exposure Rating',
icon: 'fas fa-chart-line',
link: 'exposurerating',
permission: 'exposure_rating'
}
];
const checkSubscriptions = (subscriptions, item) => {
// Always show for exposure rating
if (item.link === 'exposurerating') return true;
// ... other subscription checks
};File: resources/js/Layouts/AppLayout.vue
Menu item also added to AppLayout for dashboard consistency.
| Element | Tailwind Class | Hex Value |
|---|---|---|
| Primary | bg-primary |
#dc2626 (Red) |
| Secondary | bg-secondary |
#374151 (Dark Gray) |
| Background | bg-gray-800/50 |
Semi-transparent dark |
| Text Primary | text-white |
#FFFFFF |
| Text Secondary | text-gray-400 |
#9CA3AF |
| Success | text-green-400 |
#4ADE80 |
| Warning | text-yellow-400 |
#FACC15 |
| Error | text-red-400 |
#F87171 |
Dark Cards:
<div class="bg-secondary rounded-lg shadow-2xl overflow-hidden">
<div class="px-6 py-4 border-b border-gray-700">
<!-- Header -->
</div>
<div class="p-6">
<!-- Content -->
</div>
</div>Badges:
<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium
bg-green-900/30 text-green-400 ring-1 ring-inset ring-green-500/30">
Value
</span>Primary Buttons:
<button class="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2
text-sm font-semibold text-white hover:bg-primary/90 transition-all duration-200">
<i class="fas fa-calculator"></i> Calculate
</button>Form Inputs (Dark Theme):
<input type="text" class="w-full px-4 py-3 bg-gray-700 border border-gray-600
rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-primary
focus:border-primary transition-all duration-200" />The Exposure Rating module integrates with the Data Cleaning module through:
-
Reconciliation Relationship:
Reconciliationmodel hasexcell_id(note: double L) foreign keyExcellmodel containsimport_name(the Data Cleaning process name)- Dropdown shows human-readable process names
-
Profile Data Loading:
- Loads Excel data from reconciliation file
- Extracts SUB_CLASS, NET_SI, NET_PREM columns
- Applies curve parameters per class
- Reads layer configuration from
Scenariomodel - Supports up to 6 layers (l1_limit through l6_limit)
- Uses scenario deductible as base
- Updates
XLratestable with exposure rates - Columns: exposure_l1_rate through exposure_l6_rate
- Enables blended pricing (simulation + exposure)
Endpoint: POST /exposure-rating/calculate
Request:
{
"scenario_id": 1,
"reconciliation_id": 2,
"calculation_name": "Q4 2025 Calculation",
"class_curves": {
"ALL RISKS": 3.0,
"FIRE DOMESTIC": 1.5
}
}Response:
{
"message": "Exposure rating calculated successfully",
"results": {
"detail": [...],
"summary": {
"Layer 1": {
"limit": 1000000,
"excess": 500000,
"total_expected_loss": 25000,
"loss_on_line": 0.025,
"rate_on_line": 0.0125
}
}
}
}Endpoint: GET /exposure-rating/curve-data?c=3.0
Response:
{
"curve_data": {
"points": [
{"x": 0, "y": 0},
{"x": 0.01, "y": 0.15},
{"x": 1, "y": 1}
],
"parameters": {
"c": 3.0,
"b": 5.437,
"g": 30.123
}
}
}Endpoint: POST /exposure-rating/compare
Request:
{
"scenario_id": 1
}Response:
{
"comparison": {
"Layer 1": {
"simulation_rate": 0.0130,
"exposure_rate": 0.0125,
"difference": -0.0005,
"difference_percent": -3.85
}
}
}tests/
├── Unit/
│ └── ExposureRating/
│ ├── ExposureCurveServiceTest.php
│ ├── ROLCalculatorTest.php
│ └── ExposureRatingCalculatorTest.php
└── Feature/
└── ExposureRatingTest.php
| Test | Description |
|---|---|
test_calculate_b_with_default_parameters |
Verify b calculation |
test_calculate_g_with_default_parameters |
Verify g calculation |
test_g_function_returns_zero_for_zero_x |
Edge case: x = 0 |
test_g_function_returns_one_for_x_greater_than_one |
Edge case: x ≥ 1 |
test_g_function_is_monotonically_increasing |
Curve property validation |
test_rol_calculation |
ROL from LOL |
test_layer_extraction_from_scenario |
Layer parsing |
test_exposure_rating_page_requires_authentication |
Auth check |
test_can_calculate_exposure_rating |
Full calculation flow |
# Run all exposure rating tests
php artisan test --filter=ExposureRating
# Run specific test class
php artisan test tests/Unit/ExposureRating/ExposureCurveServiceTest.php
# Run with coverage
php artisan test --filter=ExposureRating --coverage# Run migrations
php artisan migrate
# Seed default configurations (if seeder exists)
php artisan db:seed --class=ExposureCurveConfigSeeder
# Clear caches
php artisan config:clear
php artisan cache:clear
php artisan view:clear
# Build frontend
npm run build- Migrations executed successfully
- Database seeded with default curve configs
- Menu item visible in sidebar
- Exposure Rating page loads correctly
- Scenario dropdown populated
- Reconciliation dropdown shows process names
- Calculation executes without errors
- Results display correctly
- History tab shows previous calculations
- Curve visualizer renders chart
- User acceptance testing completed
- Error monitoring for 24 hours
Issue: Exposure Rating menu item not appearing in sidebar.
Cause: Two separate layout files (MainLayout.vue and AppLayout.vue) requiring menu addition to both.
Solution: Added menu item to both layouts with proper route name exposurerating.
Issue: Database error referencing excel_id column.
Cause: Column is named excell_id (double L) in the database.
Solution: Updated all references to use excell_id and excell relationship.
Issue: Reconciliation dropdown showing IDs instead of meaningful names.
Cause: Missing relationship eager loading and display logic.
Solution: Added getReconciliationDisplayName() function that returns excell.import_name.
| Feature | Priority | Status |
|---|---|---|
| Custom Curve Builder UI | Medium | Planned |
| Machine Learning curve prediction | Low | Backlog |
| Batch calculation for multiple scenarios | Medium | Planned |
| PDF Report Generation | High | Planned |
| Real-time collaboration | Low | Backlog |
| Sensitivity analysis | Medium | Planned |
Route::prefix('api/v2/exposure-rating')->group(function () {
Route::post('/batch-calculate', 'batchCalculate');
Route::get('/historical-comparison', 'historicalComparison');
Route::get('/sensitivity-analysis', 'sensitivityAnalysis');
Route::get('/export-pdf', 'exportPdf');
});| Python (Original Script) | PHP (Implementation) |
|---|---|
np.exp(x) |
exp($x) |
np.log(x) |
log($x) |
math.exp(x) |
exp($x) |
b ** x |
pow($b, $x) |
max(min(val, 1), 0) |
max(min($val, 1), 0) |
df.apply(lambda...) |
collect(...)->map(fn...) |
df['col'].sum() |
collect($arr)->sum('col') |
| File | Lines | Purpose |
|---|---|---|
ExposureRatingController.php |
463 | Main controller |
ExposureCurveService.php |
242 | Core calculations |
ExposureRatingCalculator.php |
389 | Layer calculations |
ROLCalculator.php |
155 | ROL computation |
ExposureCurveConfig.php |
75 | Curve config model |
ClassCurveParameter.php |
131 | Class curve model |
ExposureRatingResult.php |
97 | Results model |
ExposureRating.vue |
718 | Main Vue page |
LayerResultsTable.vue |
188 | Results component |
CurveVisualizer.vue |
347 | Chart component |
ClassCurveEditor.vue |
232 | Editor component |
ComparisonView.vue |
~200 | Comparison component |
Total: ~3,400+ lines of code implemented
| Term | Definition |
|---|---|
| Exposure Curve | Mathematical function relating damage ratios to expected losses |
| G(x) | The core exposure curve function |
| ROL | Rate on Line - premium rate as percentage of layer limit |
| LOL | Loss on Line - expected loss as percentage of layer limit |
| XL | Excess of Loss - type of reinsurance treaty |
| Deductible | Retention level below which cedant retains losses |
| Layer Limit | Maximum loss covered by a reinsurance layer |
| Sum Insured (SI) | Total value insured under a policy |
| Net Premium | Premium after deducting reinsurance cessions |
Module Owner: Actuarial Development Team
Repository: ACENSURE/Acentre-System
Branch: dev
Documentation Version: 1.0.0
Last Updated: December 30, 2025
This document serves as the authoritative technical reference for the Exposure Rating module in the Acentre Reinsurance & Actuarial System.