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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
30 changes: 30 additions & 0 deletions agricultural-insurance-suite/cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"agricultural-insurance-suite/internal/handlers"
"agricultural-insurance-suite/internal/repository"
"agricultural-insurance-suite/internal/service"
"encoding/json"
"log"
"net/http"

"github.com/gorilla/mux"
)

func main() {
repo := repository.NewRepository()
svc := service.NewService(repo)
h := handlers.NewHandler(svc)

r := mux.NewRouter()
r.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "healthy", "service": "agricultural-insurance-suite"})
}).Methods("GET")
r.HandleFunc("/ready", func(w http.ResponseWriter, _ *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ready"})
}).Methods("GET")
h.RegisterRoutes(r)

log.Println("[agricultural-insurance-suite] starting on :8140 — 13 climate/agricultural products")
log.Fatal(http.ListenAndServe(":8140", r))
}
5 changes: 5 additions & 0 deletions agricultural-insurance-suite/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module agricultural-insurance-suite

go 1.22.0

require github.com/gorilla/mux v1.8.1
2 changes: 2 additions & 0 deletions agricultural-insurance-suite/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
104 changes: 104 additions & 0 deletions agricultural-insurance-suite/internal/handlers/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package handlers

import (
"agricultural-insurance-suite/internal/models"
"agricultural-insurance-suite/internal/service"
"encoding/json"
"net/http"

"github.com/gorilla/mux"
)

type Handler struct {
svc *service.Service
}

func NewHandler(svc *service.Service) *Handler {
return &Handler{svc: svc}
}

func (h *Handler) RegisterRoutes(r *mux.Router) {
api := r.PathPrefix("/api/v1/agricultural").Subrouter()
api.HandleFunc("/products", h.listProducts).Methods("GET")
api.HandleFunc("/products/{id}", h.getProduct).Methods("GET")
api.HandleFunc("/products/category/{category}", h.getByCategory).Methods("GET")
api.HandleFunc("/enroll", h.enrollPolicy).Methods("POST")
api.HandleFunc("/policies", h.listPolicies).Methods("GET")
api.HandleFunc("/trigger/evaluate", h.evaluateTrigger).Methods("POST")
api.HandleFunc("/triggers", h.listTriggers).Methods("GET")
api.HandleFunc("/payouts", h.listPayouts).Methods("GET")
api.HandleFunc("/ndvi/assess", h.ndviAssess).Methods("POST")
}

func (h *Handler) listProducts(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{"products": h.svc.GetAllProducts(), "count": len(h.svc.GetAllProducts())})
}

func (h *Handler) getProduct(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
p := h.svc.GetProduct(id)
if p == nil {
http.Error(w, `{"error":"product not found"}`, 404)
return
}
json.NewEncoder(w).Encode(p)
}

func (h *Handler) getByCategory(w http.ResponseWriter, r *http.Request) {
cat := mux.Vars(r)["category"]
products := h.svc.GetProductsByCategory(cat)
json.NewEncoder(w).Encode(map[string]interface{}{"category": cat, "products": products, "count": len(products)})
}

func (h *Handler) enrollPolicy(w http.ResponseWriter, r *http.Request) {
var req models.EnrollRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request body"}`, 400)
return
}
policy, err := h.svc.EnrollPolicy(req)
if err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, 400)
return
}
w.WriteHeader(201)
json.NewEncoder(w).Encode(policy)
}

func (h *Handler) listPolicies(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{"policies": h.svc.GetAllPolicies()})
}

func (h *Handler) evaluateTrigger(w http.ResponseWriter, r *http.Request) {
var req models.TriggerRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request body"}`, 400)
return
}
event, err := h.svc.EvaluateTrigger(req)
if err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, 400)
return
}
json.NewEncoder(w).Encode(event)
}

func (h *Handler) listTriggers(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{"triggers": h.svc.GetAllTriggers()})
}

func (h *Handler) listPayouts(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{"payouts": h.svc.GetAllPayouts()})
}

func (h *Handler) ndviAssess(w http.ResponseWriter, r *http.Request) {
var req struct {
Region string `json:"region"`
Value float64 `json:"ndvi_value"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request"}`, 400)
return
}
json.NewEncoder(w).Encode(h.svc.GetNDVIAssessment(req.Region, req.Value))
}
122 changes: 122 additions & 0 deletions agricultural-insurance-suite/internal/models/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package models

import "time"

type ProductType string

