Skip to content

Latest commit

 

History

History
836 lines (696 loc) · 24.9 KB

File metadata and controls

836 lines (696 loc) · 24.9 KB

MicroFinance Lending Platform — Full System Design

For Cline: This document is a complete system design spec. Build the project exactly as described. Follow the folder structure, tech stack, API contracts, and database schema precisely. Do not substitute libraries unless explicitly noted.


1. Project Overview

A full-stack micro-finance lending management system for a small-city financier who lends money for automobiles, small shops, personal loans, etc.

Goals

  • Admin can manage customers, approve loans, track EMIs, record repayments
  • Customers can apply for loans, view EMI schedules, download receipts
  • Works on Web (React) + Mobile (React Native)
  • Deployed entirely on free-tier services
  • Secure, robust, production-ready

2. Tech Stack

Frontend — Web

Layer Technology
Framework React 18 + Vite
Styling TailwindCSS v3
State / Data React Query (TanStack Query v5)
Routing React Router v6
Forms React Hook Form + Zod
PDF react-pdf / jsPDF
Charts Recharts
HTTP Client Axios

Frontend — Mobile

Layer Technology
Framework React Native (Expo SDK 51+)
Routing expo-router v3
Styling NativeWind v4 (TailwindCSS for RN)
State / Data TanStack Query v5
Forms React Hook Form + Zod
Notifications expo-notifications
Storage expo-secure-store (tokens)
Documents expo-document-picker, expo-image-picker

Backend

Layer Technology
Runtime Node.js 20 LTS
Framework Express.js v4
Auth JWT (jsonwebtoken) + bcrypt
Validation Zod
ORM Prisma v5
Jobs node-cron
PDF Gen pdfkit
Email Nodemailer + Brevo SMTP
File Upload Multer + Cloudinary SDK
Logging Morgan + Winston
Security Helmet, cors, express-rate-limit

Database & Infrastructure

Service Purpose Free Tier
Supabase PostgreSQL database 500MB DB, 1GB storage
Upstash Redis cache + rate limiting 10,000 cmds/day
Cloudinary KYC document/image storage 25GB
Render.com Backend API hosting 512MB RAM
Vercel Web frontend hosting Unlimited
Expo EAS Mobile app builds 30 builds/month
Brevo Transactional email 300 emails/day
GitHub Actions CI/CD 2000 min/month

3. Repository Structure

