From 5b2d77265671c145e7a5b3700212d1a6016f565e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 03:25:50 +0000 Subject: [PATCH 1/9] feat: Implement 40 product improvements across 6 categories (Tier 1-4) Category 1 - Climate & Agricultural Insurance (13 products, port 8140) Category 2 - Embedded Distribution (6 products, port 8141) Category 3 - Digital Consumer Products (8 products, port 8142) Category 4 - Takaful Products Suite (6 products, port 8143) Category 5 - NIIRA 2025 Compulsory Insurance (11 classes, port 8144) Category 6 - Tech Innovations (5 features, port 8145) All services use Go layered architecture with models, repository, service, and handler layers plus health and ready endpoints. Co-Authored-By: Patrick Munis --- .../cmd/server/main.go | 30 +++ agricultural-insurance-suite/go.mod | 5 + agricultural-insurance-suite/go.sum | 2 + .../internal/handlers/handlers.go | 104 +++++++++ .../internal/models/models.go | 122 +++++++++++ .../internal/repository/repository.go | 127 +++++++++++ .../internal/service/service.go | 198 ++++++++++++++++++ digital-consumer-products/cmd/server/main.go | 28 +++ digital-consumer-products/go.mod | 5 + digital-consumer-products/go.sum | 2 + .../internal/handlers/handlers.go | 105 ++++++++++ .../internal/models/models.go | 65 ++++++ .../internal/repository/repository.go | 101 +++++++++ .../internal/service/service.go | 111 ++++++++++ .../cmd/server/main.go | 28 +++ embedded-distribution-platform/go.mod | 5 + embedded-distribution-platform/go.sum | 2 + .../internal/handlers/handlers.go | 77 +++++++ .../internal/models/models.go | 59 ++++++ .../internal/repository/repository.go | 107 ++++++++++ .../internal/service/service.go | 76 +++++++ insurance-tech-innovations/cmd/server/main.go | 26 +++ insurance-tech-innovations/go.mod | 5 + insurance-tech-innovations/go.sum | 2 + .../internal/handlers/handlers.go | 75 +++++++ .../internal/models/models.go | 89 ++++++++ .../internal/service/service.go | 170 +++++++++++++++ niira-compulsory-insurance/cmd/server/main.go | 28 +++ niira-compulsory-insurance/go.mod | 5 + niira-compulsory-insurance/go.sum | 2 + .../internal/handlers/handlers.go | 82 ++++++++ .../internal/models/models.go | 71 +++++++ .../internal/repository/repository.go | 96 +++++++++ .../internal/service/service.go | 111 ++++++++++ takaful-products-suite/cmd/server/main.go | 28 +++ takaful-products-suite/go.mod | 5 + takaful-products-suite/go.sum | 2 + .../internal/handlers/handlers.go | 90 ++++++++ .../internal/models/models.go | 66 ++++++ .../internal/repository/repository.go | 109 ++++++++++ .../internal/service/service.go | 86 ++++++++ 41 files changed, 2507 insertions(+) create mode 100644 agricultural-insurance-suite/cmd/server/main.go create mode 100644 agricultural-insurance-suite/go.mod create mode 100644 agricultural-insurance-suite/go.sum create mode 100644 agricultural-insurance-suite/internal/handlers/handlers.go create mode 100644 agricultural-insurance-suite/internal/models/models.go create mode 100644 agricultural-insurance-suite/internal/repository/repository.go create mode 100644 agricultural-insurance-suite/internal/service/service.go create mode 100644 digital-consumer-products/cmd/server/main.go create mode 100644 digital-consumer-products/go.mod create mode 100644 digital-consumer-products/go.sum create mode 100644 digital-consumer-products/internal/handlers/handlers.go create mode 100644 digital-consumer-products/internal/models/models.go create mode 100644 digital-consumer-products/internal/repository/repository.go create mode 100644 digital-consumer-products/internal/service/service.go create mode 100644 embedded-distribution-platform/cmd/server/main.go create mode 100644 embedded-distribution-platform/go.mod create mode 100644 embedded-distribution-platform/go.sum create mode 100644 embedded-distribution-platform/internal/handlers/handlers.go create mode 100644 embedded-distribution-platform/internal/models/models.go create mode 100644 embedded-distribution-platform/internal/repository/repository.go create mode 100644 embedded-distribution-platform/internal/service/service.go create mode 100644 insurance-tech-innovations/cmd/server/main.go create mode 100644 insurance-tech-innovations/go.mod create mode 100644 insurance-tech-innovations/go.sum create mode 100644 insurance-tech-innovations/internal/handlers/handlers.go create mode 100644 insurance-tech-innovations/internal/models/models.go create mode 100644 insurance-tech-innovations/internal/service/service.go create mode 100644 niira-compulsory-insurance/cmd/server/main.go create mode 100644 niira-compulsory-insurance/go.mod create mode 100644 niira-compulsory-insurance/go.sum create mode 100644 niira-compulsory-insurance/internal/handlers/handlers.go create mode 100644 niira-compulsory-insurance/internal/models/models.go create mode 100644 niira-compulsory-insurance/internal/repository/repository.go create mode 100644 niira-compulsory-insurance/internal/service/service.go create mode 100644 takaful-products-suite/cmd/server/main.go create mode 100644 takaful-products-suite/go.mod create mode 100644 takaful-products-suite/go.sum create mode 100644 takaful-products-suite/internal/handlers/handlers.go create mode 100644 takaful-products-suite/internal/models/models.go create mode 100644 takaful-products-suite/internal/repository/repository.go create mode 100644 takaful-products-suite/internal/service/service.go diff --git a/agricultural-insurance-suite/cmd/server/main.go b/agricultural-insurance-suite/cmd/server/main.go new file mode 100644 index 000000000..8f0d5cccf --- /dev/null +++ b/agricultural-insurance-suite/cmd/server/main.go @@ -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)) +} diff --git a/agricultural-insurance-suite/go.mod b/agricultural-insurance-suite/go.mod new file mode 100644 index 000000000..79a0d3cae --- /dev/null +++ b/agricultural-insurance-suite/go.mod @@ -0,0 +1,5 @@ +module agricultural-insurance-suite + +go 1.22.0 + +require github.com/gorilla/mux v1.8.1 diff --git a/agricultural-insurance-suite/go.sum b/agricultural-insurance-suite/go.sum new file mode 100644 index 000000000..712833743 --- /dev/null +++ b/agricultural-insurance-suite/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/agricultural-insurance-suite/internal/handlers/handlers.go b/agricultural-insurance-suite/internal/handlers/handlers.go new file mode 100644 index 000000000..dc2a80759 --- /dev/null +++ b/agricultural-insurance-suite/internal/handlers/handlers.go @@ -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)) +} diff --git a/agricultural-insurance-suite/internal/models/models.go b/agricultural-insurance-suite/internal/models/models.go new file mode 100644 index 000000000..9bf9e3bca --- /dev/null +++ b/agricultural-insurance-suite/internal/models/models.go @@ -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"` +} diff --git a/agricultural-insurance-suite/internal/repository/repository.go b/agricultural-insurance-suite/internal/repository/repository.go new file mode 100644 index 000000000..ac13f5111 --- /dev/null +++ b/agricultural-insurance-suite/internal/repository/repository.go @@ -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 © + } + 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 +} diff --git a/agricultural-insurance-suite/internal/service/service.go b/agricultural-insurance-suite/internal/service/service.go new file mode 100644 index 000000000..c4e259ffd --- /dev/null +++ b/agricultural-insurance-suite/internal/service/service.go @@ -0,0 +1,198 @@ +package service + +import ( + "agricultural-insurance-suite/internal/models" + "agricultural-insurance-suite/internal/repository" + "fmt" + "math" + "time" +) + +type Service struct { + repo *repository.Repository +} + +func NewService(repo *repository.Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) GetAllProducts() []models.Product { + return s.repo.GetProducts() +} + +func (s *Service) GetProduct(id string) *models.Product { + return s.repo.GetProduct(id) +} + +func (s *Service) GetProductsByCategory(category string) []models.Product { + all := s.repo.GetProducts() + var result []models.Product + for _, p := range all { + if matchCategory(p.Type, category) { + result = append(result, p) + } + } + return result +} + +func matchCategory(pt models.ProductType, cat string) bool { + m := map[string][]models.ProductType{ + "climacash": {models.ProductClimaCashRain, models.ProductClimaCashDrought, models.ProductClimaCashFlood, models.ProductClimaCashHeat}, + "crop": {models.ProductWeatherIndexCrop, models.ProductAreaYieldIndex, models.ProductMultiPerilCrop, models.ProductFertiliserBundled}, + "livestock": {models.ProductLivestockIndex, models.ProductLivestockTakaful, models.ProductPastoralRoute}, + "marine": {models.ProductAquaculture}, + "carbon": {models.ProductCarbonCredit}, + } + for _, t := range m[cat] { + if pt == t { + return true + } + } + return false +} + +func (s *Service) EnrollPolicy(req models.EnrollRequest) (*models.Policy, error) { + product := s.repo.GetProduct(req.ProductID) + if product == nil { + return nil, fmt.Errorf("product not found: %s", req.ProductID) + } + if !product.IsActive { + return nil, fmt.Errorf("product %s is not currently active", req.ProductID) + } + totalVal := 0.0 + for _, a := range req.Assets { + totalVal += a.Value * float64(a.Quantity) + } + premium := calculatePremium(product, totalVal, req.Region) + policy := &models.Policy{ + ID: fmt.Sprintf("POL-%d", time.Now().UnixNano()%1000000000), + ProductID: req.ProductID, + CustomerID: req.CustomerID, + CustomerName: req.CustomerName, + Region: req.Region, + State: req.State, + LGA: req.LGA, + Assets: req.Assets, + PremiumPaid: premium, + CoverageStart: time.Now(), + CoverageEnd: time.Now().Add(365 * 24 * time.Hour), + Status: "active", + CreatedAt: time.Now(), + } + s.repo.CreatePolicy(policy) + return policy, nil +} + +func calculatePremium(product *models.Product, assetValue float64, region string) float64 { + base := product.PremiumAmount + mult := 1.0 + switch region { + case "North-East", "North-West": + mult = 1.3 + case "South-South": + mult = 1.2 + case "North-Central": + mult = 1.1 + } + af := math.Min(assetValue/1000000.0, 3.0) + return base * mult * (1 + af*0.1) +} + +func (s *Service) EvaluateTrigger(req models.TriggerRequest) (*models.TriggerEvent, error) { + products := s.repo.GetProducts() + var matched *models.Product + for i, p := range products { + if string(p.Type) == req.ProductType { + matched = &products[i] + break + } + } + if matched == nil { + return nil, fmt.Errorf("no product for type: %s", req.ProductType) + } + triggered := false + switch models.TriggerType(req.TriggerType) { + case models.TriggerRainfall: + if matched.Type == models.ProductClimaCashDrought { + triggered = req.MeasuredValue < matched.ThresholdValue + } else { + triggered = req.MeasuredValue > matched.ThresholdValue + } + case models.TriggerTemperature: + triggered = req.MeasuredValue > matched.ThresholdValue + case models.TriggerNDVI: + triggered = req.MeasuredValue < matched.ThresholdValue + case models.TriggerWindSpeed: + triggered = req.MeasuredValue > matched.ThresholdValue + case models.TriggerAreaYield: + triggered = req.MeasuredValue < matched.ThresholdValue + case models.TriggerCarbonFlux: + triggered = req.MeasuredValue > matched.ThresholdValue + } + event := &models.TriggerEvent{ + ID: fmt.Sprintf("TRG-%d", time.Now().UnixNano()%1000000000), + ProductType: req.ProductType, + TriggerType: req.TriggerType, + Region: req.Region, + MeasuredValue: req.MeasuredValue, + Threshold: matched.ThresholdValue, + Triggered: triggered, + DataSource: req.DataSource, + Timestamp: time.Now(), + } + s.repo.RecordTrigger(event) + if triggered { + policies := s.repo.GetPoliciesByProduct(matched.ID) + for _, pol := range policies { + if pol.Region == req.Region && pol.Status == "active" { + payout := &models.ClaimPayout{ + ID: fmt.Sprintf("PAY-%d", time.Now().UnixNano()%1000000000), + PolicyID: pol.ID, + TriggerID: event.ID, + Amount: matched.PayoutAmount, + Status: "pending_disbursement", + PayoutMethod: "mobile_money", + ProcessedAt: time.Now(), + } + s.repo.RecordPayout(payout) + } + } + } + return event, nil +} + +func (s *Service) GetNDVIAssessment(region string, ndviValue float64) *models.NDVIReading { + pct := ndviToPercentile(ndviValue) + cond := "normal" + if pct < 20 { + cond = "severe_drought" + } else if pct < 35 { + cond = "drought_stress" + } else if pct < 50 { + cond = "below_normal" + } else if pct >= 70 { + cond = "above_normal" + } + return &models.NDVIReading{Region: region, Value: ndviValue, Percentile: pct, Condition: cond, Timestamp: time.Now()} +} + +func ndviToPercentile(v float64) float64 { + if v <= 0.1 { + return 5 + } else if v <= 0.2 { + return 15 + } else if v <= 0.3 { + return 30 + } else if v <= 0.4 { + return 50 + } else if v <= 0.5 { + return 65 + } else if v <= 0.6 { + return 78 + } + return 90 +} + +func (s *Service) GetAllPolicies() []models.Policy { return s.repo.GetPolicies() } +func (s *Service) GetAllTriggers() []models.TriggerEvent { return s.repo.GetTriggers() } +func (s *Service) GetAllPayouts() []models.ClaimPayout { return s.repo.GetPayouts() } diff --git a/digital-consumer-products/cmd/server/main.go b/digital-consumer-products/cmd/server/main.go new file mode 100644 index 000000000..fa413d477 --- /dev/null +++ b/digital-consumer-products/cmd/server/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "digital-consumer-products/internal/handlers" + "digital-consumer-products/internal/repository" + "digital-consumer-products/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": "digital-consumer-products"}) + }).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("[digital-consumer-products] starting on :8142 — 8 consumer product lines") + log.Fatal(http.ListenAndServe(":8142", r)) +} diff --git a/digital-consumer-products/go.mod b/digital-consumer-products/go.mod new file mode 100644 index 000000000..7196a0fe5 --- /dev/null +++ b/digital-consumer-products/go.mod @@ -0,0 +1,5 @@ +module digital-consumer-products + +go 1.22.0 + +require github.com/gorilla/mux v1.8.1 diff --git a/digital-consumer-products/go.sum b/digital-consumer-products/go.sum new file mode 100644 index 000000000..712833743 --- /dev/null +++ b/digital-consumer-products/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/digital-consumer-products/internal/handlers/handlers.go b/digital-consumer-products/internal/handlers/handlers.go new file mode 100644 index 000000000..3c56e2bba --- /dev/null +++ b/digital-consumer-products/internal/handlers/handlers.go @@ -0,0 +1,105 @@ +package handlers + +import ( + "digital-consumer-products/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/consumer").Subrouter() + api.HandleFunc("/products", h.listProducts).Methods("GET") + api.HandleFunc("/products/{id}", h.getProduct).Methods("GET") + api.HandleFunc("/activate", h.activate).Methods("POST") + api.HandleFunc("/policies", h.listPolicies).Methods("GET") + api.HandleFunc("/cyber/assess", h.cyberAssess).Methods("POST") + api.HandleFunc("/hospicash/claim", h.hospiCashClaim).Methods("POST") + api.HandleFunc("/claims", h.listClaims).Methods("GET") +} + +func (h *Handler) listProducts(w http.ResponseWriter, _ *http.Request) { + p := h.svc.GetProducts() + json.NewEncoder(w).Encode(map[string]interface{}{"products": p, "count": len(p)}) +} + +func (h *Handler) getProduct(w http.ResponseWriter, r *http.Request) { + p := h.svc.GetProduct(mux.Vars(r)["id"]) + if p == nil { + http.Error(w, `{"error":"not found"}`, 404) + return + } + json.NewEncoder(w).Encode(p) +} + +func (h *Handler) activate(w http.ResponseWriter, r *http.Request) { + var req struct { + ProductID string `json:"product_id"` + CustomerID string `json:"customer_id"` + CustomerName string `json:"customer_name"` + Days int `json:"days"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + if req.Days <= 0 { + req.Days = 1 + } + p, err := h.svc.ActivatePolicy(req.ProductID, req.CustomerID, req.CustomerName, req.Days) + if err != nil { + http.Error(w, `{"error":"`+err.Error()+`"}`, 400) + return + } + w.WriteHeader(201) + json.NewEncoder(w).Encode(p) +} + +func (h *Handler) listPolicies(w http.ResponseWriter, _ *http.Request) { + p := h.svc.GetPolicies() + json.NewEncoder(w).Encode(map[string]interface{}{"policies": p, "count": len(p)}) +} + +func (h *Handler) cyberAssess(w http.ResponseWriter, r *http.Request) { + var req struct { + BusinessName string `json:"business_name"` + Industry string `json:"industry"` + EmployeeCount int `json:"employee_count"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + json.NewEncoder(w).Encode(h.svc.AssessCyberRisk(req.BusinessName, req.Industry, req.EmployeeCount)) +} + +func (h *Handler) hospiCashClaim(w http.ResponseWriter, r *http.Request) { + var req struct { + PolicyID string `json:"policy_id"` + HospitalName string `json:"hospital_name"` + AdmissionDate string `json:"admission_date"` + DischargeDate string `json:"discharge_date"` + DaysAdmitted int `json:"days_admitted"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + c, err := h.svc.ProcessHospiCashClaim(req.PolicyID, req.HospitalName, req.AdmissionDate, req.DischargeDate, req.DaysAdmitted) + if err != nil { + http.Error(w, `{"error":"`+err.Error()+`"}`, 400) + return + } + w.WriteHeader(201) + json.NewEncoder(w).Encode(c) +} + +func (h *Handler) listClaims(w http.ResponseWriter, _ *http.Request) { + c := h.svc.GetClaims() + json.NewEncoder(w).Encode(map[string]interface{}{"claims": c, "count": len(c)}) +} diff --git a/digital-consumer-products/internal/models/models.go b/digital-consumer-products/internal/models/models.go new file mode 100644 index 000000000..378fb9073 --- /dev/null +++ b/digital-consumer-products/internal/models/models.go @@ -0,0 +1,65 @@ +package models + +import "time" + +type ProductLine string + +const ( + LinePayPerDay ProductLine = "pay_per_day_motor" + LineGigWorker ProductLine = "gig_worker_ondemand" + LineSMECyber ProductLine = "sme_cyber" + LinePetInsurance ProductLine = "pet_insurance" + LineDigitalNomad ProductLine = "digital_nomad_travel" + LineSubscriptionMotor ProductLine = "subscription_motor" + LineHospiCash ProductLine = "hospi_cash" + LineFuneral ProductLine = "funeral_burial" +) + +type ConsumerProduct struct { + ID string `json:"id"` + Name string `json:"name"` + Line ProductLine `json:"product_line"` + Description string `json:"description"` + MinPremiumNGN float64 `json:"min_premium_ngn"` + MaxCoverageNGN float64 `json:"max_coverage_ngn"` + BillingCycle string `json:"billing_cycle"` + ActivationType string `json:"activation_type"` + TargetSegment string `json:"target_segment"` + IsActive bool `json:"is_active"` +} + +type ConsumerPolicy struct { + ID string `json:"id"` + ProductID string `json:"product_id"` + CustomerID string `json:"customer_id"` + CustomerName string `json:"customer_name"` + PremiumPaid float64 `json:"premium_paid_ngn"` + Coverage float64 `json:"coverage_ngn"` + Status string `json:"status"` + ActivatedAt time.Time `json:"activated_at"` + ExpiresAt time.Time `json:"expires_at"` +} + +type CyberRiskAssessment struct { + BusinessName string `json:"business_name"` + Industry string `json:"industry"` + EmployeeCount int `json:"employee_count"` + RiskScore float64 `json:"risk_score"` + RiskLevel string `json:"risk_level"` + Vulnerabilities []string `json:"vulnerabilities"` + RecommendedPlan string `json:"recommended_plan"` + PremiumNGN float64 `json:"premium_ngn"` +} + +type HospiCashClaim struct { + ID string `json:"id"` + PolicyID string `json:"policy_id"` + HospitalName string `json:"hospital_name"` + AdmissionDate string `json:"admission_date"` + DischargeDate string `json:"discharge_date"` + DaysAdmitted int `json:"days_admitted"` + DailyBenefit float64 `json:"daily_benefit_ngn"` + TotalPayout float64 `json:"total_payout_ngn"` + Status string `json:"status"` + ProcessedAt time.Time `json:"processed_at"` +} diff --git a/digital-consumer-products/internal/repository/repository.go b/digital-consumer-products/internal/repository/repository.go new file mode 100644 index 000000000..3e2507c81 --- /dev/null +++ b/digital-consumer-products/internal/repository/repository.go @@ -0,0 +1,101 @@ +package repository + +import ( + "digital-consumer-products/internal/models" + "sync" + "time" +) + +type Repository struct { + mu sync.RWMutex + products map[string]*models.ConsumerProduct + policies map[string]*models.ConsumerPolicy + claims map[string]*models.HospiCashClaim + cyberAssessments map[string]*models.CyberRiskAssessment +} + +func NewRepository() *Repository { + r := &Repository{ + products: make(map[string]*models.ConsumerProduct), + policies: make(map[string]*models.ConsumerPolicy), + claims: make(map[string]*models.HospiCashClaim), + cyberAssessments: make(map[string]*models.CyberRiskAssessment), + } + r.seed() + return r +} + +func (r *Repository) seed() { + products := []models.ConsumerProduct{ + {ID: "DCP-001", Name: "Pay-Per-Day Motor Insurance", Line: models.LinePayPerDay, Description: "Buy 1-7 days at a time — no annual commitment, micropayment friendly", MinPremiumNGN: 350, MaxCoverageNGN: 2000000, BillingCycle: "daily", ActivationType: "on_demand", TargetSegment: "informal_economy_drivers", IsActive: true}, + {ID: "DCP-002", Name: "Gig Worker On-Demand Cover", Line: models.LineGigWorker, Description: "Hourly/daily coverage for delivery riders, artisans, freelancers — activate with a swipe", MinPremiumNGN: 100, MaxCoverageNGN: 500000, BillingCycle: "hourly", ActivationType: "on_demand", TargetSegment: "gig_economy", IsActive: true}, + {ID: "DCP-003", Name: "SME Cyber Shield", Line: models.LineSMECyber, Description: "Full policy limit per-claim for unlimited events — data theft, ransomware, business interruption", MinPremiumNGN: 15000, MaxCoverageNGN: 50000000, BillingCycle: "annual", ActivationType: "underwritten", TargetSegment: "sme_digital", IsActive: true}, + {ID: "DCP-004", Name: "Pet Care Insurance", Line: models.LinePetInsurance, Description: "Accident + illness + dental + tele-vet consultations for dogs and cats", MinPremiumNGN: 2000, MaxCoverageNGN: 500000, BillingCycle: "monthly", ActivationType: "standard", TargetSegment: "urban_pet_owners", IsActive: true}, + {ID: "DCP-005", Name: "Digital Nomad Travel Cover", Line: models.LineDigitalNomad, Description: "Global health + equipment + liability for remote workers — subscription not trip-based", MinPremiumNGN: 8000, MaxCoverageNGN: 10000000, BillingCycle: "monthly", ActivationType: "subscription", TargetSegment: "tech_remote_workers", IsActive: true}, + {ID: "DCP-006", Name: "Subscription Motor Insurance", Line: models.LineSubscriptionMotor, Description: "Monthly subscription, cancel anytime, mileage-based pricing via telematics", MinPremiumNGN: 5000, MaxCoverageNGN: 5000000, BillingCycle: "monthly", ActivationType: "subscription", TargetSegment: "urban_young_drivers", IsActive: true}, + {ID: "DCP-007", Name: "Hospi-Cash Benefit", Line: models.LineHospiCash, Description: "Fixed daily cash payout during hospitalisation — no network restrictions, proof of admission = cash", MinPremiumNGN: 500, MaxCoverageNGN: 1000000, BillingCycle: "monthly", ActivationType: "standard", TargetSegment: "mass_market", IsActive: true}, + {ID: "DCP-008", Name: "Funeral & Burial Insurance", Line: models.LineFuneral, Description: "Formalises existing cultural practice — micro-premiums, instant payout on death certificate", MinPremiumNGN: 200, MaxCoverageNGN: 500000, BillingCycle: "monthly", ActivationType: "standard", TargetSegment: "mass_market", IsActive: true}, + } + for i := range products { + r.products[products[i].ID] = &products[i] + } + _ = time.Now() +} + +func (r *Repository) GetProducts() []models.ConsumerProduct { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.ConsumerProduct, 0, len(r.products)) + for _, p := range r.products { + result = append(result, *p) + } + return result +} + +func (r *Repository) GetProduct(id string) *models.ConsumerProduct { + r.mu.RLock() + defer r.mu.RUnlock() + if p, ok := r.products[id]; ok { + c := *p + return &c + } + return nil +} + +func (r *Repository) CreatePolicy(p *models.ConsumerPolicy) { + r.mu.Lock() + defer r.mu.Unlock() + r.policies[p.ID] = p +} + +func (r *Repository) GetPolicies() []models.ConsumerPolicy { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.ConsumerPolicy, 0, len(r.policies)) + for _, p := range r.policies { + result = append(result, *p) + } + return result +} + +func (r *Repository) CreateClaim(c *models.HospiCashClaim) { + r.mu.Lock() + defer r.mu.Unlock() + r.claims[c.ID] = c +} + +func (r *Repository) GetClaims() []models.HospiCashClaim { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.HospiCashClaim, 0, len(r.claims)) + for _, c := range r.claims { + result = append(result, *c) + } + return result +} + +func (r *Repository) StoreCyberAssessment(a *models.CyberRiskAssessment) { + r.mu.Lock() + defer r.mu.Unlock() + r.cyberAssessments[a.BusinessName] = a +} diff --git a/digital-consumer-products/internal/service/service.go b/digital-consumer-products/internal/service/service.go new file mode 100644 index 000000000..27ea3f824 --- /dev/null +++ b/digital-consumer-products/internal/service/service.go @@ -0,0 +1,111 @@ +package service + +import ( + "digital-consumer-products/internal/models" + "digital-consumer-products/internal/repository" + "fmt" + "math" + "time" +) + +type Service struct { + repo *repository.Repository +} + +func NewService(repo *repository.Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) GetProducts() []models.ConsumerProduct { return s.repo.GetProducts() } +func (s *Service) GetProduct(id string) *models.ConsumerProduct { return s.repo.GetProduct(id) } +func (s *Service) GetPolicies() []models.ConsumerPolicy { return s.repo.GetPolicies() } +func (s *Service) GetClaims() []models.HospiCashClaim { return s.repo.GetClaims() } + +func (s *Service) ActivatePolicy(productID, customerID, customerName string, days int) (*models.ConsumerPolicy, error) { + product := s.repo.GetProduct(productID) + if product == nil { + return nil, fmt.Errorf("product not found: %s", productID) + } + if !product.IsActive { + return nil, fmt.Errorf("product %s is inactive", productID) + } + premium := product.MinPremiumNGN * float64(days) + if product.BillingCycle == "hourly" { + premium = product.MinPremiumNGN * float64(days) * 8 + } + p := &models.ConsumerPolicy{ + ID: fmt.Sprintf("CPOL-%d", time.Now().UnixNano()%1000000000), + ProductID: productID, + CustomerID: customerID, + CustomerName: customerName, + PremiumPaid: premium, + Coverage: product.MaxCoverageNGN, + Status: "active", + ActivatedAt: time.Now(), + ExpiresAt: time.Now().Add(time.Duration(days) * 24 * time.Hour), + } + s.repo.CreatePolicy(p) + return p, nil +} + +func (s *Service) AssessCyberRisk(bizName, industry string, empCount int) *models.CyberRiskAssessment { + score := 50.0 + var vulns []string + if empCount < 10 { + score += 20 + vulns = append(vulns, "no_dedicated_it_staff") + } else if empCount < 50 { + score += 10 + vulns = append(vulns, "limited_security_budget") + } + switch industry { + case "fintech", "healthcare": + score += 15 + vulns = append(vulns, "high_value_data_target") + case "ecommerce": + score += 10 + vulns = append(vulns, "payment_data_exposure") + } + vulns = append(vulns, "phishing_risk", "ransomware_exposure") + level := "low" + plan := "basic" + premium := 15000.0 + if score >= 70 { + level = "high" + plan = "comprehensive" + premium = 75000 + } else if score >= 50 { + level = "medium" + plan = "standard" + premium = 35000 + } + premium = premium * math.Max(1, float64(empCount)/10) + a := &models.CyberRiskAssessment{ + BusinessName: bizName, Industry: industry, EmployeeCount: empCount, + RiskScore: score, RiskLevel: level, Vulnerabilities: vulns, + RecommendedPlan: plan, PremiumNGN: premium, + } + s.repo.StoreCyberAssessment(a) + return a +} + +func (s *Service) ProcessHospiCashClaim(policyID, hospital, admDate, disDate string, days int) (*models.HospiCashClaim, error) { + if days <= 0 { + return nil, fmt.Errorf("days admitted must be positive") + } + dailyBenefit := 5000.0 + claim := &models.HospiCashClaim{ + ID: fmt.Sprintf("HCC-%d", time.Now().UnixNano()%1000000000), + PolicyID: policyID, + HospitalName: hospital, + AdmissionDate: admDate, + DischargeDate: disDate, + DaysAdmitted: days, + DailyBenefit: dailyBenefit, + TotalPayout: dailyBenefit * float64(days), + Status: "approved", + ProcessedAt: time.Now(), + } + s.repo.CreateClaim(claim) + return claim, nil +} diff --git a/embedded-distribution-platform/cmd/server/main.go b/embedded-distribution-platform/cmd/server/main.go new file mode 100644 index 000000000..62b1c038d --- /dev/null +++ b/embedded-distribution-platform/cmd/server/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "embedded-distribution-platform/internal/handlers" + "embedded-distribution-platform/internal/repository" + "embedded-distribution-platform/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": "embedded-distribution-platform"}) + }).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("[embedded-distribution-platform] starting on :8141 — 6 distribution channels, 6 partners") + log.Fatal(http.ListenAndServe(":8141", r)) +} diff --git a/embedded-distribution-platform/go.mod b/embedded-distribution-platform/go.mod new file mode 100644 index 000000000..e8c60563d --- /dev/null +++ b/embedded-distribution-platform/go.mod @@ -0,0 +1,5 @@ +module embedded-distribution-platform + +go 1.22.0 + +require github.com/gorilla/mux v1.8.1 diff --git a/embedded-distribution-platform/go.sum b/embedded-distribution-platform/go.sum new file mode 100644 index 000000000..712833743 --- /dev/null +++ b/embedded-distribution-platform/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/embedded-distribution-platform/internal/handlers/handlers.go b/embedded-distribution-platform/internal/handlers/handlers.go new file mode 100644 index 000000000..2f98657bb --- /dev/null +++ b/embedded-distribution-platform/internal/handlers/handlers.go @@ -0,0 +1,77 @@ +package handlers + +import ( + "embedded-distribution-platform/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/embedded").Subrouter() + api.HandleFunc("/partners", h.listPartners).Methods("GET") + api.HandleFunc("/partners/{id}", h.getPartner).Methods("GET") + api.HandleFunc("/partners/{id}/revenue", h.getRevenue).Methods("GET") + api.HandleFunc("/products", h.listProducts).Methods("GET") + api.HandleFunc("/enroll", h.enroll).Methods("POST") + api.HandleFunc("/enrollments", h.listEnrollments).Methods("GET") +} + +func (h *Handler) listPartners(w http.ResponseWriter, _ *http.Request) { + p := h.svc.GetPartners() + json.NewEncoder(w).Encode(map[string]interface{}{"partners": p, "count": len(p)}) +} + +func (h *Handler) getPartner(w http.ResponseWriter, r *http.Request) { + p := h.svc.GetPartner(mux.Vars(r)["id"]) + if p == nil { + http.Error(w, `{"error":"not found"}`, 404) + return + } + json.NewEncoder(w).Encode(p) +} + +func (h *Handler) getRevenue(w http.ResponseWriter, r *http.Request) { + rs, err := h.svc.GetRevenueShare(mux.Vars(r)["id"]) + if err != nil { + http.Error(w, `{"error":"`+err.Error()+`"}`, 404) + return + } + json.NewEncoder(w).Encode(rs) +} + +func (h *Handler) listProducts(w http.ResponseWriter, _ *http.Request) { + p := h.svc.GetProducts() + json.NewEncoder(w).Encode(map[string]interface{}{"products": p, "count": len(p)}) +} + +func (h *Handler) enroll(w http.ResponseWriter, r *http.Request) { + var req struct { + PartnerID string `json:"partner_id"` + ProductID string `json:"product_id"` + CustomerRef string `json:"customer_ref"` + CustomerName string `json:"customer_name"` + TransactionRef string `json:"transaction_ref"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + e, err := h.svc.Enroll(req.PartnerID, req.ProductID, req.CustomerRef, req.CustomerName, req.TransactionRef) + if err != nil { + http.Error(w, `{"error":"`+err.Error()+`"}`, 400) + return + } + w.WriteHeader(201) + json.NewEncoder(w).Encode(e) +} + +func (h *Handler) listEnrollments(w http.ResponseWriter, _ *http.Request) { + e := h.svc.GetEnrollments() + json.NewEncoder(w).Encode(map[string]interface{}{"enrollments": e, "count": len(e)}) +} diff --git a/embedded-distribution-platform/internal/models/models.go b/embedded-distribution-platform/internal/models/models.go new file mode 100644 index 000000000..be6083c22 --- /dev/null +++ b/embedded-distribution-platform/internal/models/models.go @@ -0,0 +1,59 @@ +package models + +import "time" + +type ChannelType string + +const ( + ChannelLoanEmbedded ChannelType = "loan_embedded" + ChannelAirtimeBundled ChannelType = "airtime_bundled" + ChannelEcomCheckout ChannelType = "ecommerce_checkout" + ChannelRideHailing ChannelType = "ride_hailing" + ChannelSavingsLinked ChannelType = "savings_linked" + ChannelMarketplaceSDK ChannelType = "marketplace_sdk" +) + +type Partner struct { + ID string `json:"id"` + Name string `json:"name"` + Channel ChannelType `json:"channel"` + Industry string `json:"industry"` + APIKey string `json:"api_key"` + WebhookURL string `json:"webhook_url"` + CommissionPct float64 `json:"commission_pct"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` +} + +type EmbeddedProduct struct { + ID string `json:"id"` + Name string `json:"name"` + Channel ChannelType `json:"channel"` + InsuranceType string `json:"insurance_type"` + PremiumNGN float64 `json:"premium_ngn"` + CoverageNGN float64 `json:"coverage_ngn"` + Duration string `json:"duration"` + AutoEnroll bool `json:"auto_enroll"` + Description string `json:"description"` +} + +type Enrollment struct { + ID string `json:"id"` + PartnerID string `json:"partner_id"` + ProductID string `json:"product_id"` + CustomerRef string `json:"customer_ref"` + CustomerName string `json:"customer_name"` + PremiumPaid float64 `json:"premium_paid_ngn"` + Status string `json:"status"` + Channel string `json:"channel"` + TransactionRef string `json:"transaction_ref"` + CreatedAt time.Time `json:"created_at"` +} + +type RevenueShare struct { + PartnerID string `json:"partner_id"` + TotalPremiums float64 `json:"total_premiums_ngn"` + Commission float64 `json:"commission_ngn"` + NetToInsurer float64 `json:"net_to_insurer_ngn"` + Enrollments int `json:"enrollment_count"` +} diff --git a/embedded-distribution-platform/internal/repository/repository.go b/embedded-distribution-platform/internal/repository/repository.go new file mode 100644 index 000000000..68da9423d --- /dev/null +++ b/embedded-distribution-platform/internal/repository/repository.go @@ -0,0 +1,107 @@ +package repository + +import ( + "embedded-distribution-platform/internal/models" + "sync" + "time" +) + +type Repository struct { + mu sync.RWMutex + partners map[string]*models.Partner + products map[string]*models.EmbeddedProduct + enrollments map[string]*models.Enrollment +} + +func NewRepository() *Repository { + r := &Repository{ + partners: make(map[string]*models.Partner), + products: make(map[string]*models.EmbeddedProduct), + enrollments: make(map[string]*models.Enrollment), + } + r.seed() + return r +} + +func (r *Repository) seed() { + partners := []models.Partner{ + {ID: "PTR-001", Name: "PayStack Financial", Channel: models.ChannelLoanEmbedded, Industry: "fintech", APIKey: "PARTNER_KEY_PLACEHOLDER", WebhookURL: "https://paystack.example/webhook", CommissionPct: 15, IsActive: true, CreatedAt: time.Now()}, + {ID: "PTR-002", Name: "MTN MoMo Nigeria", Channel: models.ChannelAirtimeBundled, Industry: "telco", APIKey: "PARTNER_KEY_PLACEHOLDER", WebhookURL: "https://mtn.example/webhook", CommissionPct: 20, IsActive: true, CreatedAt: time.Now()}, + {ID: "PTR-003", Name: "Jumia Marketplace", Channel: models.ChannelEcomCheckout, Industry: "ecommerce", APIKey: "PARTNER_KEY_PLACEHOLDER", WebhookURL: "https://jumia.example/webhook", CommissionPct: 12, IsActive: true, CreatedAt: time.Now()}, + {ID: "PTR-004", Name: "Bolt Nigeria", Channel: models.ChannelRideHailing, Industry: "ride_hailing", APIKey: "PARTNER_KEY_PLACEHOLDER", WebhookURL: "https://bolt.example/webhook", CommissionPct: 18, IsActive: true, CreatedAt: time.Now()}, + {ID: "PTR-005", Name: "PiggyVest", Channel: models.ChannelSavingsLinked, Industry: "savings", APIKey: "PARTNER_KEY_PLACEHOLDER", WebhookURL: "https://piggyvest.example/webhook", CommissionPct: 10, IsActive: true, CreatedAt: time.Now()}, + {ID: "PTR-006", Name: "Kuda Bank", Channel: models.ChannelMarketplaceSDK, Industry: "neobank", APIKey: "PARTNER_KEY_PLACEHOLDER", WebhookURL: "https://kuda.example/webhook", CommissionPct: 14, IsActive: true, CreatedAt: time.Now()}, + } + for i := range partners { + r.partners[partners[i].ID] = &partners[i] + } + products := []models.EmbeddedProduct{ + {ID: "EMB-001", Name: "Credit Life Plus", Channel: models.ChannelLoanEmbedded, InsuranceType: "credit_life", PremiumNGN: 500, CoverageNGN: 100000, Duration: "loan_term", AutoEnroll: true, Description: "Auto-enrolled with loan disbursement — covers outstanding balance on death/disability"}, + {ID: "EMB-002", Name: "Airtime Accident Cover", Channel: models.ChannelAirtimeBundled, InsuranceType: "personal_accident", PremiumNGN: 50, CoverageNGN: 25000, Duration: "30_days", AutoEnroll: true, Description: "Bundled with N500+ data purchase — personal accident cover for 30 days"}, + {ID: "EMB-003", Name: "Device Protection", Channel: models.ChannelEcomCheckout, InsuranceType: "device_protection", PremiumNGN: 1500, CoverageNGN: 150000, Duration: "12_months", AutoEnroll: false, Description: "Offered at checkout for electronics — covers accidental damage and theft"}, + {ID: "EMB-004", Name: "Ride-Hailing Driver Cover", Channel: models.ChannelRideHailing, InsuranceType: "motor_commercial", PremiumNGN: 200, CoverageNGN: 500000, Duration: "per_day", AutoEnroll: true, Description: "Per-trip/daily coverage for gig drivers — activate/deactivate with swipe"}, + {ID: "EMB-005", Name: "Savings Guard", Channel: models.ChannelSavingsLinked, InsuranceType: "savings_protection", PremiumNGN: 300, CoverageNGN: 200000, Duration: "monthly", AutoEnroll: true, Description: "Protects savings from health emergencies — auto-deducted monthly"}, + {ID: "EMB-006", Name: "Marketplace Insurance Exchange", Channel: models.ChannelMarketplaceSDK, InsuranceType: "multi_product", PremiumNGN: 0, CoverageNGN: 0, Duration: "varies", AutoEnroll: false, Description: "B2B2C SDK — any distributor offers any insurer product via API"}, + } + for i := range products { + r.products[products[i].ID] = &products[i] + } +} + +func (r *Repository) GetPartners() []models.Partner { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.Partner, 0, len(r.partners)) + for _, p := range r.partners { + result = append(result, *p) + } + return result +} + +func (r *Repository) GetPartner(id string) *models.Partner { + r.mu.RLock() + defer r.mu.RUnlock() + if p, ok := r.partners[id]; ok { + c := *p + return &c + } + return nil +} + +func (r *Repository) GetProducts() []models.EmbeddedProduct { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.EmbeddedProduct, 0, len(r.products)) + for _, p := range r.products { + result = append(result, *p) + } + return result +} + +func (r *Repository) CreateEnrollment(e *models.Enrollment) { + r.mu.Lock() + defer r.mu.Unlock() + r.enrollments[e.ID] = e +} + +func (r *Repository) GetEnrollments() []models.Enrollment { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.Enrollment, 0, len(r.enrollments)) + for _, e := range r.enrollments { + result = append(result, *e) + } + return result +} + +func (r *Repository) GetEnrollmentsByPartner(partnerID string) []models.Enrollment { + r.mu.RLock() + defer r.mu.RUnlock() + var result []models.Enrollment + for _, e := range r.enrollments { + if e.PartnerID == partnerID { + result = append(result, *e) + } + } + return result +} diff --git a/embedded-distribution-platform/internal/service/service.go b/embedded-distribution-platform/internal/service/service.go new file mode 100644 index 000000000..3c58c06f8 --- /dev/null +++ b/embedded-distribution-platform/internal/service/service.go @@ -0,0 +1,76 @@ +package service + +import ( + "embedded-distribution-platform/internal/models" + "embedded-distribution-platform/internal/repository" + "fmt" + "time" +) + +type Service struct { + repo *repository.Repository +} + +func NewService(repo *repository.Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) GetPartners() []models.Partner { return s.repo.GetPartners() } +func (s *Service) GetPartner(id string) *models.Partner { return s.repo.GetPartner(id) } +func (s *Service) GetProducts() []models.EmbeddedProduct { return s.repo.GetProducts() } +func (s *Service) GetEnrollments() []models.Enrollment { return s.repo.GetEnrollments() } + +func (s *Service) Enroll(partnerID, productID, customerRef, customerName, txRef string) (*models.Enrollment, error) { + partner := s.repo.GetPartner(partnerID) + if partner == nil { + return nil, fmt.Errorf("partner not found: %s", partnerID) + } + if !partner.IsActive { + return nil, fmt.Errorf("partner %s is inactive", partnerID) + } + products := s.repo.GetProducts() + var product *models.EmbeddedProduct + for i, p := range products { + if p.ID == productID { + product = &products[i] + break + } + } + if product == nil { + return nil, fmt.Errorf("product not found: %s", productID) + } + e := &models.Enrollment{ + ID: fmt.Sprintf("ENR-%d", time.Now().UnixNano()%1000000000), + PartnerID: partnerID, + ProductID: productID, + CustomerRef: customerRef, + CustomerName: customerName, + PremiumPaid: product.PremiumNGN, + Status: "active", + Channel: string(partner.Channel), + TransactionRef: txRef, + CreatedAt: time.Now(), + } + s.repo.CreateEnrollment(e) + return e, nil +} + +func (s *Service) GetRevenueShare(partnerID string) (*models.RevenueShare, error) { + partner := s.repo.GetPartner(partnerID) + if partner == nil { + return nil, fmt.Errorf("partner not found: %s", partnerID) + } + enrollments := s.repo.GetEnrollmentsByPartner(partnerID) + total := 0.0 + for _, e := range enrollments { + total += e.PremiumPaid + } + commission := total * partner.CommissionPct / 100 + return &models.RevenueShare{ + PartnerID: partnerID, + TotalPremiums: total, + Commission: commission, + NetToInsurer: total - commission, + Enrollments: len(enrollments), + }, nil +} diff --git a/insurance-tech-innovations/cmd/server/main.go b/insurance-tech-innovations/cmd/server/main.go new file mode 100644 index 000000000..3719b9a44 --- /dev/null +++ b/insurance-tech-innovations/cmd/server/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "encoding/json" + "insurance-tech-innovations/internal/handlers" + "insurance-tech-innovations/internal/service" + "log" + "net/http" + + "github.com/gorilla/mux" +) + +func main() { + svc := service.NewService() + 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": "insurance-tech-innovations"}) + }).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("[insurance-tech-innovations] starting on :8145 — AI pricing, instant claims, gamification, P2P, product builder") + log.Fatal(http.ListenAndServe(":8145", r)) +} diff --git a/insurance-tech-innovations/go.mod b/insurance-tech-innovations/go.mod new file mode 100644 index 000000000..d63fe9802 --- /dev/null +++ b/insurance-tech-innovations/go.mod @@ -0,0 +1,5 @@ +module insurance-tech-innovations + +go 1.22.0 + +require github.com/gorilla/mux v1.8.1 diff --git a/insurance-tech-innovations/go.sum b/insurance-tech-innovations/go.sum new file mode 100644 index 000000000..712833743 --- /dev/null +++ b/insurance-tech-innovations/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/insurance-tech-innovations/internal/handlers/handlers.go b/insurance-tech-innovations/internal/handlers/handlers.go new file mode 100644 index 000000000..1d739343a --- /dev/null +++ b/insurance-tech-innovations/internal/handlers/handlers.go @@ -0,0 +1,75 @@ +package handlers + +import ( + "encoding/json" + "insurance-tech-innovations/internal/models" + "insurance-tech-innovations/internal/service" + "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/innovations").Subrouter() + api.HandleFunc("/pricing/dynamic", h.dynamicPrice).Methods("POST") + api.HandleFunc("/claims/instant", h.instantClaim).Methods("POST") + api.HandleFunc("/gamification/profile", h.gamificationProfile).Methods("POST") + api.HandleFunc("/p2p/pools", h.p2pPools).Methods("GET") + api.HandleFunc("/product-builder/create", h.buildProduct).Methods("POST") +} + +func (h *Handler) dynamicPrice(w http.ResponseWriter, r *http.Request) { + var req models.DynamicPriceRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + json.NewEncoder(w).Encode(h.svc.CalculateDynamicPrice(req)) +} + +func (h *Handler) instantClaim(w http.ResponseWriter, r *http.Request) { + var req models.InstantClaimRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + json.NewEncoder(w).Encode(h.svc.ProcessInstantClaim(req)) +} + +func (h *Handler) gamificationProfile(w http.ResponseWriter, r *http.Request) { + var req struct { + CustomerID string `json:"customer_id"` + StepsToday int `json:"steps_today"` + SafeDrivingDays int `json:"safe_driving_days"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + json.NewEncoder(w).Encode(h.svc.GetGamificationProfile(req.CustomerID, req.StepsToday, req.SafeDrivingDays)) +} + +func (h *Handler) p2pPools(w http.ResponseWriter, _ *http.Request) { + pools := h.svc.GetP2PPools() + json.NewEncoder(w).Encode(map[string]interface{}{"pools": pools, "count": len(pools)}) +} + +func (h *Handler) buildProduct(w http.ResponseWriter, r *http.Request) { + var req struct { + Name string `json:"name"` + Perils []string `json:"perils"` + TriggerType string `json:"trigger_type"` + PayoutMechanism string `json:"payout_mechanism"` + Distribution string `json:"distribution_channel"` + PremiumModel string `json:"premium_model"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + json.NewEncoder(w).Encode(h.svc.BuildProduct(req.Name, req.Perils, req.TriggerType, req.PayoutMechanism, req.Distribution, req.PremiumModel)) +} diff --git a/insurance-tech-innovations/internal/models/models.go b/insurance-tech-innovations/internal/models/models.go new file mode 100644 index 000000000..d8a5f5f37 --- /dev/null +++ b/insurance-tech-innovations/internal/models/models.go @@ -0,0 +1,89 @@ +package models + +import "time" + +type DynamicPriceRequest struct { + PolicyID string `json:"policy_id"` + ProductType string `json:"product_type"` + BasePremium float64 `json:"base_premium_ngn"` + DrivingScore float64 `json:"driving_score"` + ClaimsCount int `json:"claims_count_3yr"` + MileageKM float64 `json:"monthly_mileage_km"` + Region string `json:"region"` + VehicleAge int `json:"vehicle_age_years"` +} + +type DynamicPriceResult struct { + PolicyID string `json:"policy_id"` + BasePremium float64 `json:"base_premium_ngn"` + AdjustedPremium float64 `json:"adjusted_premium_ngn"` + Discount float64 `json:"discount_pct"` + Surcharge float64 `json:"surcharge_pct"` + Factors []PricingFactor `json:"factors"` + NextReviewAt time.Time `json:"next_review_at"` +} + +type PricingFactor struct { + Name string `json:"name"` + Impact float64 `json:"impact_pct"` + Reason string `json:"reason"` +} + +type InstantClaimRequest struct { + PolicyID string `json:"policy_id"` + ClaimType string `json:"claim_type"` + Region string `json:"region"` + SatelliteData bool `json:"satellite_verified"` + DamageScore float64 `json:"damage_score"` + Amount float64 `json:"amount_ngn"` +} + +type InstantClaimResult struct { + ClaimID string `json:"claim_id"` + PolicyID string `json:"policy_id"` + Decision string `json:"decision"` + Amount float64 `json:"amount_ngn"` + Confidence float64 `json:"confidence_pct"` + ProcessingMS int `json:"processing_ms"` + Method string `json:"method"` + ProcessedAt time.Time `json:"processed_at"` +} + +type GamificationProfile struct { + CustomerID string `json:"customer_id"` + Points int `json:"points"` + Level string `json:"level"` + StepsToday int `json:"steps_today"` + SafeDrivingDays int `json:"safe_driving_days"` + PremiumDiscount float64 `json:"premium_discount_pct"` + Rewards []Reward `json:"rewards"` + LossRatioImpact float64 `json:"loss_ratio_improvement_pct"` +} + +type Reward struct { + Name string `json:"name"` + Points int `json:"points_required"` + Status string `json:"status"` +} + +type P2PPool struct { + ID string `json:"id"` + Name string `json:"name"` + Members int `json:"members"` + PoolBalance float64 `json:"pool_balance_ngn"` + ClaimsPaid float64 `json:"claims_paid_ngn"` + Giveback float64 `json:"giveback_pct"` + CreatedAt time.Time `json:"created_at"` +} + +type ProductBuilderSpec struct { + ID string `json:"id"` + Name string `json:"name"` + Perils []string `json:"perils"` + TriggerType string `json:"trigger_type"` + PayoutMechanism string `json:"payout_mechanism"` + Distribution string `json:"distribution_channel"` + PremiumModel string `json:"premium_model"` + Status string `json:"status"` + CreatedInDays int `json:"created_in_days"` +} diff --git a/insurance-tech-innovations/internal/service/service.go b/insurance-tech-innovations/internal/service/service.go new file mode 100644 index 000000000..7d942bf90 --- /dev/null +++ b/insurance-tech-innovations/internal/service/service.go @@ -0,0 +1,170 @@ +package service + +import ( + "fmt" + "insurance-tech-innovations/internal/models" + "math" + "time" +) + +type Service struct{} + +func NewService() *Service { return &Service{} } + +func (s *Service) CalculateDynamicPrice(req models.DynamicPriceRequest) *models.DynamicPriceResult { + var factors []models.PricingFactor + totalAdj := 0.0 + + if req.DrivingScore >= 90 { + d := -25.0 + totalAdj += d + factors = append(factors, models.PricingFactor{Name: "safe_driver", Impact: d, Reason: "Excellent driving score (90+) — 25% discount"}) + } else if req.DrivingScore >= 70 { + d := -15.0 + totalAdj += d + factors = append(factors, models.PricingFactor{Name: "good_driver", Impact: d, Reason: "Good driving score (70-89) — 15% discount"}) + } else if req.DrivingScore < 50 { + s := 20.0 + totalAdj += s + factors = append(factors, models.PricingFactor{Name: "risky_driver", Impact: s, Reason: "Poor driving score (<50) — 20% surcharge"}) + } + + if req.ClaimsCount == 0 { + d := -10.0 + totalAdj += d + factors = append(factors, models.PricingFactor{Name: "no_claims", Impact: d, Reason: "Zero claims in 3 years — 10% NCD"}) + } else if req.ClaimsCount >= 3 { + s := 30.0 + totalAdj += s + factors = append(factors, models.PricingFactor{Name: "frequent_claims", Impact: s, Reason: "3+ claims in 3 years — 30% loading"}) + } + + if req.MileageKM < 500 { + d := -15.0 + totalAdj += d + factors = append(factors, models.PricingFactor{Name: "low_mileage", Impact: d, Reason: "Low monthly mileage (<500km) — 15% discount"}) + } else if req.MileageKM > 3000 { + s := 10.0 + totalAdj += s + factors = append(factors, models.PricingFactor{Name: "high_mileage", Impact: s, Reason: "High monthly mileage (>3000km) — 10% loading"}) + } + + if req.VehicleAge > 10 { + s := 15.0 + totalAdj += s + factors = append(factors, models.PricingFactor{Name: "old_vehicle", Impact: s, Reason: "Vehicle >10 years — 15% loading"}) + } + + adjusted := req.BasePremium * (1 + totalAdj/100) + adjusted = math.Max(adjusted, req.BasePremium*0.5) + + discount := 0.0 + surcharge := 0.0 + if totalAdj < 0 { + discount = -totalAdj + } else { + surcharge = totalAdj + } + + return &models.DynamicPriceResult{ + PolicyID: req.PolicyID, + BasePremium: req.BasePremium, + AdjustedPremium: adjusted, + Discount: discount, + Surcharge: surcharge, + Factors: factors, + NextReviewAt: time.Now().Add(24 * time.Hour), + } +} + +func (s *Service) ProcessInstantClaim(req models.InstantClaimRequest) *models.InstantClaimResult { + confidence := 50.0 + decision := "manual_review" + method := "manual" + + if req.SatelliteData { + confidence += 30 + method = "satellite_verified" + } + if req.DamageScore > 0.8 { + confidence += 15 + } else if req.DamageScore > 0.5 { + confidence += 10 + } + if req.Amount < 100000 { + confidence += 10 + } + if confidence >= 85 { + decision = "auto_approved" + } else if confidence >= 70 { + decision = "fast_track" + } + + return &models.InstantClaimResult{ + ClaimID: fmt.Sprintf("ICL-%d", time.Now().UnixNano()%1000000000), + PolicyID: req.PolicyID, + Decision: decision, + Amount: req.Amount, + Confidence: confidence, + ProcessingMS: 250, + Method: method, + ProcessedAt: time.Now(), + } +} + +func (s *Service) GetGamificationProfile(customerID string, steps, safeDays int) *models.GamificationProfile { + points := steps/1000 + safeDays*10 + level := "bronze" + discount := 0.0 + if points >= 500 { + level = "gold" + discount = 15 + } else if points >= 200 { + level = "silver" + discount = 8 + } else if points >= 50 { + level = "bronze" + discount = 3 + } + rewards := []models.Reward{ + {Name: "Free fuel voucher", Points: 100, Status: rewardStatus(points, 100)}, + {Name: "5% premium discount", Points: 200, Status: rewardStatus(points, 200)}, + {Name: "Free vehicle inspection", Points: 350, Status: rewardStatus(points, 350)}, + {Name: "20% premium discount", Points: 500, Status: rewardStatus(points, 500)}, + } + return &models.GamificationProfile{ + CustomerID: customerID, Points: points, Level: level, + StepsToday: steps, SafeDrivingDays: safeDays, + PremiumDiscount: discount, Rewards: rewards, + LossRatioImpact: discount * 0.8, + } +} + +func rewardStatus(points, required int) string { + if points >= required { + return "unlocked" + } + return "locked" +} + +func (s *Service) GetP2PPools() []models.P2PPool { + return []models.P2PPool{ + {ID: "P2P-001", Name: "Lagos Drivers Mutual", Members: 150, PoolBalance: 2250000, ClaimsPaid: 450000, Giveback: 35, CreatedAt: time.Now().Add(-180 * 24 * time.Hour)}, + {ID: "P2P-002", Name: "Ikoyi Neighbours Group", Members: 45, PoolBalance: 675000, ClaimsPaid: 120000, Giveback: 42, CreatedAt: time.Now().Add(-120 * 24 * time.Hour)}, + {ID: "P2P-003", Name: "Tech Workers Guild", Members: 200, PoolBalance: 4000000, ClaimsPaid: 800000, Giveback: 28, CreatedAt: time.Now().Add(-90 * 24 * time.Hour)}, + } +} + +func (s *Service) BuildProduct(name string, perils []string, trigger, payout, dist, premium string) *models.ProductBuilderSpec { + return &models.ProductBuilderSpec{ + ID: fmt.Sprintf("PB-%d", time.Now().UnixNano()%1000000000), + Name: name, + Perils: perils, + TriggerType: trigger, + PayoutMechanism: payout, + Distribution: dist, + PremiumModel: premium, + Status: "draft", + CreatedInDays: 3, + } +} diff --git a/niira-compulsory-insurance/cmd/server/main.go b/niira-compulsory-insurance/cmd/server/main.go new file mode 100644 index 000000000..1d0202213 --- /dev/null +++ b/niira-compulsory-insurance/cmd/server/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "niira-compulsory-insurance/internal/handlers" + "niira-compulsory-insurance/internal/repository" + "niira-compulsory-insurance/internal/service" + + "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": "niira-compulsory-insurance"}) + }).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("[niira-compulsory-insurance] starting on :8144 — 11 NIIRA 2025 compulsory classes, July 2026 deadline") + log.Fatal(http.ListenAndServe(":8144", r)) +} diff --git a/niira-compulsory-insurance/go.mod b/niira-compulsory-insurance/go.mod new file mode 100644 index 000000000..223355676 --- /dev/null +++ b/niira-compulsory-insurance/go.mod @@ -0,0 +1,5 @@ +module niira-compulsory-insurance + +go 1.22.0 + +require github.com/gorilla/mux v1.8.1 diff --git a/niira-compulsory-insurance/go.sum b/niira-compulsory-insurance/go.sum new file mode 100644 index 000000000..712833743 --- /dev/null +++ b/niira-compulsory-insurance/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/niira-compulsory-insurance/internal/handlers/handlers.go b/niira-compulsory-insurance/internal/handlers/handlers.go new file mode 100644 index 000000000..cf0e26baf --- /dev/null +++ b/niira-compulsory-insurance/internal/handlers/handlers.go @@ -0,0 +1,82 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "niira-compulsory-insurance/internal/service" + "strconv" + + "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/niira").Subrouter() + api.HandleFunc("/classes", h.listClasses).Methods("GET") + api.HandleFunc("/classes/{id}", h.getClass).Methods("GET") + api.HandleFunc("/compliance/check", h.checkCompliance).Methods("POST") + api.HandleFunc("/policies", h.listPolicies).Methods("GET") + api.HandleFunc("/policies/issue", h.issuePolicy).Methods("POST") + api.HandleFunc("/certificates", h.listCertificates).Methods("GET") +} + +func (h *Handler) listClasses(w http.ResponseWriter, _ *http.Request) { + p := h.svc.GetProducts() + json.NewEncoder(w).Encode(map[string]interface{}{"classes": p, "count": len(p), "compliance_deadline": "2026-07-30", "regulatory_framework": "NIIRA 2025"}) +} + +func (h *Handler) getClass(w http.ResponseWriter, r *http.Request) { + p := h.svc.GetProduct(mux.Vars(r)["id"]) + if p == nil { + http.Error(w, `{"error":"not found"}`, 404) + return + } + json.NewEncoder(w).Encode(p) +} + +func (h *Handler) checkCompliance(w http.ResponseWriter, r *http.Request) { + var req struct { + BusinessName string `json:"business_name"` + BusinessType string `json:"business_type"` + EmployeeCount string `json:"employee_count"` + ExistingClasses []string `json:"existing_classes"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + emp, _ := strconv.Atoi(req.EmployeeCount) + json.NewEncoder(w).Encode(h.svc.CheckCompliance(req.BusinessName, req.BusinessType, emp, req.ExistingClasses)) +} + +func (h *Handler) listPolicies(w http.ResponseWriter, _ *http.Request) { + p := h.svc.GetPolicies() + json.NewEncoder(w).Encode(map[string]interface{}{"policies": p, "count": len(p)}) +} + +func (h *Handler) issuePolicy(w http.ResponseWriter, r *http.Request) { + var req struct { + ProductID string `json:"product_id"` + BusinessName string `json:"business_name"` + RCNumber string `json:"rc_number"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + p, err := h.svc.IssuePolicy(req.ProductID, req.BusinessName, req.RCNumber) + if err != nil { + http.Error(w, `{"error":"`+err.Error()+`"}`, 400) + return + } + w.WriteHeader(201) + json.NewEncoder(w).Encode(p) +} + +func (h *Handler) listCertificates(w http.ResponseWriter, _ *http.Request) { + c := h.svc.GetCertificates() + json.NewEncoder(w).Encode(map[string]interface{}{"certificates": c, "count": len(c)}) +} diff --git a/niira-compulsory-insurance/internal/models/models.go b/niira-compulsory-insurance/internal/models/models.go new file mode 100644 index 000000000..b2cf8cc62 --- /dev/null +++ b/niira-compulsory-insurance/internal/models/models.go @@ -0,0 +1,71 @@ +package models + +import "time" + +type CompulsoryClass string + +const ( + ClassMotorTP CompulsoryClass = "motor_third_party" + ClassEmployerLiability CompulsoryClass = "employer_liability" + ClassBuildingInsurance CompulsoryClass = "building_insurance" + ClassProfessionalPI CompulsoryClass = "professional_indemnity" + ClassProductLiability CompulsoryClass = "product_liability" + ClassHealthcarePI CompulsoryClass = "healthcare_professional_indemnity" + ClassMarineCargo CompulsoryClass = "marine_cargo" + ClassPublicLiability CompulsoryClass = "public_liability" + ClassGroupLife CompulsoryClass = "group_life" + ClassOccupiers CompulsoryClass = "occupiers_liability" + ClassContractorsAllRisk CompulsoryClass = "contractors_all_risk" +) + +type CompulsoryProduct struct { + ID string `json:"id"` + Name string `json:"name"` + Class CompulsoryClass `json:"class"` + Description string `json:"description"` + NIIRASection string `json:"niira_section"` + MinCoverageNGN float64 `json:"min_coverage_ngn"` + BasePremiumNGN float64 `json:"base_premium_ngn"` + ApplicableTo []string `json:"applicable_to"` + ComplianceDeadline string `json:"compliance_deadline"` + PenaltyForNonCompliance string `json:"penalty_for_non_compliance"` + IsActive bool `json:"is_active"` +} + +type ComplianceCertificate struct { + ID string `json:"id"` + PolicyID string `json:"policy_id"` + BusinessName string `json:"business_name"` + RCNumber string `json:"rc_number"` + Class string `json:"class"` + CertificateNo string `json:"certificate_no"` + IssuedDate string `json:"issued_date"` + ExpiryDate string `json:"expiry_date"` + NAICOMRef string `json:"naicom_ref"` + Status string `json:"status"` + GeneratedAt time.Time `json:"generated_at"` +} + +type ComplianceCheck struct { + BusinessName string `json:"business_name"` + BusinessType string `json:"business_type"` + EmployeeCount int `json:"employee_count"` + RequiredClasses []string `json:"required_classes"` + MissingClasses []string `json:"missing_classes"` + IsCompliant bool `json:"is_compliant"` + TotalPremiumNGN float64 `json:"total_premium_ngn"` + Deadline string `json:"deadline"` +} + +type NIIRAPolicy struct { + ID string `json:"id"` + ProductID string `json:"product_id"` + BusinessName string `json:"business_name"` + RCNumber string `json:"rc_number"` + PremiumPaid float64 `json:"premium_paid_ngn"` + CoverageNGN float64 `json:"coverage_ngn"` + Status string `json:"status"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/niira-compulsory-insurance/internal/repository/repository.go b/niira-compulsory-insurance/internal/repository/repository.go new file mode 100644 index 000000000..9d98143d4 --- /dev/null +++ b/niira-compulsory-insurance/internal/repository/repository.go @@ -0,0 +1,96 @@ +package repository + +import ( + "niira-compulsory-insurance/internal/models" + "sync" + "time" +) + +type Repository struct { + mu sync.RWMutex + products map[string]*models.CompulsoryProduct + policies map[string]*models.NIIRAPolicy + certs map[string]*models.ComplianceCertificate +} + +func NewRepository() *Repository { + r := &Repository{ + products: make(map[string]*models.CompulsoryProduct), + policies: make(map[string]*models.NIIRAPolicy), + certs: make(map[string]*models.ComplianceCertificate), + } + r.seed() + return r +} + +func (r *Repository) seed() { + products := []models.CompulsoryProduct{ + {ID: "NIIRA-001", Name: "Motor Third Party", Class: models.ClassMotorTP, Description: "Mandatory third-party liability for all motor vehicles", NIIRASection: "Section 68", MinCoverageNGN: 1000000, BasePremiumNGN: 5000, ApplicableTo: []string{"vehicle_owners"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "N250,000 fine or 1 year imprisonment", IsActive: true}, + {ID: "NIIRA-002", Name: "Employer Liability", Class: models.ClassEmployerLiability, Description: "Coverage for employer obligations to employees for work-related injuries", NIIRASection: "Section 73", MinCoverageNGN: 5000000, BasePremiumNGN: 25000, ApplicableTo: []string{"employers_5plus"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "N500,000 fine", IsActive: true}, + {ID: "NIIRA-003", Name: "Building Insurance", Class: models.ClassBuildingInsurance, Description: "Fire and special perils coverage for buildings exceeding 2 floors", NIIRASection: "Section 64", MinCoverageNGN: 10000000, BasePremiumNGN: 50000, ApplicableTo: []string{"building_owners_2plus_floors"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "N1,000,000 fine", IsActive: true}, + {ID: "NIIRA-004", Name: "Professional Indemnity", Class: models.ClassProfessionalPI, Description: "NEW under NIIRA 2025 — mandatory for doctors, lawyers, accountants, engineers", NIIRASection: "Section 75A", MinCoverageNGN: 10000000, BasePremiumNGN: 45000, ApplicableTo: []string{"doctors", "lawyers", "accountants", "engineers", "architects"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "License suspension + N500,000 fine", IsActive: true}, + {ID: "NIIRA-005", Name: "Product Liability", Class: models.ClassProductLiability, Description: "NEW under NIIRA 2025 — mandatory for manufacturers of consumer products", NIIRASection: "Section 75B", MinCoverageNGN: 20000000, BasePremiumNGN: 75000, ApplicableTo: []string{"food_manufacturers", "pharma", "consumer_goods", "electronics"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "N1,000,000 fine + product recall liability", IsActive: true}, + {ID: "NIIRA-006", Name: "Healthcare Professional Indemnity", Class: models.ClassHealthcarePI, Description: "NEW under NIIRA 2025 — medical malpractice coverage for healthcare practitioners", NIIRASection: "Section 75C", MinCoverageNGN: 15000000, BasePremiumNGN: 60000, ApplicableTo: []string{"hospitals", "clinics", "pharmacies", "diagnostic_centres"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "N750,000 fine + license review", IsActive: true}, + {ID: "NIIRA-007", Name: "Marine Cargo", Class: models.ClassMarineCargo, Description: "Expanded under NIIRA 2025 — all imports must be insured locally", NIIRASection: "Section 71A", MinCoverageNGN: 50000000, BasePremiumNGN: 150000, ApplicableTo: []string{"importers", "exporters", "shipping_companies"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "Cargo seizure + N2,000,000 fine", IsActive: true}, + {ID: "NIIRA-008", Name: "Public Liability", Class: models.ClassPublicLiability, Description: "Expanded under NIIRA 2025 — mandatory for public-facing businesses", NIIRASection: "Section 76A", MinCoverageNGN: 5000000, BasePremiumNGN: 30000, ApplicableTo: []string{"hotels", "malls", "cinemas", "transport_operators", "event_venues"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "N500,000 fine + closure order", IsActive: true}, + {ID: "NIIRA-009", Name: "Group Life", Class: models.ClassGroupLife, Description: "Mandatory life insurance for employees by employers with 3+ staff", NIIRASection: "Section 73B", MinCoverageNGN: 3000000, BasePremiumNGN: 15000, ApplicableTo: []string{"employers_3plus"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "N250,000 fine", IsActive: true}, + {ID: "NIIRA-010", Name: "Occupiers Liability", Class: models.ClassOccupiers, Description: "Coverage for injuries to visitors on business premises", NIIRASection: "Section 76B", MinCoverageNGN: 3000000, BasePremiumNGN: 20000, ApplicableTo: []string{"office_buildings", "warehouses", "factories"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "N300,000 fine", IsActive: true}, + {ID: "NIIRA-011", Name: "Contractors All Risk", Class: models.ClassContractorsAllRisk, Description: "Coverage for construction projects including third-party liability", NIIRASection: "Section 77", MinCoverageNGN: 100000000, BasePremiumNGN: 250000, ApplicableTo: []string{"construction_companies", "contractors"}, ComplianceDeadline: "2026-07-30", PenaltyForNonCompliance: "Contract nullification + N5,000,000 fine", IsActive: true}, + } + for i := range products { + r.products[products[i].ID] = &products[i] + } + _ = time.Now() +} + +func (r *Repository) GetProducts() []models.CompulsoryProduct { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.CompulsoryProduct, 0, len(r.products)) + for _, p := range r.products { + result = append(result, *p) + } + return result +} + +func (r *Repository) GetProduct(id string) *models.CompulsoryProduct { + r.mu.RLock() + defer r.mu.RUnlock() + if p, ok := r.products[id]; ok { + c := *p + return &c + } + return nil +} + +func (r *Repository) CreatePolicy(p *models.NIIRAPolicy) { + r.mu.Lock() + defer r.mu.Unlock() + r.policies[p.ID] = p +} + +func (r *Repository) GetPolicies() []models.NIIRAPolicy { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.NIIRAPolicy, 0, len(r.policies)) + for _, p := range r.policies { + result = append(result, *p) + } + return result +} + +func (r *Repository) CreateCertificate(c *models.ComplianceCertificate) { + r.mu.Lock() + defer r.mu.Unlock() + r.certs[c.ID] = c +} + +func (r *Repository) GetCertificates() []models.ComplianceCertificate { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.ComplianceCertificate, 0, len(r.certs)) + for _, c := range r.certs { + result = append(result, *c) + } + return result +} diff --git a/niira-compulsory-insurance/internal/service/service.go b/niira-compulsory-insurance/internal/service/service.go new file mode 100644 index 000000000..c25035be4 --- /dev/null +++ b/niira-compulsory-insurance/internal/service/service.go @@ -0,0 +1,111 @@ +package service + +import ( + "fmt" + "niira-compulsory-insurance/internal/models" + "niira-compulsory-insurance/internal/repository" + "time" +) + +type Service struct { + repo *repository.Repository +} + +func NewService(repo *repository.Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) GetProducts() []models.CompulsoryProduct { return s.repo.GetProducts() } +func (s *Service) GetProduct(id string) *models.CompulsoryProduct { return s.repo.GetProduct(id) } +func (s *Service) GetPolicies() []models.NIIRAPolicy { return s.repo.GetPolicies() } +func (s *Service) GetCertificates() []models.ComplianceCertificate { return s.repo.GetCertificates() } + +func (s *Service) CheckCompliance(bizName, bizType string, empCount int, existingClasses []string) *models.ComplianceCheck { + required := determineRequired(bizType, empCount) + existingSet := make(map[string]bool) + for _, c := range existingClasses { + existingSet[c] = true + } + var missing []string + totalPremium := 0.0 + for _, req := range required { + if !existingSet[req] { + missing = append(missing, req) + products := s.repo.GetProducts() + for _, p := range products { + if string(p.Class) == req { + totalPremium += p.BasePremiumNGN + break + } + } + } + } + return &models.ComplianceCheck{ + BusinessName: bizName, + BusinessType: bizType, + EmployeeCount: empCount, + RequiredClasses: required, + MissingClasses: missing, + IsCompliant: len(missing) == 0, + TotalPremiumNGN: totalPremium, + Deadline: "2026-07-30", + } +} + +func determineRequired(bizType string, empCount int) []string { + required := []string{"motor_third_party"} + if empCount >= 3 { + required = append(required, "employer_liability", "group_life") + } + switch bizType { + case "hospital", "clinic", "pharmacy": + required = append(required, "healthcare_professional_indemnity", "public_liability", "occupiers_liability") + case "law_firm", "accounting_firm", "engineering_firm": + required = append(required, "professional_indemnity") + case "manufacturer", "food_producer", "pharma": + required = append(required, "product_liability") + case "hotel", "mall", "cinema", "event_venue": + required = append(required, "public_liability", "occupiers_liability") + case "importer", "exporter", "shipping": + required = append(required, "marine_cargo") + case "construction", "contractor": + required = append(required, "contractors_all_risk", "occupiers_liability") + } + return required +} + +func (s *Service) IssuePolicy(productID, bizName, rcNumber string) (*models.NIIRAPolicy, error) { + product := s.repo.GetProduct(productID) + if product == nil { + return nil, fmt.Errorf("product not found: %s", productID) + } + now := time.Now() + policy := &models.NIIRAPolicy{ + ID: fmt.Sprintf("NIIRA-POL-%d", now.UnixNano()%1000000000), + ProductID: productID, + BusinessName: bizName, + RCNumber: rcNumber, + PremiumPaid: product.BasePremiumNGN, + CoverageNGN: product.MinCoverageNGN, + Status: "active", + StartDate: now.Format("2006-01-02"), + EndDate: now.Add(365 * 24 * time.Hour).Format("2006-01-02"), + CreatedAt: now, + } + s.repo.CreatePolicy(policy) + cert := &models.ComplianceCertificate{ + ID: fmt.Sprintf("CERT-%d", now.UnixNano()%1000000000), + PolicyID: policy.ID, + BusinessName: bizName, + RCNumber: rcNumber, + Class: string(product.Class), + CertificateNo: fmt.Sprintf("NAICOM/%s/%d", product.NIIRASection, now.UnixNano()%100000), + IssuedDate: now.Format("2006-01-02"), + ExpiryDate: now.Add(365 * 24 * time.Hour).Format("2006-01-02"), + NAICOMRef: fmt.Sprintf("NAI-REF-%d", now.UnixNano()%100000), + Status: "valid", + GeneratedAt: now, + } + s.repo.CreateCertificate(cert) + return policy, nil +} diff --git a/takaful-products-suite/cmd/server/main.go b/takaful-products-suite/cmd/server/main.go new file mode 100644 index 000000000..66c29ca57 --- /dev/null +++ b/takaful-products-suite/cmd/server/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "takaful-products-suite/internal/handlers" + "takaful-products-suite/internal/repository" + "takaful-products-suite/internal/service" + + "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": "takaful-products-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("[takaful-products-suite] starting on :8143 — 6 Sharia-compliant products, 6 pools") + log.Fatal(http.ListenAndServe(":8143", r)) +} diff --git a/takaful-products-suite/go.mod b/takaful-products-suite/go.mod new file mode 100644 index 000000000..f8a6e7e83 --- /dev/null +++ b/takaful-products-suite/go.mod @@ -0,0 +1,5 @@ +module takaful-products-suite + +go 1.22.0 + +require github.com/gorilla/mux v1.8.1 diff --git a/takaful-products-suite/go.sum b/takaful-products-suite/go.sum new file mode 100644 index 000000000..712833743 --- /dev/null +++ b/takaful-products-suite/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/takaful-products-suite/internal/handlers/handlers.go b/takaful-products-suite/internal/handlers/handlers.go new file mode 100644 index 000000000..4a3e6190d --- /dev/null +++ b/takaful-products-suite/internal/handlers/handlers.go @@ -0,0 +1,90 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "takaful-products-suite/internal/service" + + "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/takaful-products").Subrouter() + api.HandleFunc("/products", h.listProducts).Methods("GET") + api.HandleFunc("/products/{id}", h.getProduct).Methods("GET") + api.HandleFunc("/products/{id}/compliance", h.checkCompliance).Methods("GET") + api.HandleFunc("/pools", h.listPools).Methods("GET") + api.HandleFunc("/pools/{id}", h.getPool).Methods("GET") + api.HandleFunc("/pools/{id}/surplus/distribute", h.distributeSurplus).Methods("POST") + api.HandleFunc("/join", h.joinPool).Methods("POST") + api.HandleFunc("/memberships", h.listMemberships).Methods("GET") +} + +func (h *Handler) listProducts(w http.ResponseWriter, _ *http.Request) { + p := h.svc.GetProducts() + json.NewEncoder(w).Encode(map[string]interface{}{"products": p, "count": len(p)}) +} + +func (h *Handler) getProduct(w http.ResponseWriter, r *http.Request) { + p := h.svc.GetProduct(mux.Vars(r)["id"]) + if p == nil { + http.Error(w, `{"error":"not found"}`, 404) + return + } + json.NewEncoder(w).Encode(p) +} + +func (h *Handler) checkCompliance(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(h.svc.CheckShariaCompliance(mux.Vars(r)["id"])) +} + +func (h *Handler) listPools(w http.ResponseWriter, _ *http.Request) { + p := h.svc.GetPools() + json.NewEncoder(w).Encode(map[string]interface{}{"pools": p, "count": len(p)}) +} + +func (h *Handler) getPool(w http.ResponseWriter, r *http.Request) { + p := h.svc.GetPool(mux.Vars(r)["id"]) + if p == nil { + http.Error(w, `{"error":"not found"}`, 404) + return + } + json.NewEncoder(w).Encode(p) +} + +func (h *Handler) distributeSurplus(w http.ResponseWriter, r *http.Request) { + d, err := h.svc.DistributeSurplus(mux.Vars(r)["id"]) + if err != nil { + http.Error(w, `{"error":"`+err.Error()+`"}`, 400) + return + } + json.NewEncoder(w).Encode(d) +} + +func (h *Handler) joinPool(w http.ResponseWriter, r *http.Request) { + var req struct { + ProductID string `json:"product_id"` + MemberName string `json:"member_name"` + MemberID string `json:"member_id"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid body"}`, 400) + return + } + m, err := h.svc.JoinPool(req.ProductID, req.MemberName, req.MemberID) + if err != nil { + http.Error(w, `{"error":"`+err.Error()+`"}`, 400) + return + } + w.WriteHeader(201) + json.NewEncoder(w).Encode(m) +} + +func (h *Handler) listMemberships(w http.ResponseWriter, _ *http.Request) { + m := h.svc.GetMemberships() + json.NewEncoder(w).Encode(map[string]interface{}{"memberships": m, "count": len(m)}) +} diff --git a/takaful-products-suite/internal/models/models.go b/takaful-products-suite/internal/models/models.go new file mode 100644 index 000000000..45523abbb --- /dev/null +++ b/takaful-products-suite/internal/models/models.go @@ -0,0 +1,66 @@ +package models + +import "time" + +type TakafulProductType string + +const ( + TakafulCropInsurance TakafulProductType = "takaful_crop" + TakafulLivestockIBLT TakafulProductType = "takaful_livestock_iblt" + TakafulMotorTP TakafulProductType = "takaful_motor_tp" + TakafulHospiCash TakafulProductType = "takaful_hospi_cash" + TakafulEducation TakafulProductType = "takaful_education" + TakafulHajjUmrah TakafulProductType = "takaful_hajj_umrah" +) + +type TakafulProduct struct { + ID string `json:"id"` + Name string `json:"name"` + Type TakafulProductType `json:"type"` + Description string `json:"description"` + ContributionNGN float64 `json:"contribution_ngn"` + CoverageNGN float64 `json:"coverage_ngn"` + SurplusSharingPct float64 `json:"surplus_sharing_pct"` + WakalaFeePct float64 `json:"wakala_fee_pct"` + PoolID string `json:"pool_id"` + ShariaApproved bool `json:"sharia_approved"` + IsActive bool `json:"is_active"` +} + +type TakafulMembership struct { + ID string `json:"id"` + ProductID string `json:"product_id"` + MemberName string `json:"member_name"` + MemberID string `json:"member_id"` + ContributionPaid float64 `json:"contribution_paid_ngn"` + PoolID string `json:"pool_id"` + Status string `json:"status"` + JoinedAt time.Time `json:"joined_at"` +} + +type TakafulPool struct { + ID string `json:"id"` + Name string `json:"name"` + ProductType string `json:"product_type"` + TotalContributions float64 `json:"total_contributions_ngn"` + TotalClaims float64 `json:"total_claims_ngn"` + Surplus float64 `json:"surplus_ngn"` + MemberCount int `json:"member_count"` + WakalaFeeCollected float64 `json:"wakala_fee_collected_ngn"` +} + +type SurplusDistribution struct { + PoolID string `json:"pool_id"` + TotalSurplus float64 `json:"total_surplus_ngn"` + MemberCount int `json:"member_count"` + PerMemberShare float64 `json:"per_member_share_ngn"` + DistributedAt time.Time `json:"distributed_at"` +} + +type ShariaCompliance struct { + ProductID string `json:"product_id"` + IsCompliant bool `json:"is_compliant"` + Principles []string `json:"principles_met"` + BoardApproval string `json:"board_approval_status"` + ReviewDate string `json:"review_date"` +} diff --git a/takaful-products-suite/internal/repository/repository.go b/takaful-products-suite/internal/repository/repository.go new file mode 100644 index 000000000..60eacffea --- /dev/null +++ b/takaful-products-suite/internal/repository/repository.go @@ -0,0 +1,109 @@ +package repository + +import ( + "sync" + "takaful-products-suite/internal/models" +) + +type Repository struct { + mu sync.RWMutex + products map[string]*models.TakafulProduct + memberships map[string]*models.TakafulMembership + pools map[string]*models.TakafulPool +} + +func NewRepository() *Repository { + r := &Repository{ + products: make(map[string]*models.TakafulProduct), + memberships: make(map[string]*models.TakafulMembership), + pools: make(map[string]*models.TakafulPool), + } + r.seed() + return r +} + +func (r *Repository) seed() { + products := []models.TakafulProduct{ + {ID: "TKF-001", Name: "Takaful Crop Insurance", Type: models.TakafulCropInsurance, Description: "Sharia-compliant mutual weather-index crop insurance — takaful structure for Muslim farming communities", ContributionNGN: 3500, CoverageNGN: 150000, SurplusSharingPct: 70, WakalaFeePct: 25, PoolID: "POOL-CROP", ShariaApproved: true, IsActive: true}, + {ID: "TKF-002", Name: "Takaful IBLT Livestock", Type: models.TakafulLivestockIBLT, Description: "NDVI-based livestock protection in Takaful wrapper — for Northern Nigeria pastoralists", ContributionNGN: 5000, CoverageNGN: 300000, SurplusSharingPct: 70, WakalaFeePct: 22, PoolID: "POOL-LIVESTOCK", ShariaApproved: true, IsActive: true}, + {ID: "TKF-003", Name: "Takaful Motor Third Party", Type: models.TakafulMotorTP, Description: "Compulsory third-party motor in Takaful structure — for Northern Nigeria market", ContributionNGN: 8000, CoverageNGN: 5000000, SurplusSharingPct: 60, WakalaFeePct: 30, PoolID: "POOL-MOTOR", ShariaApproved: true, IsActive: true}, + {ID: "TKF-004", Name: "Takaful Hospi-Cash", Type: models.TakafulHospiCash, Description: "Fixed daily benefit during hospitalisation from participants risk pool — surplus shared", ContributionNGN: 1500, CoverageNGN: 500000, SurplusSharingPct: 75, WakalaFeePct: 20, PoolID: "POOL-HEALTH", ShariaApproved: true, IsActive: true}, + {ID: "TKF-005", Name: "Takaful Education Savings", Type: models.TakafulEducation, Description: "Mudharabah-based investment + group Takaful coverage for children education", ContributionNGN: 5000, CoverageNGN: 2000000, SurplusSharingPct: 65, WakalaFeePct: 25, PoolID: "POOL-EDUCATION", ShariaApproved: true, IsActive: true}, + {ID: "TKF-006", Name: "Takaful Hajj & Umrah Travel", Type: models.TakafulHajjUmrah, Description: "Covers medical, trip cancellation, lost luggage for pilgrimage — Nigeria-to-Saudi corridor", ContributionNGN: 15000, CoverageNGN: 5000000, SurplusSharingPct: 60, WakalaFeePct: 28, PoolID: "POOL-HAJJ", ShariaApproved: true, IsActive: true}, + } + for i := range products { + r.products[products[i].ID] = &products[i] + } + pools := []models.TakafulPool{ + {ID: "POOL-CROP", Name: "Crop Takaful Pool", ProductType: "takaful_crop", TotalContributions: 45000000, TotalClaims: 12000000, Surplus: 33000000, MemberCount: 12857, WakalaFeeCollected: 11250000}, + {ID: "POOL-LIVESTOCK", Name: "Livestock Takaful Pool", ProductType: "takaful_livestock_iblt", TotalContributions: 28000000, TotalClaims: 8500000, Surplus: 19500000, MemberCount: 5600, WakalaFeeCollected: 6160000}, + {ID: "POOL-MOTOR", Name: "Motor Takaful Pool", ProductType: "takaful_motor_tp", TotalContributions: 65000000, TotalClaims: 35000000, Surplus: 30000000, MemberCount: 8125, WakalaFeeCollected: 19500000}, + {ID: "POOL-HEALTH", Name: "Health Takaful Pool", ProductType: "takaful_hospi_cash", TotalContributions: 18000000, TotalClaims: 5200000, Surplus: 12800000, MemberCount: 12000, WakalaFeeCollected: 3600000}, + {ID: "POOL-EDUCATION", Name: "Education Takaful Pool", ProductType: "takaful_education", TotalContributions: 35000000, TotalClaims: 2000000, Surplus: 33000000, MemberCount: 7000, WakalaFeeCollected: 8750000}, + {ID: "POOL-HAJJ", Name: "Hajj Takaful Pool", ProductType: "takaful_hajj_umrah", TotalContributions: 22000000, TotalClaims: 6800000, Surplus: 15200000, MemberCount: 1467, WakalaFeeCollected: 6160000}, + } + for i := range pools { + r.pools[pools[i].ID] = &pools[i] + } +} + +func (r *Repository) GetProducts() []models.TakafulProduct { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.TakafulProduct, 0, len(r.products)) + for _, p := range r.products { + result = append(result, *p) + } + return result +} + +func (r *Repository) GetProduct(id string) *models.TakafulProduct { + r.mu.RLock() + defer r.mu.RUnlock() + if p, ok := r.products[id]; ok { + c := *p + return &c + } + return nil +} + +func (r *Repository) GetPools() []models.TakafulPool { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.TakafulPool, 0, len(r.pools)) + for _, p := range r.pools { + result = append(result, *p) + } + return result +} + +func (r *Repository) GetPool(id string) *models.TakafulPool { + r.mu.RLock() + defer r.mu.RUnlock() + if p, ok := r.pools[id]; ok { + c := *p + return &c + } + return nil +} + +func (r *Repository) CreateMembership(m *models.TakafulMembership) { + r.mu.Lock() + defer r.mu.Unlock() + r.memberships[m.ID] = m + if pool, ok := r.pools[m.PoolID]; ok { + pool.TotalContributions += m.ContributionPaid + pool.MemberCount++ + pool.Surplus += m.ContributionPaid * 0.75 + } +} + +func (r *Repository) GetMemberships() []models.TakafulMembership { + r.mu.RLock() + defer r.mu.RUnlock() + result := make([]models.TakafulMembership, 0, len(r.memberships)) + for _, m := range r.memberships { + result = append(result, *m) + } + return result +} diff --git a/takaful-products-suite/internal/service/service.go b/takaful-products-suite/internal/service/service.go new file mode 100644 index 000000000..55204a220 --- /dev/null +++ b/takaful-products-suite/internal/service/service.go @@ -0,0 +1,86 @@ +package service + +import ( + "fmt" + "takaful-products-suite/internal/models" + "takaful-products-suite/internal/repository" + "time" +) + +type Service struct { + repo *repository.Repository +} + +func NewService(repo *repository.Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) GetProducts() []models.TakafulProduct { return s.repo.GetProducts() } +func (s *Service) GetProduct(id string) *models.TakafulProduct { return s.repo.GetProduct(id) } +func (s *Service) GetPools() []models.TakafulPool { return s.repo.GetPools() } +func (s *Service) GetPool(id string) *models.TakafulPool { return s.repo.GetPool(id) } +func (s *Service) GetMemberships() []models.TakafulMembership { return s.repo.GetMemberships() } + +func (s *Service) JoinPool(productID, memberName, memberID string) (*models.TakafulMembership, error) { + product := s.repo.GetProduct(productID) + if product == nil { + return nil, fmt.Errorf("product not found: %s", productID) + } + if !product.IsActive || !product.ShariaApproved { + return nil, fmt.Errorf("product %s not available", productID) + } + wakalaFee := product.ContributionNGN * product.WakalaFeePct / 100 + netContribution := product.ContributionNGN - wakalaFee + _ = netContribution + m := &models.TakafulMembership{ + ID: fmt.Sprintf("TKM-%d", time.Now().UnixNano()%1000000000), + ProductID: productID, + MemberName: memberName, + MemberID: memberID, + ContributionPaid: product.ContributionNGN, + PoolID: product.PoolID, + Status: "active", + JoinedAt: time.Now(), + } + s.repo.CreateMembership(m) + return m, nil +} + +func (s *Service) DistributeSurplus(poolID string) (*models.SurplusDistribution, error) { + pool := s.repo.GetPool(poolID) + if pool == nil { + return nil, fmt.Errorf("pool not found: %s", poolID) + } + if pool.Surplus <= 0 || pool.MemberCount == 0 { + return nil, fmt.Errorf("no surplus to distribute") + } + perMember := pool.Surplus / float64(pool.MemberCount) + return &models.SurplusDistribution{ + PoolID: poolID, + TotalSurplus: pool.Surplus, + MemberCount: pool.MemberCount, + PerMemberShare: perMember, + DistributedAt: time.Now(), + }, nil +} + +func (s *Service) CheckShariaCompliance(productID string) *models.ShariaCompliance { + product := s.repo.GetProduct(productID) + if product == nil { + return &models.ShariaCompliance{ProductID: productID, IsCompliant: false, BoardApproval: "not_found"} + } + return &models.ShariaCompliance{ + ProductID: productID, + IsCompliant: product.ShariaApproved, + Principles: []string{ + "tabarru_donation_principle", + "gharar_uncertainty_minimized", + "maysir_gambling_prohibited", + "riba_interest_free", + "mudharabah_profit_sharing", + "wakala_agency_fee_transparent", + }, + BoardApproval: "approved", + ReviewDate: "2026-01-15", + } +} From 1e6af52572bd13cbe8b8906e75a06a35ed5cff0f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 04:03:52 +0000 Subject: [PATCH 2/9] fix: Cap instant claim confidence_pct at 100% Previously satellite_verified + high damage_score + low amount could sum to 105%. Co-Authored-By: Patrick Munis --- insurance-tech-innovations/internal/service/service.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/insurance-tech-innovations/internal/service/service.go b/insurance-tech-innovations/internal/service/service.go index 7d942bf90..cd25855c5 100644 --- a/insurance-tech-innovations/internal/service/service.go +++ b/insurance-tech-innovations/internal/service/service.go @@ -94,6 +94,9 @@ func (s *Service) ProcessInstantClaim(req models.InstantClaimRequest) *models.In if req.Amount < 100000 { confidence += 10 } + if confidence > 100 { + confidence = 100 + } if confidence >= 85 { decision = "auto_approved" } else if confidence >= 70 { From 66d3d464bb73cd9eaab7bcd735b763eb5c6da779 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 11:02:21 +0000 Subject: [PATCH 3/9] feat: Integrate 40 product improvements into customer portal dashboard - Add 6 new TSX pages for all product categories: * AgriculturalInsuranceSuite (13 products) * EmbeddedDistributionPlatform (6 products) * DigitalConsumerProducts (8 products) * TakafulProductsSuite (6 products) * NIIRACompulsoryInsurance (11 classes) * InsuranceTechInnovations (5 features) - Wire all pages into App.tsx router with wouter Routes - Add '40 New Products (PR #31)' sidebar section in UnifiedLayout - Fix pre-existing issues: RiskAssessment placeholder, ClaimsEvidence and ERPNextIntegration missing default exports, TwoFactorAuth Next.js import replaced with wouter - Add PWA products showcase standalone page - Simplify vite.config.ts (remove broken plugins) Co-Authored-By: Patrick Munis --- customer-portal-full/.env.example | 188 + customer-portal-full/.gitignore | 107 + customer-portal-full/.gitkeep | 0 customer-portal-full/.prettierignore | 35 + customer-portal-full/.prettierrc | 15 + customer-portal-full/Dockerfile | 52 + customer-portal-full/client/index.html | 51 + customer-portal-full/client/public/.gitkeep | 0 .../public/__manus__/debug-collector.js | 821 ++ .../client/public/icons/icon-128x128.png | Bin 0 -> 1023 bytes .../client/public/icons/icon-144x144.png | Bin 0 -> 1160 bytes .../client/public/icons/icon-152x152.png | Bin 0 -> 1239 bytes .../client/public/icons/icon-192x192.png | Bin 0 -> 1588 bytes .../client/public/icons/icon-384x384.png | Bin 0 -> 3177 bytes .../client/public/icons/icon-512x512.png | Bin 0 -> 4431 bytes .../client/public/icons/icon-72x72.png | Bin 0 -> 567 bytes .../client/public/icons/icon-96x96.png | Bin 0 -> 733 bytes .../client/public/manifest.json | 85 + .../client/public/offline.html | 45 + customer-portal-full/client/public/sw.js | 114 + customer-portal-full/client/src/App.tsx | 731 + .../client/src/_core/hooks/useAuth.ts | 84 + .../client/src/components/AIChatBox.tsx | 335 + .../client/src/components/AICopilot.tsx | 657 + .../src/components/AccessibleComponents.tsx | 337 + .../client/src/components/DashboardLayout.tsx | 264 + .../components/DashboardLayoutSkeleton.tsx | 46 + .../client/src/components/ErrorBoundary.tsx | 62 + .../client/src/components/ManusDialog.tsx | 89 + .../client/src/components/Map.tsx | 155 + .../src/components/SmartDocumentUpload.tsx | 649 + .../client/src/components/UnifiedLayout.tsx | 560 + .../client/src/components/ui/accordion.tsx | 64 + .../client/src/components/ui/alert-dialog.tsx | 155 + .../client/src/components/ui/alert.tsx | 66 + .../client/src/components/ui/aspect-ratio.tsx | 9 + .../client/src/components/ui/avatar.tsx | 51 + .../client/src/components/ui/badge.tsx | 46 + .../client/src/components/ui/breadcrumb.tsx | 109 + .../client/src/components/ui/button-group.tsx | 83 + .../client/src/components/ui/button.tsx | 60 + .../client/src/components/ui/calendar.tsx | 211 + .../client/src/components/ui/card.tsx | 92 + .../client/src/components/ui/carousel.tsx | 239 + .../client/src/components/ui/chart.tsx | 355 + .../client/src/components/ui/checkbox.tsx | 30 + .../client/src/components/ui/collapsible.tsx | 31 + .../client/src/components/ui/command.tsx | 184 + .../client/src/components/ui/context-menu.tsx | 250 + .../client/src/components/ui/dialog.tsx | 209 + .../client/src/components/ui/drawer.tsx | 133 + .../src/components/ui/dropdown-menu.tsx | 255 + .../client/src/components/ui/empty.tsx | 104 + .../client/src/components/ui/field.tsx | 242 + .../client/src/components/ui/form.tsx | 168 + .../client/src/components/ui/hover-card.tsx | 42 + .../client/src/components/ui/input-group.tsx | 168 + .../client/src/components/ui/input-otp.tsx | 75 + .../client/src/components/ui/input.tsx | 70 + .../client/src/components/ui/item.tsx | 193 + .../client/src/components/ui/kbd.tsx | 28 + .../client/src/components/ui/label.tsx | 22 + .../client/src/components/ui/menubar.tsx | 274 + .../src/components/ui/navigation-menu.tsx | 168 + .../client/src/components/ui/pagination.tsx | 127 + .../client/src/components/ui/popover.tsx | 46 + .../client/src/components/ui/progress.tsx | 29 + .../client/src/components/ui/radio-group.tsx | 43 + .../client/src/components/ui/resizable.tsx | 54 + .../client/src/components/ui/scroll-area.tsx | 56 + .../client/src/components/ui/select.tsx | 185 + .../client/src/components/ui/separator.tsx | 26 + .../client/src/components/ui/sheet.tsx | 139 + .../client/src/components/ui/sidebar.tsx | 734 + .../client/src/components/ui/skeleton.tsx | 13 + .../client/src/components/ui/slider.tsx | 61 + .../client/src/components/ui/sonner.tsx | 29 + .../client/src/components/ui/spinner.tsx | 16 + .../client/src/components/ui/switch.tsx | 29 + .../client/src/components/ui/table.tsx | 114 + .../client/src/components/ui/tabs.tsx | 64 + .../client/src/components/ui/textarea.tsx | 67 + .../client/src/components/ui/toggle-group.tsx | 73 + .../client/src/components/ui/toggle.tsx | 45 + .../client/src/components/ui/tooltip.tsx | 59 + customer-portal-full/client/src/const.ts | 17 + .../client/src/contexts/LanguageContext.tsx | 380 + .../src/contexts/NotificationContext.tsx | 178 + .../client/src/contexts/RoleContext.tsx | 147 + .../client/src/contexts/ThemeContext.tsx | 64 + .../client/src/hooks/useComposition.ts | 81 + .../client/src/hooks/useFormValidation.ts | 218 + .../client/src/hooks/useMobile.tsx | 21 + .../client/src/hooks/useNotifications.ts | 109 + .../client/src/hooks/usePersistFn.ts | 20 + customer-portal-full/client/src/index.css | 177 + customer-portal-full/client/src/lib/trpc.ts | 4 + customer-portal-full/client/src/lib/utils.ts | 6 + customer-portal-full/client/src/main.tsx | 63 + .../client/src/pages/ABTestingFramework.tsx | 481 + .../client/src/pages/AIAdvisor.tsx | 128 + .../client/src/pages/AIClaimsAdjudication.tsx | 339 + .../client/src/pages/AIKnowledgeAssistant.tsx | 243 + .../client/src/pages/ActuarialModule.tsx | 254 + .../client/src/pages/AdminPolicyCreation.tsx | 498 + .../src/pages/AgentCommissionManagement.tsx | 291 + .../client/src/pages/AgentPerformance.tsx | 300 + .../client/src/pages/AgentPortal.tsx | 389 + .../src/pages/AgriculturalInsuranceSuite.tsx | 144 + .../src/pages/AgriculturalUnderwriting.tsx | 279 + .../client/src/pages/Analytics.tsx | 161 + .../client/src/pages/AuditLogs.tsx | 54 + .../client/src/pages/AuditTrailSystem.tsx | 44 + .../client/src/pages/Auth.tsx | 258 + .../client/src/pages/Bancassurance.tsx | 252 + .../client/src/pages/BancassurancePortal.tsx | 44 + .../src/pages/BatchProcessingEngine.tsx | 293 + .../client/src/pages/BlockchainStatus.tsx | 102 + .../client/src/pages/BrokerAPIManagement.tsx | 236 + .../client/src/pages/Chatbot.tsx | 193 + .../client/src/pages/ChurnPrediction.tsx | 220 + .../client/src/pages/Claims.tsx | 273 + .../src/pages/ClaimsAdjudicationEngine.tsx | 293 + .../client/src/pages/ClaimsEvidence.tsx | 209 + .../client/src/pages/ClaimsTimeline.tsx | 322 + .../client/src/pages/ClaimsTracker.tsx | 490 + .../client/src/pages/Commission.tsx | 213 + .../client/src/pages/Communication.tsx | 258 + .../client/src/pages/ComplianceMonitoring.tsx | 224 + .../client/src/pages/ComponentShowcase.tsx | 1437 ++ .../client/src/pages/Customer360View.tsx | 176 + .../client/src/pages/CustomerFeedbackLoop.tsx | 262 + .../client/src/pages/CustomerManagement.tsx | 212 + .../client/src/pages/Dashboard.tsx | 231 + .../src/pages/DigitalConsumerProducts.tsx | 132 + .../client/src/pages/DigitalWallet.tsx | 252 + .../src/pages/DisasterRecoveryModule.tsx | 196 + .../src/pages/DocumentManagementSystem.tsx | 249 + .../client/src/pages/DocumentScanner.tsx | 344 + .../client/src/pages/DynamicPricing.tsx | 276 + .../client/src/pages/ERPNextIntegration.tsx | 115 + .../pages/EmbeddedDistributionPlatform.tsx | 108 + .../client/src/pages/EmbeddedInsurance.tsx | 316 + .../client/src/pages/EmergencySOS.tsx | 289 + .../client/src/pages/ExecutiveDashboard.tsx | 236 + .../client/src/pages/FamilyCoverage.tsx | 203 + .../client/src/pages/FamilyPolicies.tsx | 283 + .../client/src/pages/FinancialWellness.tsx | 198 + .../client/src/pages/FraudAlerts.tsx | 170 + .../src/pages/FraudNetworkVisualization.tsx | 296 + .../client/src/pages/Gamification.tsx | 220 + .../client/src/pages/GeospatialMap.tsx | 227 + .../client/src/pages/GigEconomy.tsx | 155 + .../client/src/pages/GroupLifeAdmin.tsx | 36 + .../client/src/pages/HealthWellness.tsx | 285 + .../client/src/pages/Home.tsx | 814 + .../client/src/pages/InsuranceApplication.tsx | 362 + .../client/src/pages/InsuranceLiteracyHub.tsx | 289 + .../client/src/pages/InsuranceMarketplace.tsx | 260 + .../client/src/pages/InsuranceProducts.tsx | 390 + .../client/src/pages/InsuranceRadar.tsx | 229 + .../client/src/pages/InsuranceScore.tsx | 139 + .../src/pages/InsuranceTechInnovations.tsx | 170 + .../client/src/pages/KYCStatus.tsx | 237 + .../src/pages/KnowledgeGraphExplorer.tsx | 45 + .../client/src/pages/LoyaltyProgram.tsx | 320 + .../client/src/pages/LoyaltyRewards.tsx | 211 + .../client/src/pages/MCMCRiskModeling.tsx | 240 + .../client/src/pages/Microinsurance.tsx | 202 + .../src/pages/ModelSecurityDashboard.tsx | 147 + .../client/src/pages/MultiCurrencySupport.tsx | 208 + .../client/src/pages/MyApplications.tsx | 436 + .../client/src/pages/NAICOMCompliance.tsx | 290 + .../src/pages/NIIRACompulsoryInsurance.tsx | 127 + .../client/src/pages/NMIDIntegration.tsx | 264 + .../src/pages/NigerianBankIntegrations.tsx | 201 + .../client/src/pages/NotFound.tsx | 52 + .../client/src/pages/Onboarding.tsx | 137 + .../client/src/pages/OperationalReports.tsx | 276 + .../client/src/pages/P2PInsurance.tsx | 356 + .../client/src/pages/PFAIntegration.tsx | 45 + .../client/src/pages/ParametricInsurance.tsx | 168 + .../client/src/pages/Payments.tsx | 258 + .../pages/PerformanceMonitoringDashboard.tsx | 113 + .../client/src/pages/Policies.tsx | 161 + .../client/src/pages/PolicyApproval.tsx | 392 + .../client/src/pages/PolicyComparison.tsx | 287 + .../client/src/pages/PolicyRenewal.tsx | 260 + .../src/pages/PolicyRenewalAutomation.tsx | 221 + .../client/src/pages/PostgreSQLScaling.tsx | 162 + .../client/src/pages/PremiumCalculator.tsx | 188 + .../src/pages/PremiumRateManagement.tsx | 436 + .../src/pages/ProductRecommendationQuiz.tsx | 351 + .../client/src/pages/Profile.tsx | 178 + .../client/src/pages/ReconciliationEngine.tsx | 220 + .../client/src/pages/ReferralProgram.tsx | 258 + .../client/src/pages/Referrals.tsx | 252 + .../src/pages/ReinsuranceManagement.tsx | 240 + .../client/src/pages/Reviews.tsx | 382 + .../client/src/pages/RiskAssessment.tsx | 231 + .../client/src/pages/SMEBusiness.tsx | 339 + .../client/src/pages/SavingsInvestment.tsx | 380 + .../client/src/pages/SmartClaimRouting.tsx | 43 + .../client/src/pages/SystemSettings.tsx | 161 + .../client/src/pages/TakafulProductsSuite.tsx | 134 + .../client/src/pages/TelcoCreditScoring.tsx | 236 + .../client/src/pages/Telematics.tsx | 342 + .../client/src/pages/TwoFactorAuth.tsx | 103 + .../client/src/pages/USSDGateway.tsx | 328 + .../client/src/pages/UserManagement.tsx | 345 + .../client/src/pages/VoiceAssistant.tsx | 168 + .../client/src/pages/WhatsAppIntegration.tsx | 48 + .../src/tests/premiumCalculation.test.ts | 457 + customer-portal-full/components.json | 19 + customer-portal-full/drizzle.config.ts | 15 + .../drizzle/0000_steady_joshua_kane.sql | 13 + .../drizzle/meta/0000_snapshot.json | 110 + .../drizzle/meta/_journal.json | 13 + .../drizzle/migrations/.gitkeep | 0 customer-portal-full/drizzle/relations.ts | 1 + customer-portal-full/drizzle/schema.ts | 605 + customer-portal-full/package.json | 125 + .../patches/wouter@3.7.1.patch | 28 + customer-portal-full/playwright.config.ts | 40 + customer-portal-full/pnpm-lock.yaml | 12233 ++++++++++++++++ customer-portal-full/server/_core/context.ts | 28 + customer-portal-full/server/_core/cookies.ts | 48 + customer-portal-full/server/_core/dataApi.ts | 64 + customer-portal-full/server/_core/env.ts | 10 + .../server/_core/imageGeneration.ts | 92 + customer-portal-full/server/_core/index.ts | 83 + customer-portal-full/server/_core/llm.ts | 332 + customer-portal-full/server/_core/map.ts | 319 + .../server/_core/notification.ts | 114 + customer-portal-full/server/_core/oauth.ts | 53 + customer-portal-full/server/_core/sdk.ts | 304 + .../server/_core/systemRouter.ts | 29 + customer-portal-full/server/_core/trpc.ts | 45 + .../server/_core/types/cookie.d.ts | 6 + .../server/_core/types/manusTypes.ts | 69 + customer-portal-full/server/_core/vite.ts | 67 + .../server/_core/voiceTranscription.ts | 284 + customer-portal-full/server/api-clients.ts | 445 + .../server/auth.logout.test.ts | 62 + customer-portal-full/server/db.ts | 1195 ++ customer-portal-full/server/index.ts | 33 + customer-portal-full/server/notifications.ts | 103 + customer-portal-full/server/routers.test.ts | 290 + customer-portal-full/server/routers.ts | 1350 ++ customer-portal-full/server/seed.mjs | 197 + customer-portal-full/server/storage.ts | 102 + customer-portal-full/shared/_core/errors.ts | 19 + customer-portal-full/shared/const.ts | 5 + customer-portal-full/shared/types.ts | 7 + customer-portal-full/todo.md | 40 + customer-portal-full/tsconfig.json | 23 + customer-portal-full/tsconfig.node.json | 22 + customer-portal-full/vite.config.ts | 49 + customer-portal-full/vitest.config.ts | 19 + pwa-products-showcase/index.html | 674 + pwa-products-showcase/manifest.json | 10 + 261 files changed, 61634 insertions(+) create mode 100644 customer-portal-full/.env.example create mode 100644 customer-portal-full/.gitignore create mode 100644 customer-portal-full/.gitkeep create mode 100644 customer-portal-full/.prettierignore create mode 100644 customer-portal-full/.prettierrc create mode 100644 customer-portal-full/Dockerfile create mode 100644 customer-portal-full/client/index.html create mode 100644 customer-portal-full/client/public/.gitkeep create mode 100644 customer-portal-full/client/public/__manus__/debug-collector.js create mode 100644 customer-portal-full/client/public/icons/icon-128x128.png create mode 100644 customer-portal-full/client/public/icons/icon-144x144.png create mode 100644 customer-portal-full/client/public/icons/icon-152x152.png create mode 100644 customer-portal-full/client/public/icons/icon-192x192.png create mode 100644 customer-portal-full/client/public/icons/icon-384x384.png create mode 100644 customer-portal-full/client/public/icons/icon-512x512.png create mode 100644 customer-portal-full/client/public/icons/icon-72x72.png create mode 100644 customer-portal-full/client/public/icons/icon-96x96.png create mode 100644 customer-portal-full/client/public/manifest.json create mode 100644 customer-portal-full/client/public/offline.html create mode 100644 customer-portal-full/client/public/sw.js create mode 100644 customer-portal-full/client/src/App.tsx create mode 100644 customer-portal-full/client/src/_core/hooks/useAuth.ts create mode 100644 customer-portal-full/client/src/components/AIChatBox.tsx create mode 100644 customer-portal-full/client/src/components/AICopilot.tsx create mode 100644 customer-portal-full/client/src/components/AccessibleComponents.tsx create mode 100644 customer-portal-full/client/src/components/DashboardLayout.tsx create mode 100644 customer-portal-full/client/src/components/DashboardLayoutSkeleton.tsx create mode 100644 customer-portal-full/client/src/components/ErrorBoundary.tsx create mode 100644 customer-portal-full/client/src/components/ManusDialog.tsx create mode 100644 customer-portal-full/client/src/components/Map.tsx create mode 100644 customer-portal-full/client/src/components/SmartDocumentUpload.tsx create mode 100644 customer-portal-full/client/src/components/UnifiedLayout.tsx create mode 100644 customer-portal-full/client/src/components/ui/accordion.tsx create mode 100644 customer-portal-full/client/src/components/ui/alert-dialog.tsx create mode 100644 customer-portal-full/client/src/components/ui/alert.tsx create mode 100644 customer-portal-full/client/src/components/ui/aspect-ratio.tsx create mode 100644 customer-portal-full/client/src/components/ui/avatar.tsx create mode 100644 customer-portal-full/client/src/components/ui/badge.tsx create mode 100644 customer-portal-full/client/src/components/ui/breadcrumb.tsx create mode 100644 customer-portal-full/client/src/components/ui/button-group.tsx create mode 100644 customer-portal-full/client/src/components/ui/button.tsx create mode 100644 customer-portal-full/client/src/components/ui/calendar.tsx create mode 100644 customer-portal-full/client/src/components/ui/card.tsx create mode 100644 customer-portal-full/client/src/components/ui/carousel.tsx create mode 100644 customer-portal-full/client/src/components/ui/chart.tsx create mode 100644 customer-portal-full/client/src/components/ui/checkbox.tsx create mode 100644 customer-portal-full/client/src/components/ui/collapsible.tsx create mode 100644 customer-portal-full/client/src/components/ui/command.tsx create mode 100644 customer-portal-full/client/src/components/ui/context-menu.tsx create mode 100644 customer-portal-full/client/src/components/ui/dialog.tsx create mode 100644 customer-portal-full/client/src/components/ui/drawer.tsx create mode 100644 customer-portal-full/client/src/components/ui/dropdown-menu.tsx create mode 100644 customer-portal-full/client/src/components/ui/empty.tsx create mode 100644 customer-portal-full/client/src/components/ui/field.tsx create mode 100644 customer-portal-full/client/src/components/ui/form.tsx create mode 100644 customer-portal-full/client/src/components/ui/hover-card.tsx create mode 100644 customer-portal-full/client/src/components/ui/input-group.tsx create mode 100644 customer-portal-full/client/src/components/ui/input-otp.tsx create mode 100644 customer-portal-full/client/src/components/ui/input.tsx create mode 100644 customer-portal-full/client/src/components/ui/item.tsx create mode 100644 customer-portal-full/client/src/components/ui/kbd.tsx create mode 100644 customer-portal-full/client/src/components/ui/label.tsx create mode 100644 customer-portal-full/client/src/components/ui/menubar.tsx create mode 100644 customer-portal-full/client/src/components/ui/navigation-menu.tsx create mode 100644 customer-portal-full/client/src/components/ui/pagination.tsx create mode 100644 customer-portal-full/client/src/components/ui/popover.tsx create mode 100644 customer-portal-full/client/src/components/ui/progress.tsx create mode 100644 customer-portal-full/client/src/components/ui/radio-group.tsx create mode 100644 customer-portal-full/client/src/components/ui/resizable.tsx create mode 100644 customer-portal-full/client/src/components/ui/scroll-area.tsx create mode 100644 customer-portal-full/client/src/components/ui/select.tsx create mode 100644 customer-portal-full/client/src/components/ui/separator.tsx create mode 100644 customer-portal-full/client/src/components/ui/sheet.tsx create mode 100644 customer-portal-full/client/src/components/ui/sidebar.tsx create mode 100644 customer-portal-full/client/src/components/ui/skeleton.tsx create mode 100644 customer-portal-full/client/src/components/ui/slider.tsx create mode 100644 customer-portal-full/client/src/components/ui/sonner.tsx create mode 100644 customer-portal-full/client/src/components/ui/spinner.tsx create mode 100644 customer-portal-full/client/src/components/ui/switch.tsx create mode 100644 customer-portal-full/client/src/components/ui/table.tsx create mode 100644 customer-portal-full/client/src/components/ui/tabs.tsx create mode 100644 customer-portal-full/client/src/components/ui/textarea.tsx create mode 100644 customer-portal-full/client/src/components/ui/toggle-group.tsx create mode 100644 customer-portal-full/client/src/components/ui/toggle.tsx create mode 100644 customer-portal-full/client/src/components/ui/tooltip.tsx create mode 100644 customer-portal-full/client/src/const.ts create mode 100644 customer-portal-full/client/src/contexts/LanguageContext.tsx create mode 100644 customer-portal-full/client/src/contexts/NotificationContext.tsx create mode 100644 customer-portal-full/client/src/contexts/RoleContext.tsx create mode 100644 customer-portal-full/client/src/contexts/ThemeContext.tsx create mode 100644 customer-portal-full/client/src/hooks/useComposition.ts create mode 100644 customer-portal-full/client/src/hooks/useFormValidation.ts create mode 100644 customer-portal-full/client/src/hooks/useMobile.tsx create mode 100644 customer-portal-full/client/src/hooks/useNotifications.ts create mode 100644 customer-portal-full/client/src/hooks/usePersistFn.ts create mode 100644 customer-portal-full/client/src/index.css create mode 100644 customer-portal-full/client/src/lib/trpc.ts create mode 100644 customer-portal-full/client/src/lib/utils.ts create mode 100644 customer-portal-full/client/src/main.tsx create mode 100644 customer-portal-full/client/src/pages/ABTestingFramework.tsx create mode 100644 customer-portal-full/client/src/pages/AIAdvisor.tsx create mode 100644 customer-portal-full/client/src/pages/AIClaimsAdjudication.tsx create mode 100644 customer-portal-full/client/src/pages/AIKnowledgeAssistant.tsx create mode 100644 customer-portal-full/client/src/pages/ActuarialModule.tsx create mode 100644 customer-portal-full/client/src/pages/AdminPolicyCreation.tsx create mode 100644 customer-portal-full/client/src/pages/AgentCommissionManagement.tsx create mode 100644 customer-portal-full/client/src/pages/AgentPerformance.tsx create mode 100644 customer-portal-full/client/src/pages/AgentPortal.tsx create mode 100644 customer-portal-full/client/src/pages/AgriculturalInsuranceSuite.tsx create mode 100644 customer-portal-full/client/src/pages/AgriculturalUnderwriting.tsx create mode 100644 customer-portal-full/client/src/pages/Analytics.tsx create mode 100644 customer-portal-full/client/src/pages/AuditLogs.tsx create mode 100644 customer-portal-full/client/src/pages/AuditTrailSystem.tsx create mode 100644 customer-portal-full/client/src/pages/Auth.tsx create mode 100644 customer-portal-full/client/src/pages/Bancassurance.tsx create mode 100644 customer-portal-full/client/src/pages/BancassurancePortal.tsx create mode 100644 customer-portal-full/client/src/pages/BatchProcessingEngine.tsx create mode 100644 customer-portal-full/client/src/pages/BlockchainStatus.tsx create mode 100644 customer-portal-full/client/src/pages/BrokerAPIManagement.tsx create mode 100644 customer-portal-full/client/src/pages/Chatbot.tsx create mode 100644 customer-portal-full/client/src/pages/ChurnPrediction.tsx create mode 100644 customer-portal-full/client/src/pages/Claims.tsx create mode 100644 customer-portal-full/client/src/pages/ClaimsAdjudicationEngine.tsx create mode 100644 customer-portal-full/client/src/pages/ClaimsEvidence.tsx create mode 100644 customer-portal-full/client/src/pages/ClaimsTimeline.tsx create mode 100644 customer-portal-full/client/src/pages/ClaimsTracker.tsx create mode 100644 customer-portal-full/client/src/pages/Commission.tsx create mode 100644 customer-portal-full/client/src/pages/Communication.tsx create mode 100644 customer-portal-full/client/src/pages/ComplianceMonitoring.tsx create mode 100644 customer-portal-full/client/src/pages/ComponentShowcase.tsx create mode 100644 customer-portal-full/client/src/pages/Customer360View.tsx create mode 100644 customer-portal-full/client/src/pages/CustomerFeedbackLoop.tsx create mode 100644 customer-portal-full/client/src/pages/CustomerManagement.tsx create mode 100644 customer-portal-full/client/src/pages/Dashboard.tsx create mode 100644 customer-portal-full/client/src/pages/DigitalConsumerProducts.tsx create mode 100644 customer-portal-full/client/src/pages/DigitalWallet.tsx create mode 100644 customer-portal-full/client/src/pages/DisasterRecoveryModule.tsx create mode 100644 customer-portal-full/client/src/pages/DocumentManagementSystem.tsx create mode 100644 customer-portal-full/client/src/pages/DocumentScanner.tsx create mode 100644 customer-portal-full/client/src/pages/DynamicPricing.tsx create mode 100644 customer-portal-full/client/src/pages/ERPNextIntegration.tsx create mode 100644 customer-portal-full/client/src/pages/EmbeddedDistributionPlatform.tsx create mode 100644 customer-portal-full/client/src/pages/EmbeddedInsurance.tsx create mode 100644 customer-portal-full/client/src/pages/EmergencySOS.tsx create mode 100644 customer-portal-full/client/src/pages/ExecutiveDashboard.tsx create mode 100644 customer-portal-full/client/src/pages/FamilyCoverage.tsx create mode 100644 customer-portal-full/client/src/pages/FamilyPolicies.tsx create mode 100644 customer-portal-full/client/src/pages/FinancialWellness.tsx create mode 100644 customer-portal-full/client/src/pages/FraudAlerts.tsx create mode 100644 customer-portal-full/client/src/pages/FraudNetworkVisualization.tsx create mode 100644 customer-portal-full/client/src/pages/Gamification.tsx create mode 100644 customer-portal-full/client/src/pages/GeospatialMap.tsx create mode 100644 customer-portal-full/client/src/pages/GigEconomy.tsx create mode 100644 customer-portal-full/client/src/pages/GroupLifeAdmin.tsx create mode 100644 customer-portal-full/client/src/pages/HealthWellness.tsx create mode 100644 customer-portal-full/client/src/pages/Home.tsx create mode 100644 customer-portal-full/client/src/pages/InsuranceApplication.tsx create mode 100644 customer-portal-full/client/src/pages/InsuranceLiteracyHub.tsx create mode 100644 customer-portal-full/client/src/pages/InsuranceMarketplace.tsx create mode 100644 customer-portal-full/client/src/pages/InsuranceProducts.tsx create mode 100644 customer-portal-full/client/src/pages/InsuranceRadar.tsx create mode 100644 customer-portal-full/client/src/pages/InsuranceScore.tsx create mode 100644 customer-portal-full/client/src/pages/InsuranceTechInnovations.tsx create mode 100644 customer-portal-full/client/src/pages/KYCStatus.tsx create mode 100644 customer-portal-full/client/src/pages/KnowledgeGraphExplorer.tsx create mode 100644 customer-portal-full/client/src/pages/LoyaltyProgram.tsx create mode 100644 customer-portal-full/client/src/pages/LoyaltyRewards.tsx create mode 100644 customer-portal-full/client/src/pages/MCMCRiskModeling.tsx create mode 100644 customer-portal-full/client/src/pages/Microinsurance.tsx create mode 100644 customer-portal-full/client/src/pages/ModelSecurityDashboard.tsx create mode 100644 customer-portal-full/client/src/pages/MultiCurrencySupport.tsx create mode 100644 customer-portal-full/client/src/pages/MyApplications.tsx create mode 100644 customer-portal-full/client/src/pages/NAICOMCompliance.tsx create mode 100644 customer-portal-full/client/src/pages/NIIRACompulsoryInsurance.tsx create mode 100644 customer-portal-full/client/src/pages/NMIDIntegration.tsx create mode 100644 customer-portal-full/client/src/pages/NigerianBankIntegrations.tsx create mode 100644 customer-portal-full/client/src/pages/NotFound.tsx create mode 100644 customer-portal-full/client/src/pages/Onboarding.tsx create mode 100644 customer-portal-full/client/src/pages/OperationalReports.tsx create mode 100644 customer-portal-full/client/src/pages/P2PInsurance.tsx create mode 100644 customer-portal-full/client/src/pages/PFAIntegration.tsx create mode 100644 customer-portal-full/client/src/pages/ParametricInsurance.tsx create mode 100644 customer-portal-full/client/src/pages/Payments.tsx create mode 100644 customer-portal-full/client/src/pages/PerformanceMonitoringDashboard.tsx create mode 100644 customer-portal-full/client/src/pages/Policies.tsx create mode 100644 customer-portal-full/client/src/pages/PolicyApproval.tsx create mode 100644 customer-portal-full/client/src/pages/PolicyComparison.tsx create mode 100644 customer-portal-full/client/src/pages/PolicyRenewal.tsx create mode 100644 customer-portal-full/client/src/pages/PolicyRenewalAutomation.tsx create mode 100644 customer-portal-full/client/src/pages/PostgreSQLScaling.tsx create mode 100644 customer-portal-full/client/src/pages/PremiumCalculator.tsx create mode 100644 customer-portal-full/client/src/pages/PremiumRateManagement.tsx create mode 100644 customer-portal-full/client/src/pages/ProductRecommendationQuiz.tsx create mode 100644 customer-portal-full/client/src/pages/Profile.tsx create mode 100644 customer-portal-full/client/src/pages/ReconciliationEngine.tsx create mode 100644 customer-portal-full/client/src/pages/ReferralProgram.tsx create mode 100644 customer-portal-full/client/src/pages/Referrals.tsx create mode 100644 customer-portal-full/client/src/pages/ReinsuranceManagement.tsx create mode 100644 customer-portal-full/client/src/pages/Reviews.tsx create mode 100644 customer-portal-full/client/src/pages/RiskAssessment.tsx create mode 100644 customer-portal-full/client/src/pages/SMEBusiness.tsx create mode 100644 customer-portal-full/client/src/pages/SavingsInvestment.tsx create mode 100644 customer-portal-full/client/src/pages/SmartClaimRouting.tsx create mode 100644 customer-portal-full/client/src/pages/SystemSettings.tsx create mode 100644 customer-portal-full/client/src/pages/TakafulProductsSuite.tsx create mode 100644 customer-portal-full/client/src/pages/TelcoCreditScoring.tsx create mode 100644 customer-portal-full/client/src/pages/Telematics.tsx create mode 100644 customer-portal-full/client/src/pages/TwoFactorAuth.tsx create mode 100644 customer-portal-full/client/src/pages/USSDGateway.tsx create mode 100644 customer-portal-full/client/src/pages/UserManagement.tsx create mode 100644 customer-portal-full/client/src/pages/VoiceAssistant.tsx create mode 100644 customer-portal-full/client/src/pages/WhatsAppIntegration.tsx create mode 100644 customer-portal-full/client/src/tests/premiumCalculation.test.ts create mode 100644 customer-portal-full/components.json create mode 100644 customer-portal-full/drizzle.config.ts create mode 100644 customer-portal-full/drizzle/0000_steady_joshua_kane.sql create mode 100644 customer-portal-full/drizzle/meta/0000_snapshot.json create mode 100644 customer-portal-full/drizzle/meta/_journal.json create mode 100644 customer-portal-full/drizzle/migrations/.gitkeep create mode 100644 customer-portal-full/drizzle/relations.ts create mode 100644 customer-portal-full/drizzle/schema.ts create mode 100644 customer-portal-full/package.json create mode 100644 customer-portal-full/patches/wouter@3.7.1.patch create mode 100644 customer-portal-full/playwright.config.ts create mode 100644 customer-portal-full/pnpm-lock.yaml create mode 100644 customer-portal-full/server/_core/context.ts create mode 100644 customer-portal-full/server/_core/cookies.ts create mode 100644 customer-portal-full/server/_core/dataApi.ts create mode 100644 customer-portal-full/server/_core/env.ts create mode 100644 customer-portal-full/server/_core/imageGeneration.ts create mode 100644 customer-portal-full/server/_core/index.ts create mode 100644 customer-portal-full/server/_core/llm.ts create mode 100644 customer-portal-full/server/_core/map.ts create mode 100644 customer-portal-full/server/_core/notification.ts create mode 100644 customer-portal-full/server/_core/oauth.ts create mode 100644 customer-portal-full/server/_core/sdk.ts create mode 100644 customer-portal-full/server/_core/systemRouter.ts create mode 100644 customer-portal-full/server/_core/trpc.ts create mode 100644 customer-portal-full/server/_core/types/cookie.d.ts create mode 100644 customer-portal-full/server/_core/types/manusTypes.ts create mode 100644 customer-portal-full/server/_core/vite.ts create mode 100644 customer-portal-full/server/_core/voiceTranscription.ts create mode 100644 customer-portal-full/server/api-clients.ts create mode 100644 customer-portal-full/server/auth.logout.test.ts create mode 100644 customer-portal-full/server/db.ts create mode 100644 customer-portal-full/server/index.ts create mode 100644 customer-portal-full/server/notifications.ts create mode 100644 customer-portal-full/server/routers.test.ts create mode 100644 customer-portal-full/server/routers.ts create mode 100644 customer-portal-full/server/seed.mjs create mode 100644 customer-portal-full/server/storage.ts create mode 100644 customer-portal-full/shared/_core/errors.ts create mode 100644 customer-portal-full/shared/const.ts create mode 100644 customer-portal-full/shared/types.ts create mode 100644 customer-portal-full/todo.md create mode 100644 customer-portal-full/tsconfig.json create mode 100644 customer-portal-full/tsconfig.node.json create mode 100644 customer-portal-full/vite.config.ts create mode 100644 customer-portal-full/vitest.config.ts create mode 100644 pwa-products-showcase/index.html create mode 100644 pwa-products-showcase/manifest.json diff --git a/customer-portal-full/.env.example b/customer-portal-full/.env.example new file mode 100644 index 000000000..c764316cd --- /dev/null +++ b/customer-portal-full/.env.example @@ -0,0 +1,188 @@ +# ============================================================================ +# Unified Insurance Platform — Environment Variables Reference +# Copy this file to .env and fill in your values +# ============================================================================ + +# ── Application ────────────────────────────────────────────────────────────── +NODE_ENV=production +PORT=5000 +APP_NAME=unified-insurance-platform +APP_URL=https://insurance.example.com + +# ── Authentication & OAuth ──────────────────────────────────────────────────── +VITE_OAUTH_PORTAL_URL=https://auth.insurance.example.com +VITE_APP_ID=unified-insurance-platform +OAUTH_SERVER_URL=https://auth.insurance.example.com +JWT_SECRET= +JWT_EXPIRY=24h +SESSION_SECRET= + +# ── Database ────────────────────────────────────────────────────────────────── +DATABASE_URL=postgresql://insurance_user:password@localhost:5432/insurance_db +DATABASE_POOL_MIN=2 +DATABASE_POOL_MAX=20 +DATABASE_SSL=true +# Read replica (for analytics queries) +DATABASE_READ_REPLICA_URL=postgresql://insurance_user:password@replica:5432/insurance_db + +# ── Redis Cache ─────────────────────────────────────────────────────────────── +REDIS_URL=redis://localhost:6379 +REDIS_PASSWORD= +REDIS_TLS=true +CACHE_TTL_SECONDS=300 + +# ── Core Microservice URLs ──────────────────────────────────────────────────── +POLICY_SERVICE_URL=http://policy-service:8081 +CLAIM_SERVICE_URL=http://claims-adjudication:8082 +PAYMENT_SERVICE_URL=http://payment-service:8083 +CUSTOMER_SERVICE_URL=http://customer-360-service:8084 +VERIFICATION_SERVICE_URL=http://kyc-orchestrator:8085 +TELCO_SERVICE_URL=http://telco-integration:8010 +FRAUD_DATABASE_URL=http://fraud-detection:8020 + +# ── Extended Microservice URLs ──────────────────────────────────────────────── +ACTUARIAL_SERVICE_URL=http://actuarial-module:8091 +BANCASSURANCE_SERVICE_URL=http://bancassurance-integration:8092 +GROUP_LIFE_SERVICE_URL=http://group-life-admin:8093 +NMID_SERVICE_URL=http://nmid-integration:8094 +PFA_SERVICE_URL=http://pfa-integration:8095 +REINSURANCE_SERVICE_URL=http://reinsurance-management:8096 +KYC_SERVICE_URL=http://enhanced-kyc-kyb:8097 +ANALYTICS_SERVICE_URL=http://analytics-service:8098 +GEOSPATIAL_SERVICE_URL=http://geospatial-service:8099 +COMMUNICATION_SERVICE_URL=http://communication-service:8100 +DOCUMENT_SERVICE_URL=http://document-management:8101 +UNDERWRITING_SERVICE_URL=http://underwriting-service:8102 +ERPNEXT_SERVICE_URL=http://erpnext-integration:8103 +OPENIMIS_SERVICE_URL=http://openimis-integration:8104 +ETHERISC_SERVICE_URL=http://etherisc-gif:8105 +MOJALOOP_SERVICE_URL=http://mojaloop-integration:8106 +GDPR_SERVICE_URL=http://gdpr-compliance:8107 +USSD_SERVICE_URL=http://ussd-gateway:8108 + +# ── AI / LLM ────────────────────────────────────────────────────────────────── +OPENAI_API_KEY= +OPENAI_MODEL=gpt-4o +AI_ADVISOR_ENABLED=true +FRAUD_AI_MODEL_ENDPOINT=http://ray-serve:8000/fraud-detection +CHURN_AI_MODEL_ENDPOINT=http://ray-serve:8000/churn-prediction +UNDERWRITING_AI_MODEL_ENDPOINT=http://ray-serve:8000/underwriting-risk + +# ── Payment Gateways ────────────────────────────────────────────────────────── +PAYSTACK_SECRET_KEY=sk_live_ +PAYSTACK_PUBLIC_KEY=pk_live_ +FLUTTERWAVE_SECRET_KEY=FLWSECK_ +FLUTTERWAVE_PUBLIC_KEY=FLWPUBK_ +INTERSWITCH_CLIENT_ID= +INTERSWITCH_CLIENT_SECRET= +REMITA_MERCHANT_ID= +REMITA_API_KEY= + +# ── Nigerian Telcos (for credit scoring) ───────────────────────────────────── +MTN_API_KEY= +MTN_API_SECRET= +AIRTEL_API_KEY= +AIRTEL_API_SECRET= +GLO_API_KEY= +NINE_MOBILE_API_KEY= + +# ── NAICOM & Regulatory ─────────────────────────────────────────────────────── +NAICOM_API_KEY= +NAICOM_API_URL=https://api.naicom.gov.ng +NMID_API_KEY= +NMID_API_URL=https://api.nmid.gov.ng +NIN_VERIFICATION_API_KEY= +NIN_API_URL=https://api.nimc.gov.ng +BVN_VERIFICATION_API_KEY= +BVN_API_URL=https://api.nibss-plc.org.ng +CAC_API_KEY= + +# ── SMS / Email / Push Notifications ───────────────────────────────────────── +TWILIO_ACCOUNT_SID= +TWILIO_AUTH_TOKEN= +TWILIO_PHONE_NUMBER=+1234567890 +TERMII_API_KEY= +SENDGRID_API_KEY=SG. +EMAIL_FROM=noreply@insurance.example.com +FIREBASE_SERVER_KEY= +FIREBASE_PROJECT_ID= + +# ── WhatsApp Business API ───────────────────────────────────────────────────── +WHATSAPP_API_URL=https://graph.facebook.com/v18.0 +WHATSAPP_PHONE_NUMBER_ID= +WHATSAPP_ACCESS_TOKEN= +WHATSAPP_VERIFY_TOKEN= + +# ── Storage ─────────────────────────────────────────────────────────────────── +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=af-south-1 +S3_BUCKET_DOCUMENTS=insurance-documents-prod +S3_BUCKET_CLAIMS=insurance-claims-prod +S3_BUCKET_BACKUPS=insurance-backups-prod +# Or use MinIO for on-premise +MINIO_ENDPOINT=http://minio:9000 +MINIO_ACCESS_KEY= +MINIO_SECRET_KEY= + +# ── Observability ───────────────────────────────────────────────────────────── +OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 +OTEL_SERVICE_NAME=customer-portal +JAEGER_ENDPOINT=http://jaeger:14268/api/traces +PROMETHEUS_METRICS_PORT=9090 +LOG_LEVEL=info +LOG_FORMAT=json + +# ── Security ────────────────────────────────────────────────────────────────── +VAULT_ADDR=http://vault:8200 +VAULT_TOKEN= +VAULT_ROLE=insurance-platform +ENCRYPTION_KEY= +CORS_ORIGINS=https://insurance.example.com,https://admin.insurance.example.com +RATE_LIMIT_MAX=100 +RATE_LIMIT_WINDOW_MS=60000 + +# ── Feature Flags (Unleash) ─────────────────────────────────────────────────── +UNLEASH_URL=http://unleash:4242/api +UNLEASH_API_TOKEN= +UNLEASH_APP_NAME=insurance-platform +UNLEASH_ENVIRONMENT=production + +# ── ERPNext Integration ─────────────────────────────────────────────────────── +ERPNEXT_URL=https://erp.insurance.example.com +ERPNEXT_API_KEY= +ERPNEXT_API_SECRET= + +# ── OpenIMIS Integration ────────────────────────────────────────────────────── +OPENIMIS_URL=https://openimis.insurance.example.com +OPENIMIS_USERNAME= +OPENIMIS_PASSWORD= + +# ── Etherisc Parametric Insurance ──────────────────────────────────────────── +ETHERISC_API_KEY= +ETHERISC_PRODUCT_ID= +CHAINLINK_NODE_URL=http://chainlink-node:6688 +WEATHER_API_KEY= + +# ── Mojaloop Payments ───────────────────────────────────────────────────────── +MOJALOOP_HUB_URL=https://mojaloop.insurance.example.com +MOJALOOP_DFSP_ID= +MOJALOOP_JWS_KEY= + +# ── Analytics ───────────────────────────────────────────────────────────────── +VITE_ANALYTICS_ENDPOINT=https://analytics.insurance.example.com +VITE_ANALYTICS_WEBSITE_ID= +APACHE_PINOT_URL=http://pinot-broker:8099 +APACHE_ICEBERG_CATALOG_URL=http://iceberg-rest:8181 + +# ── Geospatial ──────────────────────────────────────────────────────────────── +GOOGLE_MAPS_API_KEY= +MAPBOX_ACCESS_TOKEN= + +# ── Owner / Admin ───────────────────────────────────────────────────────────── +OWNER_OPEN_ID=admin +ADMIN_EMAIL=admin@insurance.example.com + +# ── Internal API ───────────────────────────────────────────────────────────── +BUILT_IN_FORGE_API_URL=http://localhost:8080 +BUILT_IN_FORGE_API_KEY= diff --git a/customer-portal-full/.gitignore b/customer-portal-full/.gitignore new file mode 100644 index 000000000..c1dbd8b34 --- /dev/null +++ b/customer-portal-full/.gitignore @@ -0,0 +1,107 @@ +# Dependencies +**/node_modules +.pnpm-store/ + +# Build outputs +dist/ +build/ +*.dist + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock +*.bak + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Database +*.db +*.sqlite +*.sqlite3 diff --git a/customer-portal-full/.gitkeep b/customer-portal-full/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/customer-portal-full/.prettierignore b/customer-portal-full/.prettierignore new file mode 100644 index 000000000..72842592f --- /dev/null +++ b/customer-portal-full/.prettierignore @@ -0,0 +1,35 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +build/ +*.dist + +# Generated files +*.tsbuildinfo +coverage/ + +# Package files +package-lock.json +pnpm-lock.yaml + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log + +# Environment files +.env* + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db diff --git a/customer-portal-full/.prettierrc b/customer-portal-full/.prettierrc new file mode 100644 index 000000000..67c0bc83c --- /dev/null +++ b/customer-portal-full/.prettierrc @@ -0,0 +1,15 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "proseWrap": "preserve" +} diff --git a/customer-portal-full/Dockerfile b/customer-portal-full/Dockerfile new file mode 100644 index 000000000..9a8d30930 --- /dev/null +++ b/customer-portal-full/Dockerfile @@ -0,0 +1,52 @@ +# Multi-stage build for customer portal + +# Stage 1: Build frontend +FROM node:22-alpine AS frontend-builder +WORKDIR /app + +# Copy package files +COPY package.json pnpm-lock.yaml ./ +RUN npm install -g pnpm && pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Build frontend +RUN pnpm build + +# Stage 2: Build backend +FROM node:22-alpine AS backend-builder +WORKDIR /app + +# Copy package files +COPY package.json pnpm-lock.yaml ./ +RUN npm install -g pnpm && pnpm install --frozen-lockfile --prod + +# Stage 3: Production image +FROM node:22-alpine +WORKDIR /app + +# Install production dependencies +RUN npm install -g pnpm + +# Copy package files +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile --prod + +# Copy built frontend from frontend-builder +COPY --from=frontend-builder /app/dist ./dist + +# Copy server code +COPY server ./server +COPY shared ./shared +COPY drizzle ./drizzle + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Start server +CMD ["node", "server/_core/index.js"] diff --git a/customer-portal-full/client/index.html b/customer-portal-full/client/index.html new file mode 100644 index 000000000..350f76c79 --- /dev/null +++ b/customer-portal-full/client/index.html @@ -0,0 +1,51 @@ + + + + + + + Unified Insurance Platform + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + diff --git a/customer-portal-full/client/public/.gitkeep b/customer-portal-full/client/public/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/customer-portal-full/client/public/__manus__/debug-collector.js b/customer-portal-full/client/public/__manus__/debug-collector.js new file mode 100644 index 000000000..050455560 --- /dev/null +++ b/customer-portal-full/client/public/__manus__/debug-collector.js @@ -0,0 +1,821 @@ +/** + * Manus Debug Collector (agent-friendly) + * + * Captures: + * 1) Console logs + * 2) Network requests (fetch + XHR) + * 3) User interactions (semantic uiEvents: click/type/submit/nav/scroll/etc.) + * + * Data is periodically sent to /__manus__/logs + * Note: uiEvents are mirrored to sessionEvents for sessionReplay.log + */ +(function () { + "use strict"; + + // Prevent double initialization + if (window.__MANUS_DEBUG_COLLECTOR__) return; + + // ========================================================================== + // Configuration + // ========================================================================== + const CONFIG = { + reportEndpoint: "/__manus__/logs", + bufferSize: { + console: 500, + network: 200, + // semantic, agent-friendly UI events + ui: 500, + }, + reportInterval: 2000, + sensitiveFields: [ + "password", + "token", + "secret", + "key", + "authorization", + "cookie", + "session", + ], + maxBodyLength: 10240, + // UI event logging privacy policy: + // - inputs matching sensitiveFields or type=password are masked by default + // - non-sensitive inputs log up to 200 chars + uiInputMaxLen: 200, + uiTextMaxLen: 80, + // Scroll throttling: minimum ms between scroll events + scrollThrottleMs: 500, + }; + + // ========================================================================== + // Storage + // ========================================================================== + const store = { + consoleLogs: [], + networkRequests: [], + uiEvents: [], + lastReportTime: Date.now(), + lastScrollTime: 0, + }; + + // ========================================================================== + // Utility Functions + // ========================================================================== + + function sanitizeValue(value, depth) { + if (depth === void 0) depth = 0; + if (depth > 5) return "[Max Depth]"; + if (value === null) return null; + if (value === undefined) return undefined; + + if (typeof value === "string") { + return value.length > 1000 ? value.slice(0, 1000) + "...[truncated]" : value; + } + + if (typeof value !== "object") return value; + + if (Array.isArray(value)) { + return value.slice(0, 100).map(function (v) { + return sanitizeValue(v, depth + 1); + }); + } + + var sanitized = {}; + for (var k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + var isSensitive = CONFIG.sensitiveFields.some(function (f) { + return k.toLowerCase().indexOf(f) !== -1; + }); + if (isSensitive) { + sanitized[k] = "[REDACTED]"; + } else { + sanitized[k] = sanitizeValue(value[k], depth + 1); + } + } + } + return sanitized; + } + + function formatArg(arg) { + try { + if (arg instanceof Error) { + return { type: "Error", message: arg.message, stack: arg.stack }; + } + if (typeof arg === "object") return sanitizeValue(arg); + return String(arg); + } catch (e) { + return "[Unserializable]"; + } + } + + function formatArgs(args) { + var result = []; + for (var i = 0; i < args.length; i++) result.push(formatArg(args[i])); + return result; + } + + function pruneBuffer(buffer, maxSize) { + if (buffer.length > maxSize) buffer.splice(0, buffer.length - maxSize); + } + + function tryParseJson(str) { + if (typeof str !== "string") return str; + try { + return JSON.parse(str); + } catch (e) { + return str; + } + } + + // ========================================================================== + // Semantic UI Event Logging (agent-friendly) + // ========================================================================== + + function shouldIgnoreTarget(target) { + try { + if (!target || !(target instanceof Element)) return false; + return !!target.closest(".manus-no-record"); + } catch (e) { + return false; + } + } + + function compactText(s, maxLen) { + try { + var t = (s || "").trim().replace(/\s+/g, " "); + if (!t) return ""; + return t.length > maxLen ? t.slice(0, maxLen) + "…" : t; + } catch (e) { + return ""; + } + } + + function elText(el) { + try { + var t = el.innerText || el.textContent || ""; + return compactText(t, CONFIG.uiTextMaxLen); + } catch (e) { + return ""; + } + } + + function describeElement(el) { + if (!el || !(el instanceof Element)) return null; + + var getAttr = function (name) { + return el.getAttribute(name); + }; + + var tag = el.tagName ? el.tagName.toLowerCase() : null; + var id = el.id || null; + var name = getAttr("name") || null; + var role = getAttr("role") || null; + var ariaLabel = getAttr("aria-label") || null; + + var dataLoc = getAttr("data-loc") || null; + var testId = + getAttr("data-testid") || + getAttr("data-test-id") || + getAttr("data-test") || + null; + + var type = tag === "input" ? (getAttr("type") || "text") : null; + var href = tag === "a" ? getAttr("href") || null : null; + + // a small, stable hint for agents (avoid building full CSS paths) + var selectorHint = null; + if (testId) selectorHint = '[data-testid="' + testId + '"]'; + else if (dataLoc) selectorHint = '[data-loc="' + dataLoc + '"]'; + else if (id) selectorHint = "#" + id; + else selectorHint = tag || "unknown"; + + return { + tag: tag, + id: id, + name: name, + type: type, + role: role, + ariaLabel: ariaLabel, + testId: testId, + dataLoc: dataLoc, + href: href, + text: elText(el), + selectorHint: selectorHint, + }; + } + + function isSensitiveField(el) { + if (!el || !(el instanceof Element)) return false; + var tag = el.tagName ? el.tagName.toLowerCase() : ""; + if (tag !== "input" && tag !== "textarea") return false; + + var type = (el.getAttribute("type") || "").toLowerCase(); + if (type === "password") return true; + + var name = (el.getAttribute("name") || "").toLowerCase(); + var id = (el.id || "").toLowerCase(); + + return CONFIG.sensitiveFields.some(function (f) { + return name.indexOf(f) !== -1 || id.indexOf(f) !== -1; + }); + } + + function getInputValueSafe(el) { + if (!el || !(el instanceof Element)) return null; + var tag = el.tagName ? el.tagName.toLowerCase() : ""; + if (tag !== "input" && tag !== "textarea" && tag !== "select") return null; + + var v = ""; + try { + v = el.value != null ? String(el.value) : ""; + } catch (e) { + v = ""; + } + + if (isSensitiveField(el)) return { masked: true, length: v.length }; + + if (v.length > CONFIG.uiInputMaxLen) v = v.slice(0, CONFIG.uiInputMaxLen) + "…"; + return v; + } + + function logUiEvent(kind, payload) { + var entry = { + timestamp: Date.now(), + kind: kind, + url: location.href, + viewport: { width: window.innerWidth, height: window.innerHeight }, + payload: sanitizeValue(payload), + }; + store.uiEvents.push(entry); + pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui); + } + + function installUiEventListeners() { + // Clicks + document.addEventListener( + "click", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("click", { + target: describeElement(t), + x: e.clientX, + y: e.clientY, + }); + }, + true + ); + + // Typing "commit" events + document.addEventListener( + "change", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("change", { + target: describeElement(t), + value: getInputValueSafe(t), + }); + }, + true + ); + + document.addEventListener( + "focusin", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("focusin", { target: describeElement(t) }); + }, + true + ); + + document.addEventListener( + "focusout", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("focusout", { + target: describeElement(t), + value: getInputValueSafe(t), + }); + }, + true + ); + + // Enter/Escape are useful for form flows & modals + document.addEventListener( + "keydown", + function (e) { + if (e.key !== "Enter" && e.key !== "Escape") return; + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("keydown", { key: e.key, target: describeElement(t) }); + }, + true + ); + + // Form submissions + document.addEventListener( + "submit", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("submit", { target: describeElement(t) }); + }, + true + ); + + // Throttled scroll events + window.addEventListener( + "scroll", + function () { + var now = Date.now(); + if (now - store.lastScrollTime < CONFIG.scrollThrottleMs) return; + store.lastScrollTime = now; + + logUiEvent("scroll", { + scrollX: window.scrollX, + scrollY: window.scrollY, + documentHeight: document.documentElement.scrollHeight, + viewportHeight: window.innerHeight, + }); + }, + { passive: true } + ); + + // Navigation tracking for SPAs + function nav(reason) { + logUiEvent("navigate", { reason: reason }); + } + + var origPush = history.pushState; + history.pushState = function () { + origPush.apply(this, arguments); + nav("pushState"); + }; + + var origReplace = history.replaceState; + history.replaceState = function () { + origReplace.apply(this, arguments); + nav("replaceState"); + }; + + window.addEventListener("popstate", function () { + nav("popstate"); + }); + window.addEventListener("hashchange", function () { + nav("hashchange"); + }); + } + + // ========================================================================== + // Console Interception + // ========================================================================== + + var originalConsole = { + log: console.log.bind(console), + debug: console.debug.bind(console), + info: console.info.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console), + }; + + ["log", "debug", "info", "warn", "error"].forEach(function (method) { + console[method] = function () { + var args = Array.prototype.slice.call(arguments); + + var entry = { + timestamp: Date.now(), + level: method.toUpperCase(), + args: formatArgs(args), + stack: method === "error" ? new Error().stack : null, + }; + + store.consoleLogs.push(entry); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + originalConsole[method].apply(console, args); + }; + }); + + window.addEventListener("error", function (event) { + store.consoleLogs.push({ + timestamp: Date.now(), + level: "ERROR", + args: [ + { + type: "UncaughtError", + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + stack: event.error ? event.error.stack : null, + }, + ], + stack: event.error ? event.error.stack : null, + }); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + // Mark an error moment in UI event stream for agents + logUiEvent("error", { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }); + }); + + window.addEventListener("unhandledrejection", function (event) { + var reason = event.reason; + store.consoleLogs.push({ + timestamp: Date.now(), + level: "ERROR", + args: [ + { + type: "UnhandledRejection", + reason: reason && reason.message ? reason.message : String(reason), + stack: reason && reason.stack ? reason.stack : null, + }, + ], + stack: reason && reason.stack ? reason.stack : null, + }); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + logUiEvent("unhandledrejection", { + reason: reason && reason.message ? reason.message : String(reason), + }); + }); + + // ========================================================================== + // Fetch Interception + // ========================================================================== + + var originalFetch = window.fetch.bind(window); + + window.fetch = function (input, init) { + init = init || {}; + var startTime = Date.now(); + // Handle string, Request object, or URL object + var url = typeof input === "string" + ? input + : (input && (input.url || input.href || String(input))) || ""; + var method = init.method || (input && input.method) || "GET"; + + // Don't intercept internal requests + if (url.indexOf("/__manus__/") === 0) { + return originalFetch(input, init); + } + + // Safely parse headers (avoid breaking if headers format is invalid) + var requestHeaders = {}; + try { + if (init.headers) { + requestHeaders = Object.fromEntries(new Headers(init.headers).entries()); + } + } catch (e) { + requestHeaders = { _parseError: true }; + } + + var entry = { + timestamp: startTime, + type: "fetch", + method: method.toUpperCase(), + url: url, + request: { + headers: requestHeaders, + body: init.body ? sanitizeValue(tryParseJson(init.body)) : null, + }, + response: null, + duration: null, + error: null, + }; + + return originalFetch(input, init) + .then(function (response) { + entry.duration = Date.now() - startTime; + + var contentType = (response.headers.get("content-type") || "").toLowerCase(); + var contentLength = response.headers.get("content-length"); + + entry.response = { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + body: null, + }; + + // Semantic network hint for agents on failures (sync, no need to wait for body) + if (response.status >= 400) { + logUiEvent("network_error", { + kind: "fetch", + method: entry.method, + url: entry.url, + status: response.status, + statusText: response.statusText, + }); + } + + // Skip body capture for streaming responses (SSE, etc.) to avoid memory leaks + var isStreaming = contentType.indexOf("text/event-stream") !== -1 || + contentType.indexOf("application/stream") !== -1 || + contentType.indexOf("application/x-ndjson") !== -1; + if (isStreaming) { + entry.response.body = "[Streaming response - not captured]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // Skip body capture for large responses to avoid memory issues + if (contentLength && parseInt(contentLength, 10) > CONFIG.maxBodyLength) { + entry.response.body = "[Response too large: " + contentLength + " bytes]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // Skip body capture for binary content types + var isBinary = contentType.indexOf("image/") !== -1 || + contentType.indexOf("video/") !== -1 || + contentType.indexOf("audio/") !== -1 || + contentType.indexOf("application/octet-stream") !== -1 || + contentType.indexOf("application/pdf") !== -1 || + contentType.indexOf("application/zip") !== -1; + if (isBinary) { + entry.response.body = "[Binary content: " + contentType + "]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // For text responses, clone and read body in background + var clonedResponse = response.clone(); + + // Async: read body in background, don't block the response + clonedResponse + .text() + .then(function (text) { + if (text.length <= CONFIG.maxBodyLength) { + entry.response.body = sanitizeValue(tryParseJson(text)); + } else { + entry.response.body = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]"; + } + }) + .catch(function () { + entry.response.body = "[Unable to read body]"; + }) + .finally(function () { + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + }); + + // Return response immediately, don't wait for body reading + return response; + }) + .catch(function (error) { + entry.duration = Date.now() - startTime; + entry.error = { message: error.message, stack: error.stack }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + logUiEvent("network_error", { + kind: "fetch", + method: entry.method, + url: entry.url, + message: error.message, + }); + + throw error; + }); + }; + + // ========================================================================== + // XHR Interception + // ========================================================================== + + var originalXHROpen = XMLHttpRequest.prototype.open; + var originalXHRSend = XMLHttpRequest.prototype.send; + + XMLHttpRequest.prototype.open = function (method, url) { + this._manusData = { + method: (method || "GET").toUpperCase(), + url: url, + startTime: null, + }; + return originalXHROpen.apply(this, arguments); + }; + + XMLHttpRequest.prototype.send = function (body) { + var xhr = this; + + if ( + xhr._manusData && + xhr._manusData.url && + xhr._manusData.url.indexOf("/__manus__/") !== 0 + ) { + xhr._manusData.startTime = Date.now(); + xhr._manusData.requestBody = body ? sanitizeValue(tryParseJson(body)) : null; + + xhr.addEventListener("load", function () { + var contentType = (xhr.getResponseHeader("content-type") || "").toLowerCase(); + var responseBody = null; + + // Skip body capture for streaming responses + var isStreaming = contentType.indexOf("text/event-stream") !== -1 || + contentType.indexOf("application/stream") !== -1 || + contentType.indexOf("application/x-ndjson") !== -1; + + // Skip body capture for binary content types + var isBinary = contentType.indexOf("image/") !== -1 || + contentType.indexOf("video/") !== -1 || + contentType.indexOf("audio/") !== -1 || + contentType.indexOf("application/octet-stream") !== -1 || + contentType.indexOf("application/pdf") !== -1 || + contentType.indexOf("application/zip") !== -1; + + if (isStreaming) { + responseBody = "[Streaming response - not captured]"; + } else if (isBinary) { + responseBody = "[Binary content: " + contentType + "]"; + } else { + // Safe to read responseText for text responses + try { + var text = xhr.responseText || ""; + if (text.length > CONFIG.maxBodyLength) { + responseBody = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]"; + } else { + responseBody = sanitizeValue(tryParseJson(text)); + } + } catch (e) { + // responseText may throw for non-text responses + responseBody = "[Unable to read response: " + e.message + "]"; + } + } + + var entry = { + timestamp: xhr._manusData.startTime, + type: "xhr", + method: xhr._manusData.method, + url: xhr._manusData.url, + request: { body: xhr._manusData.requestBody }, + response: { + status: xhr.status, + statusText: xhr.statusText, + body: responseBody, + }, + duration: Date.now() - xhr._manusData.startTime, + error: null, + }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + if (entry.response && entry.response.status >= 400) { + logUiEvent("network_error", { + kind: "xhr", + method: entry.method, + url: entry.url, + status: entry.response.status, + statusText: entry.response.statusText, + }); + } + }); + + xhr.addEventListener("error", function () { + var entry = { + timestamp: xhr._manusData.startTime, + type: "xhr", + method: xhr._manusData.method, + url: xhr._manusData.url, + request: { body: xhr._manusData.requestBody }, + response: null, + duration: Date.now() - xhr._manusData.startTime, + error: { message: "Network error" }, + }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + logUiEvent("network_error", { + kind: "xhr", + method: entry.method, + url: entry.url, + message: "Network error", + }); + }); + } + + return originalXHRSend.apply(this, arguments); + }; + + // ========================================================================== + // Data Reporting + // ========================================================================== + + function reportLogs() { + var consoleLogs = store.consoleLogs.splice(0); + var networkRequests = store.networkRequests.splice(0); + var uiEvents = store.uiEvents.splice(0); + + // Skip if no new data + if ( + consoleLogs.length === 0 && + networkRequests.length === 0 && + uiEvents.length === 0 + ) { + return Promise.resolve(); + } + + var payload = { + timestamp: Date.now(), + consoleLogs: consoleLogs, + networkRequests: networkRequests, + // Mirror uiEvents to sessionEvents for sessionReplay.log + sessionEvents: uiEvents, + // agent-friendly semantic events + uiEvents: uiEvents, + }; + + return originalFetch(CONFIG.reportEndpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }).catch(function () { + // Put data back on failure (but respect limits) + store.consoleLogs = consoleLogs.concat(store.consoleLogs); + store.networkRequests = networkRequests.concat(store.networkRequests); + store.uiEvents = uiEvents.concat(store.uiEvents); + + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui); + }); + } + + // Periodic reporting + setInterval(reportLogs, CONFIG.reportInterval); + + // Report on page unload + window.addEventListener("beforeunload", function () { + var consoleLogs = store.consoleLogs; + var networkRequests = store.networkRequests; + var uiEvents = store.uiEvents; + + if ( + consoleLogs.length === 0 && + networkRequests.length === 0 && + uiEvents.length === 0 + ) { + return; + } + + var payload = { + timestamp: Date.now(), + consoleLogs: consoleLogs, + networkRequests: networkRequests, + // Mirror uiEvents to sessionEvents for sessionReplay.log + sessionEvents: uiEvents, + uiEvents: uiEvents, + }; + + if (navigator.sendBeacon) { + var payloadStr = JSON.stringify(payload); + // sendBeacon has ~64KB limit, truncate if too large + var MAX_BEACON_SIZE = 60000; // Leave some margin + if (payloadStr.length > MAX_BEACON_SIZE) { + // Prioritize: keep recent events, drop older logs + var truncatedPayload = { + timestamp: Date.now(), + consoleLogs: consoleLogs.slice(-50), + networkRequests: networkRequests.slice(-20), + sessionEvents: uiEvents.slice(-100), + uiEvents: uiEvents.slice(-100), + _truncated: true, + }; + payloadStr = JSON.stringify(truncatedPayload); + } + navigator.sendBeacon(CONFIG.reportEndpoint, payloadStr); + } + }); + + // ========================================================================== + // Initialization + // ========================================================================== + + // Install semantic UI listeners ASAP + try { + installUiEventListeners(); + } catch (e) { + console.warn("[Manus] Failed to install UI listeners:", e); + } + + // Mark as initialized + window.__MANUS_DEBUG_COLLECTOR__ = { + version: "2.0-no-rrweb", + store: store, + forceReport: reportLogs, + }; + + console.debug("[Manus] Debug collector initialized (no rrweb, UI events only)"); +})(); diff --git a/customer-portal-full/client/public/icons/icon-128x128.png b/customer-portal-full/client/public/icons/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..0ad3a0c39bc32b9ab5f3d5d680c607e8b5ccff16 GIT binary patch literal 1023 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU_R#Q;uumf=k46xg(8k3uF)cP zYGRx#QiK@!*ixKqQkr;Vln#Wju`q>A-?mGvOk(f(y|wQ*Zr%F6_}N#V?6PO?&fflg z|L}=VtE?F=ce4r{XX;31JhGADQ3OMwPJ@M*gABJqA5P)trR5BwHZ?yhUlseu-)DDk zI)1nBJIlP>AG&2CN!*N^r0Tk5TNa2dSp8A^j8Xts!=)dhZh|XV9CH6~P390`+F(-G z>d2D87$LpCNr&l;+XcO|^32xa9*h&X%fznCSul;^ijr&d7L@}&3{#)=H%`@VVenvG z$)x(cj4^=gBBRs0wM+^k3ueEQ+QNBZu7%Lttrbu-v7S+-@Ci*{U;~JEPwne z;kC3=pV^;l&v&(~SWv^1BL6picHNX5$yLi8Rx->ydp_@v*D{W&LR03lyu0$}&HFVr z?u+c$Uu68*`aa#xU1KJLq+7Z9|4BU&tCl(Z_In^XVV1pP&r6P%OfIu~VqUzsp|y`pRtScz?DCo-e)qITxOmQI+5*ibe<)vhFZf(Wmg9I zzvmbDGv46b;QU$r#Lfd;30Vyy3_m+8xwHHpGW`f!Z)23jZOP~(TfnrS?-YkXakKD& zD?(Zg*ZVzCHJU3d z9k~)-HHb2N?XYAn($i#eSsQwy)!HkBFYgTe~DWM4f?i{94 literal 0 HcmV?d00001 diff --git a/customer-portal-full/client/public/icons/icon-144x144.png b/customer-portal-full/client/public/icons/icon-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..f2f1ffde5751aa00554a3f4c533f72b0b1d0a9e1 GIT binary patch literal 1160 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$farfyLj`#WAE}&fB}TlZ3qmTpzX^ zIDRCF`N)hl4D8HhhpKLHN?5F6C@wqTbYN9SU)m9mQ|nGuZ$6*CfBw2Ti*tNm+kSlX zb#{3@(~irhUvW2>GdXcFDz-LEaB}caR9GS)5X8Zu$)(UnkDedB`EPwsb@I+@Hy35eUwD%+@3QTRZH%Gqo3EQL31eL8TI{)eMik2vp>uKV z6V@;n@ur`*RJr}!A@)({2fOaOY(;Y4Oz&))c$MvyoPW!GNnNuEw*&egZRmYsej)O4 z=Ld`3B@TSdb9`&rHZiW`&r^LG>A|+dW`ou$B^!L9yMH9an?gkVe3JS z;8{~$9K4td7aDE;#NMgZ{NwNE*n5MoAvWlL1%jQGt>2VoP^|4Q!I`d zTg3m~prSCfU;A9j)iZ0&1KPtk{FcmNE|UKb+!+1Jjy)G>qf9#v9k|IXxI47SQtcfsQOTXybI>M!^bnf@>6 zFq>0Xs6+YyaN0xtny>`~luWVf(>R7FTA*Him=6a6f@<+~O$L;)h`%h)!WA;V= zdd2!S{y%>y`bJlH+b8~vqg&V3?ce#w_ClocD+zy>#nCtBoznF+e5O40&(4p6ulKX% zmwK!|ys|1JNG!nRY`Nw+zLiOjL+sMCtGRclTwA;L_u5yJF7b*i{M@^4JKItrpRd9H zJ~b$nC=~_E`nQ~Lm*i?LSZrk62^3;`o)aP&l0At3(^yWj@~@MDzeaW zMHVm_@@P1fCx}i-SCkHD`Su_zaDHQngWxabIVT^!5#kqGUpkGwkMAMRC+UpSnH#S%%dAB^>)jQ{~WE6|Zj+QHma~6g=L}wY6cBQS( zb#N~Ah&?QMi+zi^^@?`~-^|rpu>6kn)tgC`!2Emko27sWv3VCYr%%}*X%ecn=j#T2 QUFVdQ&MBb@0G4(Jq5uE@ literal 0 HcmV?d00001 diff --git a/customer-portal-full/client/public/icons/icon-152x152.png b/customer-portal-full/client/public/icons/icon-152x152.png new file mode 100644 index 0000000000000000000000000000000000000000..c65ab3c53422b179ee56949ad7db3ba7b01a4997 GIT binary patch literal 1239 zcmeAS@N?(olHy`uVBq!ia0vp^GeDSw4M<8HQcz@IV43IX;uumf=j|QCNh0n7tq)^Z z?bN2}IV2P@SsE>w{lexBi)4`nqnh~xg%hs=jFWE6?W~M^pZa$`@5b2B`t9 z@b6&wr1zQ6|8FbH_t<{Ra?e|)*Iln>-A%f!wBc%C>}Kbj#mvRLuP;5haEx`D&{fmR zNz%d*RT0ydwU|A!y|HD-s%L97tD1Z*Tb`@~?g-1YZ5zEjs}uis_}n@pdPjcdHQ}Be^PcZ>-`2P@e~$3;y_R347m`EF)x z!aC`x2MXV>VJg0}z$C9aLsa6~#!BCZWzTE>emU=3|L^+!I`P<(wJ~w=5iHLHuCH~p zJ+5fceqtK?Hlua!?xkl>-3W8v_Ow3wHP`D;uPuGH=6S0}eE;%_@Aa&A7sC8DrrNu{ zlL`<0zje(Yvy?x|mo(RGj+LA3trl@z_NmnM$m^!-W+lCC6ukc5w=d_(>_1C(@O57Q zBeb@$^p4|6_N#?&?*`{wVe#&JZIH;Dp3F46#9;EZOJXyMO#H1TG#B~K?LYa0)68() z;g+piS)P4Zulix{I^)iH3BvC_JYTZj&}_q(-utoU-Z&}^cx&d9hEdJ18 z>AmO-pOvFPySP+^|tm7%c(3aeM#oPxV(-7oa{^I$1g6z_NOw2fCBKIf>nP)qnoj zX|N$6y;@oumb|wOfm`Lt9Q>zb(zlKZm7}+=+P^hEs30nNqc2#e^z5et6%Svnew?GE z9ufLZwzmJt)?;j8Q}=$4k?gw8ZwGe0Td;)r z^{#88>$|7<7%2Td|76FiRdKpMSM~NWm;4Sn(DFk*@#go0yX8|ZHs|hNd{4fdSydKocW{-B-KRenH(I)Et*Q#0$rAQd@aE06 z2Q67jp9zFd4NM5Fo4B;7b9L5>*)=~laGiO1&im=M_OQm#pSb}-$MwYbMGb zV=bE|bbHH+b8hTq1-!XM5$(E6*3q)J`2y$WirFVdQ&MBb@0E=--AOHXW literal 0 HcmV?d00001 diff --git a/customer-portal-full/client/public/icons/icon-192x192.png b/customer-portal-full/client/public/icons/icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..92e7202fa5fb8c61bd4e958e0adec43138348923 GIT binary patch literal 1588 zcmb`I`#;lr9LGQ3T@{-xNgTv>N-Z1Fy0~pQLvl{0$Ak`A9hV9rgo=$Fx5%YjszY0s zM-dM+DYrB0$ZcdrMYWyKIl0Xvx1)@0$2q^8^9S^JJYVnEkFVD+@5d{PdC(2B)@UsN z0OP)&acI@W|40k9dJoCM(EuQq-5L8>38mBW9YPrkmo}1I7?Y7Z#GOhDOwof4Wet8P zqtky|Yb-KJFyA6zZ7_P4vy&)yl8LL3ebTe6yNQEfPnIZjK@b7>62Bhj zOH@$PdDYx>MXP3d>K@N&&Fhw#O`VP%b=e|=W5zsSy7Sz~C-clsoEUq`gM---?grvn ze#)@SkzFufucV0@^K=h{^+4QgiPne(OpD)slz^BA{sx@xy@(#*yAqdcKBOWcdN^et z!oyF4&bFlVo(Og{ymn=|v9~G-P|%5}j5qa3$TNFd3YtXg>RTXR^@q5!lg*&Xv@kWd zE<&H)2|qLt(GdX~KWi;3a*)J# z=>{a-mdg8~hbY|pytB;N@B=CmqaE@TY34syx2gxFtz`@DeLfR|TOk}}U%NoM_29;Mxd7BsAovRm(LOg)F_hsw#VlcQD@ zX#!)21`lTj&sq$Od28=0Q1Id94ysDBrc@H@v$uzO87kNLJ~-vZ%{dcRUu>3-K%e9h zvpz7Z`bsk-DA^16$!la@03e_@E#KYB#8=*AdL?M))=LJq!lo!w3-eANh*E@>+$oR4R=7 zf9i{&8BLpG;NZ1KI$-Ej0*E|HyQFrzm6;TzNwknQ&7v|rXY4|X8JQ`c)q0Ob?JHCxEc>5mUO+AIT8+&^RVEFn=;2M}j z@q9?kDfM2gv|cUihZK{e`0sN1;Tw~XO(rv@w?nU}lzQekI*QQnhn-#z;ASy^q0h}J zy~rOrEY}Be2n<-4GoqKig855&c`oiwOx-Qu4Au~S4d!JLf zWg_#RnZq(ZHDvgMH=db&+Lu*wq4eytW`(Zc#F0XZCQ_zo_t_E;XN)^+v$M>V#54B} zZ0-hxSkzr1J&(DhAcZ!j`v@@ie_dB$nT)jhM7yw3pfW$5sG1`$6aAT(ebKW;d^H{k z%Y2?==4^pzZ_j&vRGmqH7~`(ih98L{L{PG64;rA4 zGHilw=bIrph>@W*(f%u0aX@bnS8Bj<2DTLQ+AlP`_+s^{muL;=PVT5(c#Icgy^pge z9uv9figs~7EgfKO?6fwhHzJ~hp>6CJQWsDg^0^WxHipDTUyrw~)V6k|JyFuPx(2}A LP$8Eg3UxqH zlvD=olU5KFn<}UX0U1<6qPSE*3J6MqN--FeAtWzg>-+tF_QzT4th3Ls?>YM$Zf;1h zzoGs@eE={FSnabO0ENspJWe-qzV%u?0Kq=M$160YXylLESE>Hw16z^~d92;E2J!ud zbocnVb|0nfiOs8yS{%Dan2(Q(+^j@5$t2vpH>dZ@Ou(SoC9QX>r*+-*TjistCa^>E zXc_|#4gzEw0}_G;;u-*NJ@B+e0p1$m92V#&|2e_l4;rZYRaUR*QpvPq&+^u4_p>{0 zeQwT48<}Bn%byr%BG3&%ScGw;PVv+j=Vt|kbKQ-6A0j0K9{A4n6kfp27Stc5Ct*=% z%>w!6F`moN;>*mL>rKd4dazEUnkHgOtYLf1aGo!r(;LE@2h7h9s}4X_PBX`p!PP_c z+XU9*9Qw(qem63It+&`3)gMjf;~4z~z*QbJMq}xP&9Y%D>^=1%?l~quN+^g-fGxD$ zf9{~(wEzY){RuIXP>0s%PKr(m@AczC3Rg)M_uyV7V{Gq_#oO`9S2!avw2hmRsTZ0J z&l1~8pGd-&C%`=RNmaj-J;xcj7okzbydPQuCOcucogi%w^E63TP>x^89vZ~io3)jG zEo`5}i+yPJe(`>C3pkNfRVaukKG+91xxE9c-r#V%A(wlKBVyf&H9@>w*r8iawORuv znR}rnC@%nTgJ(kux)$dFp$T=)mE}Q3QL?kV^hF*QjJFd{`r6-#SpIEmNG>Q7SKWy`tI z=qk%L{;i5YE!m+n82jE5+^(vt1HM(C&gN1{ow}Q0M^1I{Uyt+&C`l`wkwq!f(Jcef ze)v;63{pzK{oWW~GSKITUu=>gpU}_YF*Wj9&5R!EQVq(z;hA<9nNRgjD*jfyTN9r| zjnF)e(2Pvnx-E_9k%oD`X-sxKa7EC0f-1)KvZWE4)^~f-rNW+;L|A6_dm{6kLyh77 z!4Jr;9_VZuGd~#Lun}FAS*5p?u1ncCFzoQo>a z%>%$Z<`FV%5a%~6W6nrcSVK=i-D=)Ymof+3kEw08WJv5L&1_2lD8nKb^G*G~EU~V} zix-tA63KpFmCII+6YQ*^C}8t^J@;={=!p$;6OG6|X}L1Ck~td){#DX^?7nEI03wgS zZ{adt1ne8y8o&1o6#n#e{6ve$A2K7LFk?G9iSe>R(f@8~9C^L;{X#U+_N7=vnJ(*D=TpJGLUn(9^f~!T0TlA6v}Q-&uSj zkJeoIm}G{$7*!*qWq)Sh1)zgVUFqh*z47}Z(gJ`w5)yD@FH2vEG#ooId-3dv4cli(6U1`>o@0ij@sOk;&hxW3yq{Eo_KKmd`yF`k3mq zM^#k!(HwoxFE971csRLk_N*F(MnXket6V?W?lJF5cS5+u3V>Q7g08ro>)`R%gAhCi z_N0mbRqn=cgatS9cg`1WoKrZ&yH@z{C$o#zPEg)AiIV5<@!Ad*V+Z_}@h&dbUF?EI zNJSVY@rO5;UIlIV0~P4|8PJ6=f5Z5ZBZI=Sn**4TNqt5CSmk|I=;Rx_1?GT+qC+BI zIDN$t-@B3X*t01$Cwm*S|>B7>B<{fL( zVe0bgTT3@#Q#(phyM<9}fL$D%YQPZ2w=YB0FqB6cVlLPz&6bqB)Ipk+YhWRWZ?i_# zSdOwmvR4a7_wiv!c{5xyHTXeVn!T;Vl=J!qs3axdN7vEb~%t+Qh{0w$aZW&df^lv?ZZA%sVYyEw7!qBF9x_c)7)s7RmZ1 zUE-6e9JI)QLo~lL5Wg+~sHuZB(##)P`$l$22X?1pQu6yS2|KBs2b=k4UTSI4sE3EI z2ufm=kA9+xje5|hNSSd=O`ML{*THTJbemqoF0wZHl-6Vayr$>9D{iVN;Zv#?xGKwP zWTeBJt4x%S@Hyxz7AYm_u`VPNsXthQVU9SRmzPmiZ{Eh|r(?L^pEmfniXJ{8X)-)Z zszzc}MwtRWAyNA;yyq&52T!ARRK1w~P5q37wD`bvk^0#fCu^Go?l?(<_svO{QU6-p zlO4RHI<<3nP<2?8Ow*}d!-0=#*Fa!L%D2;WB5AKkk_r{8z}4aH+r|imKe#$hoZpF? zGDf5{STCH>V7Xv~-YQX3uw0@M_6x}r8_T5|;T{W>SMg#ljacFMY>&Qe`&RX zES*+2GV=lfH`H(M#&UPkT69{e+hQb51EpD1u@%jhw`HT80VX@DD8**Qhajj06&uRi z>5+~pEbN;{$!O--IU~6)DIcMCLT4nJ-?YoZJOvYLaxBBuyJ}GL2e=(NFzGCaZ8hrs zcgm+Pa3Lo#ZBdf-n-c)lgX?S@xFR#8BUzG7Jew@4C#4$o=hX=j-qV)Rzx)vA#1+Kb z9GqBLSc zMF#?7IiBocNrVHTa~)&&#c?l%xtC`FCp%n;WcLER&i!gFDF09k{E6>Bx%nBx?(?bY UukX@>biWV~;2Z37*_)gBUq>9?ivR!s literal 0 HcmV?d00001 diff --git a/customer-portal-full/client/public/icons/icon-512x512.png b/customer-portal-full/client/public/icons/icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..c3a7f91b5733823d6a7e582b73883f67be885efd GIT binary patch literal 4431 zcmeI0Ygkh07RT4#AfcgxmPu-2YFT1|m|BAhY1zfnrYtE_ij*$qCCemJw_UWd)G}{r zN}XvgO=n`F(x!`;rI{pFQ)ZV5bQCKqO;ao{XR9;QGv_%U&WH2yd}8ym_TKMW@4Np0 z-^;;(rM`v+6Ab{saNz=J$iaD5h&7`4rG>A|=&{EincSLx0LAQxdmN_DdIA+o=D%w6DWQOS_ zw0&@u3QSd#m}H}PO@!JDx*vFC*>ps)jhQsv1U-j`QPNAslaZA+;DCqCn~ULLVS@g4 z9yuTfq?YL=R{F@{{!m+rDk%L;Z2SUZVS&i>>2fiTP4i#wzrpnuOIdlg(=j{-K8` z>`64GradNS6%?)4@vK7hqf;%wNZx_h+%N%iGPSBetKq{f4w;cH&jfNf@LV~JLLj%O3C70Z zd}oAuCuPhej;XUg#xj}{o!*$`3&a>jN?txqK;)6fSd^s+j(U%@C6#xd;G0jU6BVtN z8`KoJC>a%sj9Ah8=@;A(*|aeeHdZhj^WaGqVeG{X-?lG=h$_C`F4mf!uI6@QxcE_P zUB0tVHSkIs`b)nSjlCxM{B9tx(Rz>TxLp|`iwBu8Sd&z`g>Pwy>D$# z>9Iq57~m@GdoTL9e`q3KW=xZLmLphATy(NsR4e2z~%R}%$N_TqA@aypju9GG`XhfXjD+531) zB9X_oU?%Eo5Ou0-h_7kL78;EGx&MuWb&!%N$DVlkbnZf;cEkhhms%%t%62%-DcD3+yx#|31VWR93TVUw^RWtrwt;{R^_T zM}ooE$^pUP?iN9t%J0X8Fs@nJEmn7VsC<&c*LZ{=)XFTaeTaTml_Af@;EHu^NrGhM zCwBAZ;hyAa9aI$k{;y!3Lvs%$W_8CRsmG8j&oC069Bc7+EVhrc2ZN5ce!*p@_3o=%7Jo+n=%XLZBQ}*XpQ;4V#hL2NC#JkvZn%b4JVSbnL(mGO| zJp`TLE1AU}E2qMgWj7k=3S}$(S{<{<7i4yO^QLpt&n*H!Y5aM1^bb`W$&mv0G!RZB zIR#2w1xLW#?^Ur_@%2e@H&-6RMHD8Kl`A!Zu<@vnu|FXrNs@2fT;Z#%Mc(aA+)lR?VN1@HH zW|Ao2f}^U39WG8rF=xtxI4%7cSKYl zTN=FfMYWsF*OH$1qTC)^$gcvslN^N~Dka=DsLv>Pif3c+a<_VB9V#b}%yMFE;Kw;H z9^VSH~2@skSj6zxuvyQ>WJ6}!5l9W#_6Tw>-PUwwUX+CeH z@#+u!I1L+AK8+*7iz^TIjY`z8S=Q%7IY(#6y`xYm^8OpBV#hwiwX4~J65LF=pACTV zMkk$qE&Z2!kL)jMm@mS6LZ$*RvOqffWsuZKd}z`a?+sS_I{05XyU(-epWi9fTfY1- z?9x2WZ_DO})|or$W7+*WqR>;W(*8L+gYVJW<3 zWvl)zV%YP8n;P!0(&W{FDC|ibRF#J(6NRG-@fQ+U{vKL{eqSBoxpbr&icNzPO_VK^ zZTm+P+>@y5O~zH@-*5Y0phfgkf3G3u&$c`lahBs~J;o#szHM&L1jSBeYxCbrGy*bt zx-AR-NC@9ABRgQY+{0_3CJr@?<5`7&n2ln!{S)a`+gdmBM{@cLfm}2v?G9_s-9I#X zgI)65KPt`R{50j%O!48|zrQj%#kz#sVrAd0iSP&`h1!lR?eM#>*5JUoYwE^%jBdgi zA8e05+bv3QEjsf|`P-W>52-)$hoCt61mPXI2_I7^=$l%vRav!e1Wh3CipOWx9E(smX* zED6U0C2=aYWA*5crF;{i>RfW@QGB?HZCh;u%nDxE_~JmEOi0ViwDfZ;v~&+ugrz^o z#u0MQatm>I@sn;p&))b&;TDo+1jd^Qava8S}ELOZwcy1#1Vbu%`ZJAi84sr0}*^ZcAvP1;pRl z^CZt}jV{*$(<{@1RBX=XVtZzUeRkqmI?mDe2Z3T?RsSh~BTZq*ht?o6fh%PN*B6FYxijq46NEJA{sMsJlbpU2Oix` z(Ob=0++z=}t!i{tzD*zIx;7yN? zE|gbnjKuwUD+Ki`aid;J&breic0CXIbqu>B;$nm65OC-j#D#Y*KWG7q*o?*;ap7is zFc%*bb`FaI*G6%OnRM0^nBHU=nqCq}hFn|X_xNLGp;(wcGF_&_J6b52i_w`$IPW}3 zr3#7ZC2MhNvL&`vG>P3%Y^y%9TBd8n!xH9V9A;A7R5&o16o2rHzX`&#vr&l5ybcnf z^A>N);*#wsOrjAIh{r1ADbfn=&zD(GS50){>~t+}r^I{y$*%m!{&>G_4#4tDDd?^znr>H<2y9m=n)s&wV^X8jdZ@M?Vs3Oh&{JFPlueZ;>{d>M= z%#*AVrn?gr8dw-rI20TiIGH>I9GVz}Sa!%s{`v81>&xQjmzgampO(?8&&m;PS|Jjq zq~U#L5?5e|qmH5Xj71r~yS(_U!>U+AyCRi$&kEAc*dpV`m)T@>V&=&+s;&X*md25m zt*oU}j3-ZwUg4Rp^5O5#kMHllmz#gcP^q{2gg>v!(@W{bDUpT#hWTG3o}~QEUveon z_>8cDQW~$nY2~u0lV$GPWUiTBH{jkB#$T*(ExTH4ew3qV`ApfvDl0zEO4q(-ctLB$ zD)$Y^fh?h?5|3Y5W}C2if$f+1lBXjsX=_BidcOU6;>PP2M3|vIn*j0b1U-#ix>Yrm z@?zD!XPeyQ98VrI67w}=aJ|s6oPW)XEnKUn9O+3F+R|>9BFns$Jv2nnx-D|TS3U#w zguP|ApC|EnhT5L1tw=nw_<821?vLO6%ULU#-QLw~os%h5vg`4C2kpN7E*$cY-t3zx zA3LjMcFFUtfo^%4_mgB*Q=d-xW_sTjiw1HWe?}yE^dho=EfdZIjlmQA^X= z%g1_wyR;$5(ffgTe~DWM4fezNL1 literal 0 HcmV?d00001 diff --git a/customer-portal-full/client/public/icons/icon-96x96.png b/customer-portal-full/client/public/icons/icon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..45483c277b6a55dc6a90050c00c82148bad74752 GIT binary patch literal 733 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U|Qno;uumf=k46xxzdgzuG8C! zTf6QwEVv-5QJ7HrsOnCqh(;kR-_1s@iHjL|nzo#*oL}6&M_tV#|NTt!-;edm%fIu> z&p!CWMZDm31Vaz&0h~n5GH#3M$L06y|N8Uq`SnF!MK@Q*_3bnH@PDC}i6ndiPp3YwXW3TAx;Fc?M3rfC)x6Ze8`?gQl_W;+!^xmnO zj*q6Lr2lv_Qz)Zmufr%S8?_zk%&>HD+T|mshTKEO;|JgUAjCL`+WLWxvJ;YbY z`eC%1O2RT$(e(cb%eK8RdcxO`DZ0SsH~U@FUCco7n8RnM=_o~RDRl_VejmSYd42kG zqbI5y$Cwgy7!X8SC1d~j^(CB&)5JGe;J58l|5H%!H0&~ToZNnupHdN6_F_U zYTZRy24NwC&EGW35BlU@tX*i$z$Uoh25X3KBI9P=6)K6_ru8@%mT(45aJW7}Nx?|? z1cOp?`rHn$H!Yl;U(U!c;p{lqan4~;a%xWxwSD^BFjckW%Dp0`ZHrpg&ANTAC4AC_ z$n4ikJ497Vu6OLU41defIZNioQH!N{3fUh|?@m-<;ra8cO7RanU(yeKA0JMk?JD__ i+zlHUBybTk?7t+xa&BN)`w5tA89ZJ6T-G@yGywqpOgysy literal 0 HcmV?d00001 diff --git a/customer-portal-full/client/public/manifest.json b/customer-portal-full/client/public/manifest.json new file mode 100644 index 000000000..a13e1d387 --- /dev/null +++ b/customer-portal-full/client/public/manifest.json @@ -0,0 +1,85 @@ +{ + "name": "Unified Insurance Platform", + "short_name": "InsurePlatform", + "description": "End-to-end unified insurance management platform for all stakeholders", + "start_url": "/", + "display": "standalone", + "background_color": "#0f172a", + "theme_color": "#3b82f6", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable any" + } + ], + "categories": ["finance", "business", "productivity"], + "screenshots": [], + "shortcuts": [ + { + "name": "Dashboard", + "short_name": "Dashboard", + "description": "View your insurance dashboard", + "url": "/dashboard", + "icons": [{ "src": "/icons/icon-96x96.png", "sizes": "96x96" }] + }, + { + "name": "Claims", + "short_name": "Claims", + "description": "Manage insurance claims", + "url": "/claims", + "icons": [{ "src": "/icons/icon-96x96.png", "sizes": "96x96" }] + }, + { + "name": "Policies", + "short_name": "Policies", + "description": "View your policies", + "url": "/policies", + "icons": [{ "src": "/icons/icon-96x96.png", "sizes": "96x96" }] + } + ] +} diff --git a/customer-portal-full/client/public/offline.html b/customer-portal-full/client/public/offline.html new file mode 100644 index 000000000..aebe89fa7 --- /dev/null +++ b/customer-portal-full/client/public/offline.html @@ -0,0 +1,45 @@ + + + + + + Offline - Unified Insurance Platform + + + +
+
🛡️
+

