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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
395 changes: 79 additions & 316 deletions insureportal/.env.example

Large diffs are not rendered by default.

100 changes: 100 additions & 0 deletions insureportal/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Contributing to InsurePortal

## Getting Started

### Prerequisites
- Node.js 20+
- Go 1.21+
- Python 3.11+
- Docker & Docker Compose
- PostgreSQL 15+

### Setup
```bash
cd insureportal
npm install
cp .env.example .env # Edit with your local credentials
npm run db:push # Create database tables
npm run seed # Populate with demo data
npm run dev # Start development server
```

### Middleware (Docker Compose)
```bash
docker-compose -f infrastructure/docker-compose.staging.yml up -d
```
This starts: PostgreSQL, Redis, Kafka, Keycloak, Temporal, OpenSearch.

## Architecture

```
insureportal/
├── client/src/ # React frontend (430 pages)
│ ├── pages/ # Route-based page components
│ ├── components/ # Shared UI components
│ ├── hooks/ # Custom React hooks
│ └── store/ # Zustand state management
├── server/ # tRPC backend (449 routers)
│ ├── _core/ # Server bootstrap, tRPC setup
│ ├── routers/ # Domain-specific route handlers
│ ├── middleware/ # Auth, rate limiting
│ └── db.ts # Database procedures
├── services/ # Polyglot microservices
│ ├── */main.go # Go services
│ ├── */main.py # Python services
│ └── */src/ # TypeScript/Rust services
├── shared/ # Shared types, schema, utilities
├── drizzle/ # Database schema & migrations
├── infrastructure/ # Helm, K8s, Docker, monitoring
└── docs/ # Architecture, deployment, API docs
```

## Development Workflow

### Frontend
- Framework: React 19 + TypeScript + Tailwind CSS 4
- Routing: wouter
- State: Zustand + React Query (TanStack)
- API: tRPC client
- UI: Radix UI + shadcn/ui patterns

### Backend
- API: tRPC v11 on Express
- DB: PostgreSQL via Drizzle ORM
- Auth: Keycloak OpenID Connect
- Queue: Kafka (kafkajs)
- Cache: Redis (ioredis)
- Workflow: Temporal

### Microservices
- **Go services**: `cd services/<name> && go build ./...`
- **Python services**: `cd services/<name> && python main.py`
- **TypeScript services**: Import from `services/<name>/src/index.ts`

## Testing
```bash
npm test # Run all tests
npm run test:coverage # With coverage report
```

## Code Style
- TypeScript: Strict mode, no `any` (use `@ts-nocheck` only for legacy files)
- Go: Standard `go fmt` + `go vet`
- Python: PEP 8
- Commits: Conventional commits (`feat:`, `fix:`, `chore:`)

## NAICOM Compliance
All insurance features must comply with NAICOM regulations:
- Product codes: `NIC/<TYPE>/<YEAR>/<SEQ>` format
- Minimum capital requirements per license class
- Solvency margin calculations
- Quarterly return filing deadlines
- AML/CFT reporting thresholds (CBN ₦5M single / ₦10M cumulative)

## Environment Variables
See `.env.example` for all required variables. Key groups:
- `DATABASE_URL` — PostgreSQL connection
- `REDIS_URL` — Redis cache
- `KAFKA_BROKERS` — Kafka cluster
- `KEYCLOAK_*` — Auth configuration
- `SERVICE_DISCOVERY_HOST` — Microservice hostname (K8s DNS or localhost)
10 changes: 10 additions & 0 deletions insureportal/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "drizzle-kit";

export default defineConfig({
out: "./drizzle",
schema: "./shared/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
129 changes: 128 additions & 1 deletion insureportal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,136 @@
"build": "vite build && esbuild server/_core/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist",
"start": "NODE_ENV=production node dist/index.js",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"lint": "eslint client/src server --ext .ts,.tsx",
"typecheck": "tsc --noEmit",
"seed": "node server/seed-comprehensive.mjs",
"db:push": "drizzle-kit push"
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.693.0",
"@aws-sdk/s3-request-presigner": "^3.693.0",
"@hookform/resolvers": "^5.2.2",
"@opentelemetry/auto-instrumentations-node": "^0.75.0",
"@opentelemetry/exporter-prometheus": "^0.217.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
"@opentelemetry/resources": "^2.6.1",
"@opentelemetry/sdk-node": "^0.217.0",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.2",
"@trpc/client": "^11.6.0",
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
"axios": "^1.12.0",
"bcryptjs": "^3.0.3",
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"compression": "^1.8.1",
"cookie": "^1.0.2",
"date-fns": "^4.1.0",
"dotenv": "^17.2.2",
"drizzle-orm": "^0.44.7",
"embla-carousel-react": "^8.6.0",
"express": "^4.21.2",
"express-rate-limit": "^8.3.2",
"framer-motion": "^12.23.22",
"helmet": "^8.1.0",
"idb-keyval": "^6.2.2",
"input-otp": "^1.4.2",
"ioredis": "^5.10.1",
"jose": "6.1.0",
"kafkajs": "^2.2.4",
"leaflet": "^1.9.4",
"lucide-react": "^0.453.0",
"nanoid": "^5.1.5",
"next-themes": "^0.4.6",
"node-cron": "^4.2.1",
"nodemailer": "^8.0.4",
"openid-client": "^6.8.2",
"pdfkit": "^0.18.0",
"pg": "^8.20.0",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"prom-client": "^15.1.3",
"qrcode": "^1.5.4",
"qrcode.react": "^4.2.0",
"rate-limit-redis": "^4.3.1",
"react": "^19.2.1",
"react-chartjs-2": "^5.3.1",
"react-day-picker": "^9.11.1",
"react-dom": "^19.2.1",
"react-hook-form": "^7.64.0",
"react-leaflet": "^5.0.0",
"react-resizable-panels": "^3.0.6",
"recharts": "^2.15.2",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"sonner": "^2.0.7",
"stripe": "^22.0.2",
"superjson": "^1.13.3",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"web-push": "^3.6.7",
"wouter": "^3.3.5",
"yaml": "^2.9.0",
"zod": "^4.1.12",
"zustand": "^5.0.12"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.1.3",
"@types/bcryptjs": "^3.0.0",
"@types/compression": "^1.8.1",
"@types/express": "4.17.21",
"@types/leaflet": "^1.9.21",
"@types/node": "^24.7.0",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^7.0.11",
"@types/pg": "^8.20.0",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.1",
"@types/react-dom": "^19.2.1",
"@types/web-push": "^3.6.4",
"@vitejs/plugin-react": "^5.0.4",
"autoprefixer": "^10.4.20",
"drizzle-kit": "^0.31.4",
"esbuild": "^0.25.0",
"eslint": "^9.0.0",
"pino-pretty": "^13.1.3",
"postcss": "^8.4.47",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.14",
"tsx": "^4.19.1",
"tw-animate-css": "^1.4.0",
"typescript": "5.9.3",
"vite": "^7.1.7",
"vitest": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4"
}
}
73 changes: 73 additions & 0 deletions insureportal/server/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* OpenTelemetry Instrumentation for InsurePortal
* Must be imported BEFORE any other modules to ensure proper instrumentation.
*
* Usage: import './instrumentation' at the top of server/_core/index.ts
*
* Exports metrics and traces to:
* - Prometheus (metrics): http://localhost:9464/metrics
* - OTLP (traces): configured via OTEL_EXPORTER_OTLP_ENDPOINT
*/