microfinance-platform/
├── apps/
│   ├── web/                        # React + Vite web app
│   │   ├── src/
│   │   │   ├── api/                # Axios API client + endpoints
│   │   │   ├── components/         # Shared UI components
│   │   │   ├── features/
│   │   │   │   ├── auth/
│   │   │   │   ├── dashboard/
│   │   │   │   ├── loans/
│   │   │   │   ├── customers/
│   │   │   │   ├── repayments/
│   │   │   │   └── reports/
│   │   │   ├── hooks/              # Custom React hooks
│   │   │   ├── layouts/            # AdminLayout, CustomerLayout
│   │   │   ├── pages/              # Page-level components
│   │   │   ├── stores/             # Zustand stores (auth, ui)
│   │   │   ├── utils/              # EMI calc, formatters, etc.
│   │   │   └── main.tsx
│   │   ├── index.html
│   │   ├── vite.config.ts
│   │   └── tailwind.config.ts
│   │
│   └── mobile/                     # Expo React Native app
│       ├── app/                    # expo-router file-based routing
│       │   ├── (auth)/
│       │   │   ├── login.tsx
│       │   │   ├── register.tsx
│       │   │   └── otp-verify.tsx
│       │   ├── (customer)/
│       │   │   ├── _layout.tsx     # Tab layout
│       │   │   ├── index.tsx       # Home/Dashboard
│       │   │   ├── loans/
│       │   │   │   ├── index.tsx
│       │   │   │   ├── apply.tsx
│       │   │   │   └── [id].tsx
│       │   │   ├── emi/
│       │   │   │   └── [loanId].tsx
│       │   │   ├── profile.tsx
│       │   │   └── notifications.tsx
│       │   └── _layout.tsx         # Root layout
│       ├── components/
│       ├── hooks/
│       ├── utils/
│       ├── app.json
│       └── tailwind.config.ts
│
├── packages/
│   └── shared/                     # Shared types, Zod schemas, utils
│       ├── src/
│       │   ├── types/              # TypeScript interfaces
│       │   ├── schemas/            # Zod validation schemas
│       │   └── utils/              # EMI calculator, date helpers
│       └── package.json
│
├── server/                         # Express.js backend
│   ├── src/
│   │   ├── config/
│   │   │   ├── db.ts               # Prisma client singleton
│   │   │   ├── redis.ts            # Upstash Redis client
│   │   │   ├── cloudinary.ts
│   │   │   └── env.ts              # Zod env validation
│   │   ├── middleware/
│   │   │   ├── auth.middleware.ts  # JWT verify
│   │   │   ├── role.middleware.ts  # RBAC guard
│   │   │   ├── validate.middleware.ts
│   │   │   ├── rateLimit.middleware.ts
│   │   │   └── error.middleware.ts
│   │   ├── modules/
│   │   │   ├── auth/
│   │   │   │   ├── auth.router.ts
│   │   │   │   ├── auth.controller.ts
│   │   │   │   └── auth.service.ts
│   │   │   ├── customers/
│   │   │   │   ├── customers.router.ts
│   │   │   │   ├── customers.controller.ts
│   │   │   │   └── customers.service.ts
│   │   │   ├── loans/
│   │   │   │   ├── loans.router.ts
│   │   │   │   ├── loans.controller.ts
│   │   │   │   └── loans.service.ts
│   │   │   ├── emi/
│   │   │   │   ├── emi.router.ts
│   │   │   │   ├── emi.controller.ts
│   │   │   │   └── emi.service.ts
│   │   │   ├── repayments/
│   │   │   │   ├── repayments.router.ts
│   │   │   │   ├── repayments.controller.ts
│   │   │   │   └── repayments.service.ts
│   │   │   ├── documents/
│   │   │   └── reports/
│   │   ├── jobs/
│   │   │   ├── emi-reminder.job.ts
│   │   │   ├── overdue-check.job.ts
│   │   │   └── index.ts
│   │   ├── utils/
│   │   │   ├── emi-calculator.ts
│   │   │   ├── pdf-generator.ts
│   │   │   ├── mailer.ts
│   │   │   └── logger.ts
│   │   └── app.ts
│   ├── prisma/
│   │   ├── schema.prisma
│   │   └── migrations/
│   └── package.json
│
├── .github/
│   └── workflows/
│       ├── deploy-server.yml
│       └── deploy-web.yml
├── package.json                    # Monorepo root (npm workspaces)
└── README.md

4. Database Schema (Prisma)

File: server/prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

enum Role {
  ADMIN
  CUSTOMER
}

enum KycStatus {
  PENDING
  VERIFIED
  REJECTED
}

enum LoanStatus {
  PENDING
  APPROVED
  ACTIVE
  CLOSED
  REJECTED
}

enum LoanType {
  AUTOMOBILE
  SMALL_BUSINESS
  PERSONAL
  HOME
  CONSUMER_ELECTRONICS
  AGRICULTURE
  EDUCATION
  MEDICAL
  EQUIPMENT
}

enum EmiStatus {
  PENDING
  PAID
  OVERDUE
}

enum PaymentMode {
  CASH
  UPI
  BANK_TRANSFER
}

model User {
  id            String      @id @default(uuid())
  name          String
  phone         String      @unique
  email         String?     @unique
  passwordHash  String
  role          Role        @default(CUSTOMER)
  isActive      Boolean     @default(true)
  kycStatus     KycStatus   @default(PENDING)
  createdAt     DateTime    @default(now())
  updatedAt     DateTime    @updatedAt

  customer      Customer?
  refreshTokens RefreshToken[]
}