const (
ProductClimaCashRain ProductType = "climacash_rain"
ProductClimaCashDrought ProductType = "climacash_drought"
ProductClimaCashFlood ProductType = "climacash_flood"
ProductClimaCashHeat ProductType = "climacash_heat"
ProductWeatherIndexCrop ProductType = "weather_index_crop"
ProductLivestockIndex ProductType = "livestock_index_ibli"
ProductLivestockTakaful ProductType = "livestock_takaful_iblt"
ProductFertiliserBundled ProductType = "fertiliser_bundled"
ProductAreaYieldIndex ProductType = "area_yield_index"
ProductAquaculture ProductType = "aquaculture_fisheries"
ProductMultiPerilCrop ProductType = "multi_peril_crop"
ProductPastoralRoute ProductType = "pastoral_route"
ProductCarbonCredit ProductType = "carbon_credit"
)

type TriggerType string

const (
TriggerRainfall TriggerType = "rainfall"
TriggerTemperature TriggerType = "temperature"
TriggerNDVI TriggerType = "ndvi_vegetation"
TriggerWindSpeed TriggerType = "wind_speed"
TriggerAreaYield TriggerType = "area_yield"
TriggerGPS TriggerType = "gps_movement"
TriggerCarbonFlux TriggerType = "carbon_flux"
)

type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Type ProductType `json:"type"`
Description string `json:"description"`
TriggerType TriggerType `json:"trigger_type"`
TriggerSource string `json:"trigger_source"`
ThresholdValue float64 `json:"threshold_value"`
ThresholdUnit string `json:"threshold_unit"`
PayoutAmount float64 `json:"payout_amount_ngn"`
PremiumAmount float64 `json:"premium_amount_ngn"`
CoverageRegions []string `json:"coverage_regions"`
CoveredAssets []string `json:"covered_assets"`
MaxPayoutNGN float64 `json:"max_payout_ngn"`
Season string `json:"season"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
}

type Policy struct {
ID string `json:"id"`
ProductID string `json:"product_id"`
CustomerID string `json:"customer_id"`
CustomerName string `json:"customer_name"`
Region string `json:"region"`
State string `json:"state"`
LGA string `json:"lga"`
Assets []Asset `json:"assets"`
PremiumPaid float64 `json:"premium_paid_ngn"`
CoverageStart time.Time `json:"coverage_start"`
CoverageEnd time.Time `json:"coverage_end"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}

type Asset struct {
Type string `json:"type"`
Quantity int `json:"quantity"`
Value float64 `json:"value_ngn"`
}

type TriggerEvent struct {
ID string `json:"id"`
ProductType string `json:"product_type"`
TriggerType string `json:"trigger_type"`
Region string `json:"region"`
MeasuredValue float64 `json:"measured_value"`
Threshold float64 `json:"threshold"`
Triggered bool `json:"triggered"`
DataSource string `json:"data_source"`
Timestamp time.Time `json:"timestamp"`
}

type ClaimPayout struct {
ID string `json:"id"`
PolicyID string `json:"policy_id"`
TriggerID string `json:"trigger_event_id"`
Amount float64 `json:"amount_ngn"`
Status string `json:"status"`
PayoutMethod string `json:"payout_method"`
ProcessedAt time.Time `json:"processed_at"`
}

type NDVIReading struct {
Region string `json:"region"`
Value float64 `json:"ndvi_value"`
Percentile float64 `json:"percentile"`
Condition string `json:"condition"`
Timestamp time.Time `json:"timestamp"`
}

type EnrollRequest struct {
ProductID string `json:"product_id"`
CustomerID string `json:"customer_id"`
CustomerName string `json:"customer_name"`
Region string `json:"region"`
State string `json:"state"`
LGA string `json:"lga"`
Assets []Asset `json:"assets"`
}

type TriggerRequest struct {
ProductType string `json:"product_type"`
TriggerType string `json:"trigger_type"`
Region string `json:"region"`
MeasuredValue float64 `json:"measured_value"`
DataSource string `json:"data_source"`
}
127 changes: 127 additions & 0 deletions agricultural-insurance-suite/internal/repository/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package repository

import (
"agricultural-insurance-suite/internal/models"
"sync"
"time"
)

type Repository struct {
mu sync.RWMutex
products map[string]*models.Product
policies map[string]*models.Policy
triggers map[string]*models.TriggerEvent
payouts map[string]*models.ClaimPayout
}

func NewRepository() *Repository {
r := &Repository{
products: make(map[string]*models.Product),
policies: make(map[string]*models.Policy),
triggers: make(map[string]*models.TriggerEvent),
payouts: make(map[string]*models.ClaimPayout),
}
r.seedProducts()
return r
}