You're Offline

+

The Unified Insurance Platform requires an internet connection. Please check your network and try again.

+ +
+ + diff --git a/customer-portal-full/client/public/sw.js b/customer-portal-full/client/public/sw.js new file mode 100644 index 000000000..bc83c12f7 --- /dev/null +++ b/customer-portal-full/client/public/sw.js @@ -0,0 +1,114 @@ +// Unified Insurance Platform - Service Worker +const CACHE_NAME = 'uip-v1'; +const OFFLINE_URL = '/offline.html'; + +const PRECACHE_ASSETS = [ + '/', + '/manifest.json', + '/icons/icon-192x192.png', + '/icons/icon-512x512.png', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(PRECACHE_ASSETS); + }).then(() => self.skipWaiting()) + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames + .filter((name) => name !== CACHE_NAME) + .map((name) => caches.delete(name)) + ); + }).then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // Skip non-GET requests and API calls (always network-first for APIs) + if (request.method !== 'GET') return; + if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/trpc/')) return; + + // For navigation requests, use network-first with cache fallback + if (request.mode === 'navigate') { + event.respondWith( + fetch(request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); + return response; + }) + .catch(() => caches.match('/') || caches.match(OFFLINE_URL)) + ); + return; + } + + // For static assets, use cache-first strategy + event.respondWith( + caches.match(request).then((cached) => { + if (cached) return cached; + return fetch(request).then((response) => { + if (response.ok && response.type === 'basic') { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); + } + return response; + }); + }) + ); +}); + +// Background sync for offline form submissions +self.addEventListener('sync', (event) => { + if (event.tag === 'sync-claims') { + event.waitUntil(syncPendingClaims()); + } + if (event.tag === 'sync-payments') { + event.waitUntil(syncPendingPayments()); + } +}); + +async function syncPendingClaims() { + // Sync any offline-queued claims when connectivity is restored + const clients = await self.clients.matchAll(); + clients.forEach((client) => client.postMessage({ type: 'SYNC_CLAIMS' })); +} + +async function syncPendingPayments() { + const clients = await self.clients.matchAll(); + clients.forEach((client) => client.postMessage({ type: 'SYNC_PAYMENTS' })); +} + +// Push notifications +self.addEventListener('push', (event) => { + if (!event.data) return; + const data = event.data.json(); + event.waitUntil( + self.registration.showNotification(data.title || 'Insurance Platform', { + body: data.body || 'You have a new notification', + icon: '/icons/icon-192x192.png', + badge: '/icons/icon-96x96.png', + data: { url: data.url || '/' }, + actions: [ + { action: 'view', title: 'View' }, + { action: 'dismiss', title: 'Dismiss' }, + ], + }) + ); +}); + +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + if (event.action === 'view' || !event.action) { + const url = event.notification.data?.url || '/'; + event.waitUntil(clients.openWindow(url)); + } +}); diff --git a/customer-portal-full/client/src/App.tsx b/customer-portal-full/client/src/App.tsx new file mode 100644 index 000000000..ec72f4557 --- /dev/null +++ b/customer-portal-full/client/src/App.tsx @@ -0,0 +1,731 @@ +import { Toaster } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import NotFound from "@/pages/NotFound"; +import { Route, Switch, Link } from "wouter"; +import { Button } from "@/components/ui/button"; +import { Shield } from "lucide-react"; +import ErrorBoundary from "./components/ErrorBoundary"; +import { ThemeProvider } from "./contexts/ThemeContext"; +import { RoleProvider } from "./contexts/RoleContext"; +import UnifiedLayout from "./components/UnifiedLayout"; +import Home from "./pages/Home"; +import Dashboard from "./pages/Dashboard"; +import Policies from "./pages/Policies"; +import Claims from "./pages/Claims"; +import Payments from "./pages/Payments"; +import Profile from "./pages/Profile"; +import Referrals from "./pages/Referrals"; +import Reviews from "./pages/Reviews"; +import KYCStatus from "./pages/KYCStatus"; +import BlockchainStatus from "./pages/BlockchainStatus"; +import FraudAlerts from "./pages/FraudAlerts"; +import Analytics from "./pages/Analytics"; +import Communication from "./pages/Communication"; +import UserManagement from "./pages/UserManagement"; +import SystemSettings from "./pages/SystemSettings"; +import RiskAssessment from "./pages/RiskAssessment"; +import PolicyApproval from "./pages/PolicyApproval"; +import CustomerManagement from "./pages/CustomerManagement"; +import Commission from "./pages/Commission"; +import AuditLogs from "./pages/AuditLogs"; +import InsuranceProducts from "./pages/InsuranceProducts"; +import InsuranceApplication from "./pages/InsuranceApplication"; +import MyApplications from "./pages/MyApplications"; +import Auth from "./pages/Auth"; +import AIAdvisor from "./pages/AIAdvisor"; +import AIClaimsAdjudication from "./pages/AIClaimsAdjudication"; +import DynamicPricing from "./pages/DynamicPricing"; +import ComplianceMonitoring from "./pages/ComplianceMonitoring"; +import Onboarding from "./pages/Onboarding"; +import PolicyComparison from "./pages/PolicyComparison"; +import FamilyPolicies from "./pages/FamilyPolicies"; +import WhatsAppIntegration from "./pages/WhatsAppIntegration"; +import DocumentScanner from "./pages/DocumentScanner"; +import ExecutiveDashboard from "./pages/ExecutiveDashboard"; +import Telematics from "./pages/Telematics"; +import GeospatialMap from "./pages/GeospatialMap"; +import AdminPolicyCreation from "./pages/AdminPolicyCreation"; +import AgriculturalUnderwriting from "./pages/AgriculturalUnderwriting"; +import BrokerAPIManagement from "./pages/BrokerAPIManagement"; +import Gamification from "./pages/Gamification"; +import TwoFactorAuth from "./pages/TwoFactorAuth"; +import InsuranceMarketplace from "./pages/InsuranceMarketplace"; +import Chatbot from "./pages/Chatbot"; +import ReferralProgram from "./pages/ReferralProgram"; +import AgentPerformance from "./pages/AgentPerformance"; +import KnowledgeGraphExplorer from "./pages/KnowledgeGraphExplorer"; +import AIKnowledgeAssistant from "./pages/AIKnowledgeAssistant"; +import FraudNetworkVisualization from "./pages/FraudNetworkVisualization"; +import MCMCRiskModeling from "./pages/MCMCRiskModeling"; +import VoiceAssistant from "./pages/VoiceAssistant"; +import ChurnPrediction from "./pages/ChurnPrediction"; +import LoyaltyProgram from "./pages/LoyaltyProgram"; +import InsuranceLiteracyHub from "./pages/InsuranceLiteracyHub"; +import SmartClaimRouting from "./pages/SmartClaimRouting"; +import ProductRecommendationQuiz from "./pages/ProductRecommendationQuiz"; +import PremiumCalculator from "./pages/PremiumCalculator"; +import InsuranceScore from "./pages/InsuranceScore"; +import ClaimsTimeline from "./pages/ClaimsTimeline"; +import EmergencySOS from "./pages/EmergencySOS"; +import DigitalWallet from "./pages/DigitalWallet"; +import PremiumRateManagement from "./pages/PremiumRateManagement"; +import ERPNextIntegration from "./pages/ERPNextIntegration"; +import TelcoCreditScoring from "./pages/TelcoCreditScoring"; +import Microinsurance from "./pages/Microinsurance"; +import ModelSecurityDashboard from "./pages/ModelSecurityDashboard"; +import ClaimsEvidence from "./pages/ClaimsEvidence"; +import PolicyRenewal from "./pages/PolicyRenewal"; +import FamilyCoverage from "./pages/FamilyCoverage"; +import ClaimsTracker from "./pages/ClaimsTracker"; +import HealthWellness from "./pages/HealthWellness"; +import EmbeddedInsurance from "./pages/EmbeddedInsurance"; +import SavingsInvestment from "./pages/SavingsInvestment"; +import P2PInsurance from "./pages/P2PInsurance"; +import ParametricInsurance from "./pages/ParametricInsurance"; +import Bancassurance from "./pages/Bancassurance"; +import GigEconomy from "./pages/GigEconomy"; +import SMEBusiness from "./pages/SMEBusiness"; +import LoyaltyRewards from "./pages/LoyaltyRewards"; +import FinancialWellness from "./pages/FinancialWellness"; +import ReinsuranceManagement from "./pages/ReinsuranceManagement"; +import OperationalReports from "./pages/OperationalReports"; +import NAICOMCompliance from "./pages/NAICOMCompliance"; +import AuditTrailSystem from "./pages/AuditTrailSystem"; +import ClaimsAdjudicationEngine from "./pages/ClaimsAdjudicationEngine"; +import PolicyRenewalAutomation from "./pages/PolicyRenewalAutomation"; +import AgentCommissionManagement from "./pages/AgentCommissionManagement"; +import BatchProcessingEngine from "./pages/BatchProcessingEngine"; +import Customer360View from "./pages/Customer360View"; +import DocumentManagementSystem from "./pages/DocumentManagementSystem"; +import CustomerFeedbackLoop from "./pages/CustomerFeedbackLoop"; +import MultiCurrencySupport from "./pages/MultiCurrencySupport"; +import NigerianBankIntegrations from "./pages/NigerianBankIntegrations"; +import ReconciliationEngine from "./pages/ReconciliationEngine"; +import DisasterRecoveryModule from "./pages/DisasterRecoveryModule"; +import ABTestingFramework from "./pages/ABTestingFramework"; +import PerformanceMonitoringDashboard from "./pages/PerformanceMonitoringDashboard"; +import InsuranceRadar from "./pages/InsuranceRadar"; +import PostgreSQLScaling from "./pages/PostgreSQLScaling"; +import USSDGateway from "./pages/USSDGateway"; +import NMIDIntegration from "./pages/NMIDIntegration"; +import ActuarialModule from "./pages/ActuarialModule"; +import AgentPortal from "./pages/AgentPortal"; +import BancassurancePortal from "./pages/BancassurancePortal"; +import GroupLifeAdmin from "./pages/GroupLifeAdmin"; +import PFAIntegration from "./pages/PFAIntegration"; +import AgriculturalInsuranceSuite from "./pages/AgriculturalInsuranceSuite"; +import EmbeddedDistributionPlatform from "./pages/EmbeddedDistributionPlatform"; +import DigitalConsumerProducts from "./pages/DigitalConsumerProducts"; +import TakafulProductsSuite from "./pages/TakafulProductsSuite"; +import NIIRACompulsoryInsurance from "./pages/NIIRACompulsoryInsurance"; +import InsuranceTechInnovations from "./pages/InsuranceTechInnovations"; + +function Router() { + return ( + + + + {/* Public routes - accessible without login */} + +
+ +
+ +
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} + +function App() { + return ( + + + + + + + + + + + ); +} + +export default App; diff --git a/customer-portal-full/client/src/_core/hooks/useAuth.ts b/customer-portal-full/client/src/_core/hooks/useAuth.ts new file mode 100644 index 000000000..dcef9bd84 --- /dev/null +++ b/customer-portal-full/client/src/_core/hooks/useAuth.ts @@ -0,0 +1,84 @@ +import { getLoginUrl } from "@/const"; +import { trpc } from "@/lib/trpc"; +import { TRPCClientError } from "@trpc/client"; +import { useCallback, useEffect, useMemo } from "react"; + +type UseAuthOptions = { + redirectOnUnauthenticated?: boolean; + redirectPath?: string; +}; + +export function useAuth(options?: UseAuthOptions) { + const { redirectOnUnauthenticated = false, redirectPath = getLoginUrl() } = + options ?? {}; + const utils = trpc.useUtils(); + + const meQuery = trpc.auth.me.useQuery(undefined, { + retry: false, + refetchOnWindowFocus: false, + }); + + const logoutMutation = trpc.auth.logout.useMutation({ + onSuccess: () => { + utils.auth.me.setData(undefined, null); + }, + }); + + const logout = useCallback(async () => { + try { + await logoutMutation.mutateAsync(); + } catch (error: unknown) { + if ( + error instanceof TRPCClientError && + error.data?.code === "UNAUTHORIZED" + ) { + return; + } + throw error; + } finally { + utils.auth.me.setData(undefined, null); + await utils.auth.me.invalidate(); + } + }, [logoutMutation, utils]); + + const state = useMemo(() => { + localStorage.setItem( + "manus-runtime-user-info", + JSON.stringify(meQuery.data) + ); + return { + user: meQuery.data ?? null, + loading: meQuery.isLoading || logoutMutation.isPending, + error: meQuery.error ?? logoutMutation.error ?? null, + isAuthenticated: Boolean(meQuery.data), + }; + }, [ + meQuery.data, + meQuery.error, + meQuery.isLoading, + logoutMutation.error, + logoutMutation.isPending, + ]); + + useEffect(() => { + if (!redirectOnUnauthenticated) return; + if (meQuery.isLoading || logoutMutation.isPending) return; + if (state.user) return; + if (typeof window === "undefined") return; + if (window.location.pathname === redirectPath) return; + + window.location.href = redirectPath + }, [ + redirectOnUnauthenticated, + redirectPath, + logoutMutation.isPending, + meQuery.isLoading, + state.user, + ]); + + return { + ...state, + refresh: () => meQuery.refetch(), + logout, + }; +} diff --git a/customer-portal-full/client/src/components/AIChatBox.tsx b/customer-portal-full/client/src/components/AIChatBox.tsx new file mode 100644 index 000000000..1c00871fc --- /dev/null +++ b/customer-portal-full/client/src/components/AIChatBox.tsx @@ -0,0 +1,335 @@ +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import { Loader2, Send, User, Sparkles } from "lucide-react"; +import { useState, useEffect, useRef } from "react"; +import { Streamdown } from "streamdown"; + +/** + * Message type matching server-side LLM Message interface + */ +export type Message = { + role: "system" | "user" | "assistant"; + content: string; +}; + +export type AIChatBoxProps = { + /** + * Messages array to display in the chat. + * Should match the format used by invokeLLM on the server. + */ + messages: Message[]; + + /** + * Callback when user sends a message. + * Typically you'll call a tRPC mutation here to invoke the LLM. + */ + onSendMessage: (content: string) => void; + + /** + * Whether the AI is currently generating a response + */ + isLoading?: boolean; + + /** + * Placeholder text for the input field + */ + placeholder?: string; + + /** + * Custom className for the container + */ + className?: string; + + /** + * Height of the chat box (default: 600px) + */ + height?: string | number; + + /** + * Empty state message to display when no messages + */ + emptyStateMessage?: string; + + /** + * Suggested prompts to display in empty state + * Click to send directly + */ + suggestedPrompts?: string[]; +}; + +/** + * A ready-to-use AI chat box component that integrates with the LLM system. + * + * Features: + * - Matches server-side Message interface for seamless integration + * - Markdown rendering with Streamdown + * - Auto-scrolls to latest message + * - Loading states + * - Uses global theme colors from index.css + * + * @example + * ```tsx + * const ChatPage = () => { + * const [messages, setMessages] = useState([ + * { role: "system", content: "You are a helpful assistant." } + * ]); + * + * const chatMutation = trpc.ai.chat.useMutation({ + * onSuccess: (response) => { + * // Assuming your tRPC endpoint returns the AI response as a string + * setMessages(prev => [...prev, { + * role: "assistant", + * content: response + * }]); + * }, + * onError: (error) => { + * console.error("Chat error:", error); + * // Optionally show error message to user + * } + * }); + * + * const handleSend = (content: string) => { + * const newMessages = [...messages, { role: "user", content }]; + * setMessages(newMessages); + * chatMutation.mutate({ messages: newMessages }); + * }; + * + * return ( + * + * ); + * }; + * ``` + */ +export function AIChatBox({ + messages, + onSendMessage, + isLoading = false, + placeholder = "Type your message...", + className, + height = "600px", + emptyStateMessage = "Start a conversation with AI", + suggestedPrompts, +}: AIChatBoxProps) { + const [input, setInput] = useState(""); + const scrollAreaRef = useRef(null); + const containerRef = useRef(null); + const inputAreaRef = useRef(null); + const textareaRef = useRef(null); + + // Filter out system messages + const displayMessages = messages.filter((msg) => msg.role !== "system"); + + // Calculate min-height for last assistant message to push user message to top + const [minHeightForLastMessage, setMinHeightForLastMessage] = useState(0); + + useEffect(() => { + if (containerRef.current && inputAreaRef.current) { + const containerHeight = containerRef.current.offsetHeight; + const inputHeight = inputAreaRef.current.offsetHeight; + const scrollAreaHeight = containerHeight - inputHeight; + + // Reserve space for: + // - padding (p-4 = 32px top+bottom) + // - user message: 40px (item height) + 16px (margin-top from space-y-4) = 56px + // Note: margin-bottom is not counted because it naturally pushes the assistant message down + const userMessageReservedHeight = 56; + const calculatedHeight = scrollAreaHeight - 32 - userMessageReservedHeight; + + setMinHeightForLastMessage(Math.max(0, calculatedHeight)); + } + }, []); + + // Scroll to bottom helper function with smooth animation + const scrollToBottom = () => { + const viewport = scrollAreaRef.current?.querySelector( + '[data-radix-scroll-area-viewport]' + ) as HTMLDivElement; + + if (viewport) { + requestAnimationFrame(() => { + viewport.scrollTo({ + top: viewport.scrollHeight, + behavior: 'smooth' + }); + }); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const trimmedInput = input.trim(); + if (!trimmedInput || isLoading) return; + + onSendMessage(trimmedInput); + setInput(""); + + // Scroll immediately after sending + scrollToBottom(); + + // Keep focus on input + textareaRef.current?.focus(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + return ( +
+ {/* Messages Area */} +
+ {displayMessages.length === 0 ? ( +
+
+
+ +

{emptyStateMessage}

+
+ + {suggestedPrompts && suggestedPrompts.length > 0 && ( +
+ {suggestedPrompts.map((prompt, index) => ( + + ))} +
+ )} +
+
+ ) : ( + +
+ {displayMessages.map((message, index) => { + // Apply min-height to last message only if NOT loading (when loading, the loading indicator gets it) + const isLastMessage = index === displayMessages.length - 1; + const shouldApplyMinHeight = + isLastMessage && !isLoading && minHeightForLastMessage > 0; + + return ( +
+ {message.role === "assistant" && ( +
+ +
+ )} + +
+ {message.role === "assistant" ? ( +
+ {message.content} +
+ ) : ( +

+ {message.content} +

+ )} +
+ + {message.role === "user" && ( +
+ +
+ )} +
+ ); + })} + + {isLoading && ( +
0 + ? { minHeight: `${minHeightForLastMessage}px` } + : undefined + } + > +
+ +
+
+ +
+
+ )} +
+
+ )} +
+ + {/* Input Area */} +
+