Skip to content

Risk Assessment & Premium Calculation

Weekly premium is computed per rider from zone history, weather forecast, seasonal patterns, social disruption risk, and individual claim frequency. Strictly weekly — computed every Sunday, covers Monday through Sunday.

premium = BASE_PREMIUM + min(risk_adjustment, MAX_PREMIUM - BASE_PREMIUM)
risk_adjustment = (risk_from_events + risk_from_forecast + risk_from_social)
× seasonal_multiplier
× claim_frequency_multiplier

Constants:

ConstantValue
BASE_PREMIUM₹49
MAX_PREMIUM₹199
RISK_FACTOR_PER_EVENT₹12
FORECAST_WEIGHT₹20
WEEKS_LOOKBACK4

India’s weather follows strong seasonal patterns that directly affect delivery disruption frequency. The premium calculator applies a calendar-month seasonal multiplier to the weekly premium (billing stays weekly per use case; this is not monthly pricing).

MonthsMultiplierReason
Jun – Sep (Monsoon)1.40×Peak rain, flooding, waterlogging
Oct1.15×Post-monsoon cyclones, residual rain
Nov1.15×Cyclone season (Bay of Bengal)
Dec – Feb (Winter)0.85×Mild weather, low disruption risk
Mar (Transition)1.00×Baseline
Apr – May (Pre-monsoon)1.25×Extreme heat waves across north India
const SEASONAL_RISK_MULTIPLIER: Record<number, number> = {
0: 0.85, // Jan
1: 0.85, // Feb
2: 1.0, // Mar
3: 1.25, // Apr
4: 1.25, // May
5: 1.4, // Jun
6: 1.4, // Jul
7: 1.4, // Aug
8: 1.4, // Sep
9: 1.15, // Oct
10: 1.15, // Nov
11: 0.85, // Dec
};

Social disruption (strikes, curfews, lockdowns) is tracked separately from weather. The getSocialRiskFactor function queries live_disruption_events of type social within a rider’s zone over the past 4 weeks:

socialRiskFactor = min(1.0, socialEventCount / 5)
Social events (4 weeks)Risk factor
00.0
10.2
30.6
5+1.0 (cap)

This ensures riders in protest-prone zones pay a fair premium reflecting their actual risk.


Riders with frequent recent claims represent higher risk. The premium calculator counts claims from the past 4 weeks:

claimFreqMultiplier = 1.0 + min(0.2, claimCountLast4Weeks × 0.04)
Claims (4 weeks)Multiplier
01.00×
21.08×
51.20× (cap)
101.20× (cap)

The 0.2 cap ensures that even high-claim riders don’t see premiums spike beyond 20% above base due to this factor alone.


The premium calculator queries live_disruption_events for the past 28 days. For each event, it checks whether the event’s geofence overlaps with the rider’s zone using isWithinCircle():

export async function getHistoricalEventCount(
supabase,
zoneLatitude?: number,
zoneLongitude?: number
): Promise<number> {
const since = new Date();
since.setDate(since.getDate() - WEEKS_LOOKBACK * 7);
const { data } = await supabase
.from("live_disruption_events")
.select("id, geofence_polygon")
.gte("created_at", since.toISOString());
let count = 0;
for (const ev of data) {
const gf = ev.geofence_polygon;
if (!gf?.lat || !gf?.lng) {
count++;
continue;
}
if (isWithinCircle(zoneLatitude, zoneLongitude, gf.lat, gf.lng, gf.radius_km ?? 10)) {
count++;
}
}
return count;
}

Zones without explicit geofence data (e.g., citywide curfews) count toward every rider’s risk score.


A 0–1 multiplier derived from Tomorrow.io’s 5-day hourly forecast for the rider’s zone. Any forecast hour meeting a trigger threshold (temperature ≥ 43°C or precipitation ≥ 4 mm/h) increments the trigger counter:

export async function getForecastRiskFactor(
_supabase, lat: number, lng: number
): Promise<number> {
const hourly = await fetchTomorrowForecast(lat, lng);
let triggerHours = 0;
for (const interval of hourly) {
if (interval.temperature >= 43 || interval.precipitationIntensity >= 4) {
triggerHours++;
}
}
return Math.min(1, triggerHours / hourly.length);
}

Zone history (4w)ForecastSocial eventsClaims (4w)MonthPremium
0 events0.000Nov₹49 (base)
3 events0.212Mar₹89
5 events0.534Jul (monsoon)₹175
8+ events0.8510Aug (monsoon)₹199 (cap)

Premiums apply PREMIUM.RESERVE_LOAD (currently 2%) after core expected-loss math in both the legacy weekly formula and the dynamic engine. This funds technical reserve narrative (IBNR-style lag, tail correlation) and aligns with Policy §10. It is not a separate rider-visible line item — it is baked into the quoted weekly premium before clamping to PREMIUM.BASEPREMIUM.MAX.

Dynamic engine: raw_premium = expected_loss × (1 + margin + safety_buffer) × (1 + RESERVE_LOAD).

Reinsurance (quota share, catastrophe XL) is described at contract level in Policy §10; it is not modeled in application code in the current release.


After the premium is calculated, riders choose from three flat-rate plans. The dynamic calculation informs the recommendation, but riders can select any tier:

PlanWeekly PremiumPayout Per ClaimMax Claims/Week
Basic₹49₹3001
Standard₹99₹7002
Premium₹199₹1,5003

The plan chosen is stored as weekly_policies.plan_id referencing plan_packages.id.


Every Sunday at 17:30 UTC, /api/cron/weekly-premium runs:

  1. Fetches all active policies where week_end_date < today.
  2. Sets is_active = false for expired policies.
  3. For each rider with an expired policy, recalculates the premium:
    • Fetches historical events, forecast risk, and social risk per zone (cached per zone).
    • Queries the rider’s claim count over the past 4 weeks.
    • Applies seasonal multiplier for the current month.
    • Passes all factors to calculatePremiumWithLlm for final recommendation.
  4. Stores the result in premium_recommendations with risk_factors JSONB:
    {
    "historical_events": 3,
    "forecast_risk": 0.25,
    "social_risk": 0.4,
    "claim_count_4w": 2,
    "seasonal_multiplier": 1.4
    }

Riders must manually re-subscribe each week — automatic renewal would require a recurring billing integration (for example Razorpay Subscriptions) on top of the current weekly Checkout flow.


The admin analytics panel shows a predicted claims range for the coming week, calculated by lib/ml/next-week-risk.ts:

With Tomorrow.io API key (primary path):

  1. Discover all active rider zones from weekly_policies + profiles.
  2. Deduplicate zones within ~11 km of each other.
  3. For each zone in parallel:
    • Fetch 5-day hourly weather forecast (Tomorrow.io) — count hours above heat/rain thresholds.
    • Fetch 5-day AQI forecast (Open-Meteo) — count hours above AQI 150.
  4. Aggregate trigger hours and risk types across all zones.
  5. Factor in active policy count and severity weight.
  6. Return a low–high range, risk level, AQI risk note, and zones checked count.

Historical fallback (no API key):

  1. Query parametric_claims for the past 21 days.
  2. Calculate the weekly average.
  3. Apply a linear trend (week 1 vs. week 3 claim rate).
  4. Return avg ± 2 as the range.
interface NextWeekPrediction {
expectedClaimsRange: string; // e.g. "8–14"
riskLevel: "low" | "medium" | "high";
source: "forecast" | "historical";
details?: string;
aqiRisk?: string; // e.g. "12h of poor AQI across 3 zones"
zonesChecked?: number; // number of zones analyzed
}