model Customer {
  id              String    @id @default(uuid())
  userId          String    @unique
  user            User      @relation(fields: [userId], references: [id])

  address         String
  city            String
  pincode         String
  state           String

  aadharNo        String?   @unique
  panNo           String?   @unique
  dateOfBirth     DateTime?
  occupation      String?
  monthlyIncome   Float?

  guarantorName   String?
  guarantorPhone  String?
  guarantorRelation String?

  profilePhotoUrl String?
  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt

  loans           Loan[]
  documents       Document[]
}

model Loan {
  id              String      @id @default(uuid())
  customerId      String
  customer        Customer    @relation(fields: [customerId], references: [id])

  loanType        LoanType
  principalAmount Float
  interestRate    Float       // annual percentage
  tenureMonths    Int
  processingFee   Float       @default(0)
  purpose         String?
  collateral      String?

  status          LoanStatus  @default(PENDING)
  approvedBy      String?     // admin user id
  approvedAt      DateTime?
  disbursedAt     DateTime?
  closedAt        DateTime?
  rejectionReason String?

  totalAmount     Float       // principal + total interest
  emiAmount       Float

  createdAt       DateTime    @default(now())
  updatedAt       DateTime    @updatedAt

  emiSchedule     EmiSchedule[]
}

model EmiSchedule {
  id                  String      @id @default(uuid())
  loanId              String
  loan                Loan        @relation(fields: [loanId], references: [id])

  emiNumber           Int
  dueDate             DateTime
  emiAmount           Float
  principalComponent  Float
  interestComponent   Float
  outstandingBalance  Float
  latePenalty         Float       @default(0)

  status              EmiStatus   @default(PENDING)
  createdAt           DateTime    @default(now())

  repayment           Repayment?
}

model Repayment {
  id            String      @id @default(uuid())
  emiId         String      @unique
  emi           EmiSchedule @relation(fields: [emiId], references: [id])

  paidAmount    Float
  paidDate      DateTime    @default(now())
  paymentMode   PaymentMode @default(CASH)
  receiptNo     String      @unique
  collectedBy   String      // admin user id
  notes         String?
  latePenalty   Float       @default(0)

  createdAt     DateTime    @default(now())
}

model Document {
  id            String    @id @default(uuid())
  customerId    String
  customer      Customer  @relation(fields: [customerId], references: [id])

  docType       String    // AADHAR_FRONT, AADHAR_BACK, PAN, PHOTO, INCOME_PROOF, etc.
  fileUrl       String
  publicId      String    // Cloudinary public_id
  verifiedBy    String?
  verifiedAt    DateTime?
  isVerified    Boolean   @default(false)

  createdAt     DateTime  @default(now())
}

model RefreshToken {
  id        String   @id @default(uuid())
  userId    String
  user      User     @relation(fields: [userId], references: [id])
  token     String   @unique
  expiresAt DateTime
  createdAt DateTime @default(now())
}

5. API Endpoints

Base URL

https://your-app.onrender.com/api/v1

Authentication

POST   /auth/register              # Customer self-register
POST   /auth/login                 # Login (phone + password)
POST   /auth/otp/send              # Send OTP to phone
POST   /auth/otp/verify            # Verify OTP → issue token
POST   /auth/refresh               # Refresh access token
POST   /auth/logout                # Invalidate refresh token
GET    /auth/me                    # Get current user

Customers (Admin only unless noted)

GET    /customers                  # List all customers (admin)
POST   /customers                  # Create customer profile (admin)
GET    /customers/:id              # Get customer details
PUT    /customers/:id              # Update customer info
GET    /customers/:id/loans        # All loans for a customer
GET    /customers/me               # Own profile (customer)
PUT    /customers/me               # Update own profile (customer)

Loans

POST   /loans/apply                # Apply for loan (customer)
GET    /loans                      # List loans (admin: all, customer: own)
GET    /loans/:id                  # Loan details
PUT    /loans/:id/approve          # Approve loan (admin)
PUT    /loans/:id/reject           # Reject loan (admin)
PUT    /loans/:id/disburse         # Mark as disbursed (admin)
GET    /loans/pending              # Pending approvals (admin)
GET    /loans/overdue              # Loans with overdue EMIs (admin)