import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";

const OTEL_ENABLED = process.env.OTEL_ENABLED !== "false";
const SERVICE_NAME = process.env.OTEL_SERVICE_NAME || "insureportal-api";
const SERVICE_VERSION = process.env.npm_package_version || "1.0.0";
const OTLP_ENDPOINT = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
const PROMETHEUS_PORT = parseInt(process.env.PROMETHEUS_METRICS_PORT || "9464", 10);

let sdk: NodeSDK | undefined;

if (OTEL_ENABLED) {
const resource = new Resource({
[ATTR_SERVICE_NAME]: SERVICE_NAME,
[ATTR_SERVICE_VERSION]: SERVICE_VERSION,
"deployment.environment": process.env.NODE_ENV || "development",
"service.namespace": "insureportal",
});

const prometheusExporter = new PrometheusExporter({
port: PROMETHEUS_PORT,
});

const traceExporter = new OTLPTraceExporter({
url: `${OTLP_ENDPOINT}/v1/traces`,
});

sdk = new NodeSDK({
resource,
metricReader: prometheusExporter,
traceExporter,
instrumentations: [
getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-http": {
ignoreIncomingPaths: ["/health", "/metrics", "/favicon.ico"],
},
"@opentelemetry/instrumentation-express": { enabled: true },
"@opentelemetry/instrumentation-pg": { enabled: true },
"@opentelemetry/instrumentation-ioredis": { enabled: true },
"@opentelemetry/instrumentation-dns": { enabled: false },
"@opentelemetry/instrumentation-fs": { enabled: false },
}),
],
});

sdk.start();
console.log(`[OTel] Instrumentation active — Prometheus :${PROMETHEUS_PORT}, traces → ${OTLP_ENDPOINT}`);
}

// Graceful shutdown
process.on("SIGTERM", () => {
sdk?.shutdown().then(
() => console.log("[OTel] Shutdown complete"),
(err) => console.error("[OTel] Shutdown error", err)
);
});

export { sdk };
10 changes: 6 additions & 4 deletions insureportal/server/routers/healthCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { router, publicProcedure } from "../_core/trpc";
import { getDb } from "../db";
import { TRPCError } from "@trpc/server";

const SERVICE_HOST = process.env.SERVICE_DISCOVERY_HOST || "localhost";

export const healthCheckRouter = router({
status: publicProcedure.query(async () => {
const checks: Record<
Expand Down Expand Up @@ -83,7 +85,7 @@ export const healthCheckRouter = router({
for (const svc of goServices) {
try {
const start = Date.now();
const resp = await fetch(`http://localhost:${svc.port}/health`, {
const resp = await fetch(`http://${SERVICE_HOST}:${svc.port}/health`, {
signal: AbortSignal.timeout(2000),
});
checks[`go:${svc.name}`] = resp.ok
Expand All @@ -104,7 +106,7 @@ export const healthCheckRouter = router({
for (const svc of pyServices) {
try {
const start = Date.now();
const resp = await fetch(`http://localhost:${svc.port}/health`, {
const resp = await fetch(`http://${SERVICE_HOST}:${svc.port}/health`, {
signal: AbortSignal.timeout(2000),
});
checks[`py:${svc.name}`] = resp.ok
Expand All @@ -123,7 +125,7 @@ export const healthCheckRouter = router({
for (const svc of rustServices) {
try {
const start = Date.now();
const resp = await fetch(`http://localhost:${svc.port}/health`, {
const resp = await fetch(`http://${SERVICE_HOST}:${svc.port}/health`, {
signal: AbortSignal.timeout(2000),
});
checks[`rust:${svc.name}`] = resp.ok
Expand Down Expand Up @@ -172,7 +174,7 @@ export const healthCheckRouter = router({
for (const svc of allServices) {
try {
const start = Date.now();
const resp = await fetch(`http://localhost:${svc.port}/health`, {
const resp = await fetch(`http://${SERVICE_HOST}:${svc.port}/health`, {
signal: AbortSignal.timeout(2000),
});
services.push({
Expand Down
Loading
Loading