func (r *Repository) seedProducts() {
products := []models.Product{
{ID: "PROD-RAIN-001", Name: "ClimaCash RainCash", Type: models.ProductClimaCashRain, Description: "Parametric payout when rainfall exceeds threshold — protects farmers from flood damage", TriggerType: models.TriggerRainfall, TriggerSource: "NiMet", ThresholdValue: 255, ThresholdUnit: "mm/week", PayoutAmount: 50000, PremiumAmount: 2500, CoverageRegions: []string{"North-Central", "South-West", "South-South"}, CoveredAssets: []string{"crops", "farmland"}, MaxPayoutNGN: 200000, Season: "rainy", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-DROUGHT-001", Name: "ClimaCash DroughtCash", Type: models.ProductClimaCashDrought, Description: "Auto payout when rainfall drops below minimum for extended period", TriggerType: models.TriggerRainfall, TriggerSource: "NiMet", ThresholdValue: 20, ThresholdUnit: "mm/month", PayoutAmount: 75000, PremiumAmount: 3500, CoverageRegions: []string{"North-West", "North-East", "North-Central"}, CoveredAssets: []string{"crops", "livestock_feed"}, MaxPayoutNGN: 300000, Season: "dry", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-FLOOD-001", Name: "ClimaCash FloodCash", Type: models.ProductClimaCashFlood, Description: "Emergency cash for communities impacted by flooding events", TriggerType: models.TriggerRainfall, TriggerSource: "NiMet", ThresholdValue: 380, ThresholdUnit: "mm/week", PayoutAmount: 100000, PremiumAmount: 5000, CoverageRegions: []string{"South-South", "South-East", "North-Central"}, CoveredAssets: []string{"property", "crops", "livestock"}, MaxPayoutNGN: 500000, Season: "rainy", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-HEAT-001", Name: "ClimaCash HeatCash", Type: models.ProductClimaCashHeat, Description: "Payout when temperature exceeds dangerous threshold for livestock and crops", TriggerType: models.TriggerTemperature, TriggerSource: "NiMet", ThresholdValue: 42, ThresholdUnit: "celsius", PayoutAmount: 40000, PremiumAmount: 2000, CoverageRegions: []string{"North-East", "North-West"}, CoveredAssets: []string{"livestock", "crops"}, MaxPayoutNGN: 160000, Season: "harmattan", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-WICI-001", Name: "Weather Index Crop Insurance", Type: models.ProductWeatherIndexCrop, Description: "NiMet satellite rainfall data triggers automatic crop loss payouts — GIZ-EU VACE Programme", TriggerType: models.TriggerRainfall, TriggerSource: "NiMet-Satellite", ThresholdValue: 150, ThresholdUnit: "mm/season", PayoutAmount: 85000, PremiumAmount: 4200, CoverageRegions: []string{"Benue", "Niger", "Kaduna"}, CoveredAssets: []string{"maize", "rice", "sorghum", "millet", "cassava"}, MaxPayoutNGN: 350000, Season: "long_rains", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-IBLI-001", Name: "Index-Based Livestock Insurance", Type: models.ProductLivestockIndex, Description: "NDVI satellite monitors pasture — auto-payout below 20th percentile — Africa Re model", TriggerType: models.TriggerNDVI, TriggerSource: "NDVI-Satellite", ThresholdValue: 0.20, ThresholdUnit: "percentile", PayoutAmount: 120000, PremiumAmount: 6000, CoverageRegions: []string{"Sokoto", "Bauchi", "Adamawa", "Plateau"}, CoveredAssets: []string{"cattle", "camels", "sheep", "goats"}, MaxPayoutNGN: 500000, Season: "year_round", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-IBLT-001", Name: "Index-Based Livestock Takaful", Type: models.ProductLivestockTakaful, Description: "Sharia-compliant IBLI with Takaful mutual structure and NDVI triggers", TriggerType: models.TriggerNDVI, TriggerSource: "NDVI-Satellite", ThresholdValue: 0.20, ThresholdUnit: "percentile", PayoutAmount: 120000, PremiumAmount: 5500, CoverageRegions: []string{"Sokoto", "Zamfara", "Katsina", "Kano", "Borno"}, CoveredAssets: []string{"cattle", "camels", "sheep", "goats"}, MaxPayoutNGN: 500000, Season: "year_round", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-FERT-001", Name: "Fertiliser-Bundled Crop Insurance", Type: models.ProductFertiliserBundled, Description: "Auto coverage bundled with subsidised fertiliser purchase — zero-friction enrollment", TriggerType: models.TriggerRainfall, TriggerSource: "NiMet", ThresholdValue: 100, ThresholdUnit: "mm/season", PayoutAmount: 7000, PremiumAmount: 500, CoverageRegions: []string{"Trans-Nzoia", "Kakamega", "Kericho"}, CoveredAssets: []string{"fertiliser_investment", "crops"}, MaxPayoutNGN: 28000, Season: "long_rains", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-AYI-001", Name: "Area Yield Index Insurance", Type: models.ProductAreaYieldIndex, Description: "Payouts based on average area yield — lower basis risk than weather-only indices", TriggerType: models.TriggerAreaYield, TriggerSource: "NAIC-Nigeria", ThresholdValue: 70, ThresholdUnit: "pct_of_avg", PayoutAmount: 95000, PremiumAmount: 4800, CoverageRegions: []string{"Benue", "Niger", "Kaduna", "Kogi", "Taraba"}, CoveredAssets: []string{"maize", "rice", "yam", "cassava"}, MaxPayoutNGN: 400000, Season: "harvest", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-AQUA-001", Name: "Aquaculture & Fisheries Insurance", Type: models.ProductAquaculture, Description: "Protects fisherfolk against storms — wind speed + wave height triggers", TriggerType: models.TriggerWindSpeed, TriggerSource: "NiMet-Marine", ThresholdValue: 65, ThresholdUnit: "kmh", PayoutAmount: 80000, PremiumAmount: 4000, CoverageRegions: []string{"Lagos", "Rivers", "Bayelsa", "Cross-River", "Akwa-Ibom"}, CoveredAssets: []string{"fishing_boats", "nets", "catch", "aquaculture_ponds"}, MaxPayoutNGN: 350000, Season: "year_round", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-MPCI-001", Name: "Multi-Peril Crop Insurance", Type: models.ProductMultiPerilCrop, Description: "Hybrid parametric + indemnity combining weather index with named perils", TriggerType: models.TriggerRainfall, TriggerSource: "NiMet+FieldAssessors", ThresholdValue: 100, ThresholdUnit: "mm/season", PayoutAmount: 150000, PremiumAmount: 7500, CoverageRegions: []string{"All-Nigeria"}, CoveredAssets: []string{"all_crops"}, MaxPayoutNGN: 600000, Season: "year_round", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-PAST-001", Name: "Pastoral Migration Route Insurance", Type: models.ProductPastoralRoute, Description: "Insures pastoralist transhumance corridor movements with GPS + drought triggers", TriggerType: models.TriggerGPS, TriggerSource: "GPS+NDVI", ThresholdValue: 0.25, ThresholdUnit: "ndvi_along_route", PayoutAmount: 60000, PremiumAmount: 3000, CoverageRegions: []string{"Adamawa-Taraba-Benue-Corridor", "Sokoto-Zamfara-Corridor"}, CoveredAssets: []string{"cattle_in_transit", "herder_equipment"}, MaxPayoutNGN: 250000, Season: "migration", IsActive: true, CreatedAt: time.Now()},
{ID: "PROD-CARB-001", Name: "Carbon Credit Insurance", Type: models.ProductCarbonCredit, Description: "Protects farmers carbon credit revenue from climate events reducing sequestration", TriggerType: models.TriggerCarbonFlux, TriggerSource: "Verra-Registry", ThresholdValue: 30, ThresholdUnit: "pct_reduction", PayoutAmount: 200000, PremiumAmount: 10000, CoverageRegions: []string{"All-Nigeria"}, CoveredAssets: []string{"carbon_credits", "agroforestry"}, MaxPayoutNGN: 800000, Season: "year_round", IsActive: true, CreatedAt: time.Now()},
}
for i := range products {
r.products[products[i].ID] = &products[i]
}
}

func (r *Repository) GetProducts() []models.Product {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]models.Product, 0, len(r.products))
for _, p := range r.products {
result = append(result, *p)
}
return result
}