EMI

GET    /emi/schedule/:loanId       # Full EMI schedule for a loan
GET    /emi/upcoming               # Upcoming EMIs (customer: own, admin: all)
GET    /emi/overdue                # All overdue EMIs (admin)
GET    /emi/summary/:customerId    # EMI summary for customer (admin)

Repayments

POST   /repayments                 # Record repayment (admin)
GET    /repayments/:loanId         # Repayment history for loan
GET    /repayments/receipt/:id     # Get receipt data
GET    /repayments/receipt/:id/pdf # Download PDF receipt

Documents

POST   /documents/upload           # Upload KYC document
GET    /documents/:customerId      # List customer documents
PUT    /documents/:id/verify       # Verify document (admin)
DELETE /documents/:id              # Delete document

Reports (Admin only)

GET    /reports/dashboard          # KPI stats for dashboard
GET    /reports/monthly?month=&year= # Monthly repayment report
GET    /reports/customers          # Customer portfolio summary
GET    /reports/overdue            # Overdue analysis
GET    /reports/monthly/pdf        # Download monthly report PDF

6. Environment Variables

File: server/.env (never commit — add to Render dashboard)

# App
NODE_ENV=production
PORT=5000
FRONTEND_URL=https://your-app.vercel.app

# Database (Supabase)
DATABASE_URL=postgresql://postgres:[password]@db.[ref].supabase.co:5432/postgres

# JWT
JWT_ACCESS_SECRET=your_access_secret_min_32_chars
JWT_REFRESH_SECRET=your_refresh_secret_min_32_chars
JWT_ACCESS_EXPIRY=15m
JWT_REFRESH_EXPIRY=7d

# Redis (Upstash)
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=your_token

# Cloudinary
CLOUDINARY_CLOUD_NAME=your_cloud
CLOUDINARY_API_KEY=your_key
CLOUDINARY_API_SECRET=your_secret

# Email (Brevo SMTP)
SMTP_HOST=smtp-relay.brevo.com
SMTP_PORT=587
SMTP_USER=your_brevo_login
SMTP_PASS=your_brevo_smtp_key
EMAIL_FROM=noreply@yourfinance.com

# Admin
ADMIN_PHONE=9999999999
ADMIN_DEFAULT_PASSWORD=ChangeMe@123

File: apps/web/.env

VITE_API_URL=https://your-app.onrender.com/api/v1

File: apps/mobile/.env

EXPO_PUBLIC_API_URL=https://your-app.onrender.com/api/v1

7. EMI Calculator Logic

File: packages/shared/src/utils/emi-calculator.ts

Implement reducing balance (flat interest optional via flag):

export interface EmiInput {
  principal: number;
  annualInterestRate: number; // e.g. 18 for 18%
  tenureMonths: number;
  method?: 'reducing' | 'flat';
}

export interface EmiScheduleRow {
  emiNumber: number;
  dueDate: Date;
  emiAmount: number;
  principalComponent: number;
  interestComponent: number;
  outstandingBalance: number;
}

export function calculateEmi(input: EmiInput): number {
  // Reducing balance formula: EMI = P * r * (1+r)^n / ((1+r)^n - 1)
  const r = input.annualInterestRate / 12 / 100;
  const n = input.tenureMonths;
  const P = input.principal;
  if (input.method === 'flat') {
    const totalInterest = P * (input.annualInterestRate / 100) * (n / 12);
    return Math.round((P + totalInterest) / n * 100) / 100;
  }
  const emi = P * r * Math.pow(1 + r, n) / (Math.pow(1 + r, n) - 1);
  return Math.round(emi * 100) / 100;
}

export function generateEmiSchedule(
  input: EmiInput,
  startDate: Date
): EmiScheduleRow[] {
  const emiAmount = calculateEmi(input);
  const r = input.annualInterestRate / 12 / 100;
  const schedule: EmiScheduleRow[] = [];
  let balance = input.principal;

  for (let i = 1; i <= input.tenureMonths; i++) {
    const interest = Math.round(balance * r * 100) / 100;
    const principal = Math.round((emiAmount - interest) * 100) / 100;
    balance = Math.round((balance - principal) * 100) / 100;

    const dueDate = new Date(startDate);
    dueDate.setMonth(dueDate.getMonth() + i);

    schedule.push({
      emiNumber: i,
      dueDate,
      emiAmount,
      principalComponent: principal,
      interestComponent: interest,
      outstandingBalance: Math.max(balance, 0),
    });
  }
  return schedule;
}

8. Auth Middleware

File: server/src/middleware/auth.middleware.ts

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { env } from '../config/env';

export interface AuthRequest extends Request {
  user?: { id: string; role: string };
}

export const authenticate = (req: AuthRequest, res: Response, next: NextFunction) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ message: 'No token provided' });

  try {
    const decoded = jwt.verify(token, env.JWT_ACCESS_SECRET) as { id: string; role: string };
    req.user = decoded;
    next();
  } catch {
    return res.status(401).json({ message: 'Invalid or expired token' });
  }
};

export const requireRole = (...roles: string[]) =>
  (req: AuthRequest, res: Response, next: NextFunction) => {
    if (!req.user || !roles.includes(req.user.role)) {
      return res.status(403).json({ message: 'Insufficient permissions' });
    }
    next();
  };

9. Cron Jobs

File: server/src/jobs/index.ts

import cron from 'node-cron';
import { sendEmiReminders } from './emi-reminder.job';
import { checkOverdueEmis } from './overdue-check.job';

export function initJobs() {
  // Daily 8:00 AM — send EMI due reminders (3 days before due)
  cron.schedule('0 8 * * *', sendEmiReminders);

  // Daily 9:00 AM — mark overdue EMIs and apply penalty
  cron.schedule('0 9 * * *', checkOverdueEmis);
}

checkOverdueEmis logic:

  • Query all EmiSchedule where status = PENDING and dueDate < today
  • Set status = OVERDUE
  • Apply late penalty (e.g., 2% of EMI amount per day capped at 10%)
  • Send email/notification to customer

10. Deployment Configuration

Render (Backend)

File: server/render.yaml

services:
  - type: web
    name: microfinance-api
    env: node
    region: singapore
    buildCommand: npm install && npx prisma generate && npx prisma migrate deploy
    startCommand: node dist/app.js
    envVars:
      - key: NODE_ENV
        value: production

Vercel (Web Frontend)

File: apps/web/vercel.json

{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}

GitHub Actions (CI/CD)

File: .github/workflows/deploy-server.yml

name: Deploy Server
on:
  push:
    branches: [main]
    paths: ['server/**']

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Render
        run: curl -X POST ${{ secrets.RENDER_DEPLOY_HOOK_URL }}

11. Web App Pages & Routes

/login                         → LoginPage
/register                      → CustomerRegisterPage

# Admin routes (role: ADMIN)
/admin                         → AdminDashboard
/admin/customers               → CustomerListPage
/admin/customers/:id           → CustomerDetailPage
/admin/loans                   → LoanListPage (with filters)
/admin/loans/pending           → PendingApprovalsPage
/admin/loans/:id               → LoanDetailPage
/admin/repayments              → RepaymentLogPage
/admin/repayments/record       → RecordRepaymentPage
/admin/reports                 → ReportsPage
/admin/settings                → SettingsPage

# Customer routes (role: CUSTOMER)
/dashboard                     → CustomerDashboard
/loans                         → MyLoansPage
/loans/apply                   → LoanApplicationPage
/loans/:id                     → LoanDetailPage
/loans/:id/emi                 → EmiSchedulePage
/profile                       → ProfilePage
/documents                     → DocumentsPage

12. Mobile App Screens (expo-router)