func (r *Repository) GetProduct(id string) *models.Product {
r.mu.RLock()
defer r.mu.RUnlock()
if p, ok := r.products[id]; ok {
copy := *p
return &copy
}
return nil
}

func (r *Repository) CreatePolicy(p *models.Policy) {
r.mu.Lock()
defer r.mu.Unlock()
r.policies[p.ID] = p
}

func (r *Repository) GetPolicies() []models.Policy {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]models.Policy, 0, len(r.policies))
for _, p := range r.policies {
result = append(result, *p)
}
return result
}

func (r *Repository) GetPoliciesByProduct(productID string) []models.Policy {
r.mu.RLock()
defer r.mu.RUnlock()
var result []models.Policy
for _, p := range r.policies {
if p.ProductID == productID {
result = append(result, *p)
}
}
return result
}

func (r *Repository) RecordTrigger(t *models.TriggerEvent) {
r.mu.Lock()
defer r.mu.Unlock()
r.triggers[t.ID] = t
}

func (r *Repository) GetTriggers() []models.TriggerEvent {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]models.TriggerEvent, 0, len(r.triggers))
for _, t := range r.triggers {
result = append(result, *t)
}
return result
}

func (r *Repository) RecordPayout(p *models.ClaimPayout) {
r.mu.Lock()
defer r.mu.Unlock()
r.payouts[p.ID] = p
}

func (r *Repository) GetPayouts() []models.ClaimPayout {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]models.ClaimPayout, 0, len(r.payouts))
for _, p := range r.payouts {
result = append(result, *p)
}
return result
}
Loading