app/
├── (auth)/
│   ├── login.tsx              # Phone + password login
│   ├── register.tsx           # Multi-step registration
│   └── otp-verify.tsx         # OTP input screen
│
└── (customer)/                # Tab-based layout
    ├── index.tsx              # Home: EMI due, loan status cards
    ├── loans/
    │   ├── index.tsx          # My loans list
    │   ├── apply.tsx          # Loan application form
    │   └── [id].tsx           # Loan detail + EMI schedule
    ├── payments/
    │   └── index.tsx          # Payment history
    ├── profile.tsx            # Profile + KYC documents
    └── notifications.tsx      # In-app notifications

13. Key Components to Build

Shared (Web + Mobile)

  • EmiCalculatorWidget — input principal/rate/tenure → show EMI + schedule
  • LoanStatusBadge — color-coded: PENDING/ACTIVE/CLOSED/REJECTED
  • DocumentUploader — drag-drop (web) / picker (mobile) → upload to Cloudinary
  • EmiScheduleTable — show all EMIs with status indicators
  • OverdueAlert — banner showing total overdue amount

Web Only

  • AdminDashboard — KPI cards + charts (total disbursed, collected, overdue)
  • CustomerTable — searchable, filterable customer list
  • LoanApprovalCard — approve/reject with reason modal
  • RepaymentForm — record cash/UPI payment, auto-generate receipt
  • ReportExporter — monthly report → download PDF

Mobile Only

  • HomeScreen — next EMI card, outstanding balance, quick actions
  • LoanApplicationStepper — 4-step form with progress bar
  • EmiCalendarView — calendar with due dates highlighted

14. Security Checklist

Cline must implement all of the following:

  • All routes except /auth/* require valid JWT
  • Customers can only access their own data (filter by req.user.id)
  • File uploads: validate MIME type (images/PDF only), max 5MB
  • Rate limiting: 100 requests/15 min per IP on all routes
  • Stricter rate limit on /auth/login: 5 attempts/15 min
  • All passwords hashed with bcrypt (12 rounds)
  • Refresh tokens stored in DB, invalidated on logout
  • CORS whitelist: only allow known frontend origins
  • Helmet.js enabled with default config
  • All env variables validated with Zod at startup
  • Receipt numbers: auto-generated, non-guessable (UUID-based)
  • Admin seed script with default password force-change on first login

15. Build Order (Recommended for Cline)

Build in this sequence to avoid dependency issues:

  1. packages/shared — types, Zod schemas, EMI calculator utility
  2. server/prisma/schema.prisma — full schema + run migration
  3. server/src/config/ — db, redis, cloudinary, env config
  4. server/src/middleware/ — auth, role, validate, error handlers
  5. server/src/modules/auth/ — register, login, OTP, token refresh
  6. server/src/modules/customers/ — CRUD + KYC
  7. server/src/modules/loans/ — apply, approve, reject, disburse
  8. server/src/modules/emi/ — schedule generation, overdue detection
  9. server/src/modules/repayments/ — record payment, PDF receipt
  10. server/src/modules/reports/ — dashboard stats, monthly PDF
  11. server/src/jobs/ — cron jobs for reminders + overdue checks
  12. apps/web/ — auth → admin dashboard → loan management → reports
  13. apps/mobile/ — auth → home → loans → EMI view → profile

16. Seed Data Script

File: server/prisma/seed.ts

Create:

  • 1 admin user (phone from env, hashed password)
  • 2 sample customers with complete profiles
  • 1 active loan per customer with full EMI schedule
  • Sample repayment records

Run with: npx ts-node prisma/seed.ts


17. Free Tier Limits & Monitoring

Service Watch Out For
Render API sleeps after 15min inactivity → use UptimeRobot (free) to ping every 14min
Supabase 500MB DB limit → archive closed loans after 2 years
Cloudinary 25 credits/month → compress images before upload
Upstash 10k commands/day → cache aggressively, don't cache per-request
Brevo 300 emails/day → batch reminders, don't send per EMI individually

This document was generated for the Cline AI coding agent. All architectural decisions are final. Start with step 1 of the Build Order.