This README explains the CreditSea Assignment project in simple language and with clear examples. It covers what the system does, the technologies used, how each logic block is implemented (including the Business Rule Engine, or BRE), how the dashboards work, and how to run the project locally.
The goal is to teach a developer or reviewer how the app is structured, why design choices were made, and how to operate the main flows. This document uses plain language and many bullets and examples for clarity.
- Project summary
- Technologies used
- High-level architecture and data flow
- Business Rule Engine (BRE): what, why, and where it runs
- Server-side logic: models, services, controllers, and routes
- Client-side logic: pages, components, and dashboards
- Dashboard explanations (module by module)
- Sales
- Sanction
- Disbursement
- Collection
- Borrower
- Example flows (apply -> sanction -> loan -> disburse -> repay)
- Error handling and edge cases
- Best practices and architectural notes
- How to run locally (quick start)
CreditSea is a small loan processing app with two main parts:
- A server (API) built with TypeScript + Express with MongoDB for persistence.
- A client (Next.js + React + Tailwind) for user interfaces and dashboards.
The app supports a borrower creating an application, uploading documents, and several internal modules (Sales, Sanction, Disbursement, Collection) each having a dashboard where staff perform actions. The server contains a Business Rule Engine (BRE) that decides eligibility and status transitions.
This README explains the code and architecture so you can understand, extend, and run the project.
- Node.js and npm — runtime and package management.
- TypeScript — typed JavaScript for both server and client.
- Express — server framework for APIs.
- MongoDB (mongoose) — document database; models are defined with Mongoose.
- Next.js + React — client UI and routing.
- Tailwind CSS — utility-first CSS framework for styling.
- Multer — file upload handling on the server (salary slips).
- Zod — validation library for incoming application input.
Why these choices?
- TypeScript improves code clarity and prevents common runtime errors.
- Express is lightweight and fits the API-first approach.
- MongoDB is flexible for the domain model; mongoose keeps models and schemas simple.
- Next.js makes it easy to build a React app with routing and server-side capabilities if needed.
- The browser (client) requests pages and calls API endpoints on the server.
- Authentication is cookie-based; protected endpoints use middleware that attaches
authinformation to the request. - Borrowers create
Applicationdocuments on the server. TheApplicationhas astatus(ELIGIBLE, PENDING, APPROVED, REJECTED). - The BRE (Business Rule Engine) evaluates application data when created, setting
ELIGIBLEorREJECTED. - If a borrower uploads documents (salary slip), the application may move from
ELIGIBLEtoPENDINGfor human review. - Sanction users review
PENDINGapplications; on approval aLoandocument is created and theApplicationmoves toAPPROVED. - Disbursement users mark
Loandocuments as disbursed, moving the loan toACTIVE. - Collection users or borrowers make repayments; repayments update
Loan.outstandingBalanceand may close the loan.
Data model (simplified):
- User: { email, passwordHash, role }
- Application: { borrowerId, fullName, pan, dob, monthlySalary, amount, tenureDays, employmentMode, status, reason, salarySlipPath }
- Loan: { borrowerId, amount, tenureDays, interestRate, simpleInterest, outstandingBalance, status, sanctionedBy, disbursedBy }
- Repayment: { loanId, utr, amount, createdAt }
Status transitions (summary):
- Application: ELIGIBLE -> PENDING -> APPROVED or REJECTED
- Loan: PENDING -> SANCTIONED -> DISBURSED -> ACTIVE -> CLOSED
What is the BRE?
- The BRE is the collection of rules that determine whether an application is automatically rejected, considered eligible, or needs manual review.
Why use a BRE?
- Make eligibility decisions consistent and auditable.
- Allow complex business logic to be centralized and easy to change.
- Keep validation and business policy out of the UI so rules do not leak into many places.
Where is the BRE implemented in this project?
- The BRE is implemented on the server inside
server/src/services/applicationService.ts. - Example rules implemented:
- Age must be between 23 and 50.
- Monthly salary must be >= ₹25,000.
- Unemployed applicants are rejected.
Why on the server and not only in the client?
- Server-side enforcement is authoritative; clients can be tampered with.
- Keeping BRE on the server ensures rules apply no matter how the client behaves (web, API, or automated scripts).
- The client may run a small subset of the rules for better UX (e.g., form validation) but the server must be the source of truth.
Should components of BRE live on the client as well?
- Some client-side checks are helpful to provide immediate feedback (e.g., required fields, basic format checks). Those are not replacements for server BRE.
- The server BRE contains the canonical rules; small client-side rules duplicate behavior for UX only.
Summary recommendation:
- Keep authoritative rules on the server. Mirror lightweight checks on the client purely for UX.
- If you need more advanced BRE features (rule versions, toggles), consider a configuration-driven rules engine or storing rules in a JSON format that the server reads.
The server follows a layered architecture:
- Models: Mongoose models under
server/src/models(Application, Loan, Repayment, User). - Services: Business logic and data orchestration under
server/src/services(applicationService, loanService, etc.). BRE rules live here. - Controllers: HTTP handlers under
server/src/controllersthat validate auth, extract inputs, call services, and send responses. - Routes: Express route definitions under
server/src/routesthat map endpoints to controllers. - Middleware: Authentication and error handling under
server/src/middleware.
Why this separation?
- Controllers stay thin and focus on HTTP concerns (status codes, request/response).
- Services contain the actual logic that can be unit tested without HTTP.
- Models encapsulate data shape and persistence logic.
Important server flow examples
-
Create application (Borrower)
- Route:
POST /applications - Controller: parses and validates input with Zod, then calls
applicationService.createApplication. - Service: runs BRE rules, creates the
Applicationin DB, returns the created document.
- Route:
-
Sanction action (Sanction user)
- Route:
POST /loans/:loanId/sanction(the code expects an application id here — the naming is historical but behavior usesApplicationid) - Controller: authorizes role, reads
action(approve/reject), callsloanService.sanctionLoan. - Service: if approve → creates a
Loanand setsApplication.status = APPROVED; if reject → setsApplication.status = REJECTEDand saves reason.
- Route:
-
Disburse action (Disbursement user)
- Route:
POST /loans/:loanId/disburse - Service: set loan status to DISBURSED then ACTIVE, set
disbursedAtanddisbursedBy.
- Route:
-
Repayment
- Route:
POST /loans/:loanId/repay - Service: validates loan status, creates
Repaymentrecord, updatesLoan.outstandingBalance, mark loanCLOSEDif fully paid.
- Route:
Notes about error handling
- The server uses a global error handler middleware. Services throw errors; controllers forward them to the handler.
- Controllers should return meaningful HTTP status codes (400 for bad input, 401 for auth, 403 for forbidden).
Project structure (client):
client/pages— Next.js pages, including dashboards underclient/pages/dashboard.client/components— shared components likeAuthProvider.client/styles— Tailwind and global CSS.
How the client talks to the server
- API base URL is
process.env.NEXT_PUBLIC_API_URLwith a default ofhttp://localhost:4000. - Calls include
credentials: 'include'to allow cookie-based authentication.
Auth flow
- There's an
AuthProvidercomponent that storesuserandlogoutand provides it to pages via React context. - Dashboard pages check
user.roleto allow or deny access on the client side.
Why check roles on the client?
- This is for UX (hide/redirect unauthorized UI). The server also enforces role-based access with middleware. Never rely on client checks alone for security.
Major client pages and purpose
pages/dashboard/sales.tsx— shows leads for sales users.pages/dashboard/sanction.tsx— shows pending applications for sanction users; has Approve/Reject actions.pages/dashboard/disbursement.tsx— shows sanctioned loans for disbursement users; action to mark disbursed.pages/dashboard/collection.tsx— shows active loans for collection; actions to record repayments.pages/borrower/*— application creation and salary slip upload pages for borrowers.
Client responsibilities vs server responsibilities
- Client: UI, input validation (for UX), navigation, user feedback, and calling authenticated endpoints.
- Server: BRE, data integrity, authorization, business logic, and persistence.
Below each module is explained: what it shows, why it exists, and exactly how it works in the code.
-
What it shows:
- Leads: users who are borrowers but have not created an application.
-
Why it exists:
- Sales staff track potential borrowers and encourage them to apply.
-
How it does it (in code):
- The server endpoint
GET /loans/module/sales(controllerlistForModule) queriesUserandApplicationcollections. - It finds borrower users and then filters out those who already have an
Applicationdocument. - Client renders these leads and provides short actions like "Contact" or "Create application" (if implemented).
- The server endpoint
-
Example:
- The controller builds a set of borrower IDs that have applications, then returns other borrowers as
leads.
- The controller builds a set of borrower IDs that have applications, then returns other borrowers as
-
What it shows:
- Applications with
status: PENDINGthat require manual review.
- Applications with
-
Why it exists:
- Some applicants are automatically rejected or marked ELIGIBLE by the BRE. Others need human review because the system needs documents or verification.
-
How it does it (in code):
- Client calls
GET /loans/module/sanctionand the server returnsApplication.find({ status: 'PENDING' }). - Approve action: Client posts
POST /loans/:applicationId/sanctionwith{ action: 'approve' }. The server runsloanService.sanctionLoanwhich:- Validates application is
PENDING. - Sets
Application.status = APPROVED. - Creates a
Loandocument with computed interest and repayment amounts. - Returns both the
loanand the updatedapplication.
- Validates application is
- Reject action: Client posts
POST /loans/:applicationId/sanctionwith{ action: 'reject', reason }. The server setsApplication.status = REJECTEDand stores the reason.
- Client calls
-
Example UI flow (approve):
- Sanction user clicks Approve.
- Client sends request to sanction endpoint.
- Server creates
Loanand returns result. - Client refetches the pending list and the item disappears.
-
What it shows:
- Loans that have been sanctioned and are waiting to be released (status SANCTIONED).
-
Why it exists:
- To separate duties: sanction approves creditworthiness and terms; disbursement performs fund release and records payment details.
-
How it does it (in code):
- Client calls
GET /loans/module/disbursementto get loans withLoanStatus.SANCTIONED. - Disbursement user clicks Disburse which calls
POST /loans/:loanId/disburse. - Server marks loan
DISBURSEDandACTIVE, setsdisbursedByanddisbursedAt.
- Client calls
-
Notes:
- In production, disbursement would integrate with payment rails and more checks.
-
What it shows:
- Loans in
ACTIVEorDISBURSEDstate which may need repayments.
- Loans in
-
Why it exists:
- Collection staff track repayments, reconcile UTRs, and follow up on late payments.
-
How it does it (in code):
- Client calls
GET /loans/module/collectionto get loans with statusACTIVEorDISBURSED. - When recording payment, client calls
POST /loans/:loanId/repaywith{ utr, amount }. - Server creates a
Repaymentrecord and reducesLoan.outstandingBalance. If outstanding balance reaches zero, Loan becomesCLOSED.
- Client calls
-
Edge cases handled:
- Reject payments that exceed outstanding balance.
- Ensure unique UTRs per
Repayment(model-level constraint).
-
What it shows:
- Borrower can create an application and upload salary slips.
applications/mereturns the borrower's applications and their status.
-
Why it exists:
- Borrower-facing flow is needed to collect information and documents.
-
How it does it (in code):
POST /applicationscreates an application. Service runs BRE rules and sets initial status.POST /applications/:id/salary-slipis a file upload endpoint using Multer. It saves a file path on the application and may promoteELIGIBLE->PENDING.
-
Important UX note:
- Client shows
application.statusin the UI so borrowers can track progress.
- Client shows
I will explain one full example: from application submission to repayment, with snippets and expected server behavior.
- Borrower submits application
- Client request:
POST /applications
Content-Type: application/json
Cookie: session=...
{
"fullName": "Rina Sharma",
"pan": "ABCDE1234F",
"dob": "1990-04-12",
"monthlySalary": 30000,
"amount": 150000,
"tenureDays": 180,
"employmentMode": "Salaried"
}- Server behavior:
borrowerController.createApplicationvalidates with Zod.applicationService.createApplicationruns BRE:- Age is within 23-50, salary >= 25000, so status becomes
ELIGIBLE.
- Age is within 23-50, salary >= 25000, so status becomes
- Save
Applicationwithstatus: ELIGIBLE. - Return the created
applicationto client.
- Borrower uploads salary slip (optional but common)
- Client uses
POST /applications/:id/salary-slipwith multipart/form-data containing the file. - Server stores file under
uploads/and setsapplication.salarySlipPath. - Service moves
ELIGIBLE->PENDINGso human review is required.
- Sanction user approves the application
- Client (sanction dashboard) calls:
POST /loans/:applicationId/sanction
Content-Type: application/json
Cookie: session=...
{ "action": "approve" }- Server behavior:
- Validate role (Sanction or Admin).
- Ensure application status is
PENDING. - Set
Application.status = APPROVED. - Create
Loandocument with computed interest and total repayment fields. - Return
loanand updatedapplication.
- Disbursement user releases funds
- Client calls:
POST /loans/:loanId/disburse
Cookie: session=...- Server marks loan
DISBURSEDandACTIVE, timestampsdisbursedAt, and setsdisbursedBy.
- Repayment
- Client (collection dashboard or borrower) calls:
POST /loans/:loanId/repay
Content-Type: application/json
Cookie: session=...
{ "utr": "UTR12345678", "amount": 5000 }- Server validates loan is
ACTIVEorDISBURSED. - Creates
Repaymentrecord and adjusts outstanding balance. - If outstanding becomes 0 → loan
CLOSED.
-
The server returns clear status codes and messages for common problems:
- 400 Bad Request: invalid input or missing parameters.
- 401 Unauthorized: no valid auth information.
- 403 Forbidden: user role not allowed for the action.
- 500 Internal Server Error: unexpected failures.
-
Edge cases handled in code:
- Unique UTR enforcement for repayments.
- Rejecting sanction actions when application status is not
PENDING. - Rejecting disbursement if loan is not
SANCTIONED. - Prevent creating duplicate loans for the same application.
- Keep business rules on the server. Use the client only for UX safety checks.
- Keep controllers thin. Put logic in services so it can be tested independently of HTTP.
- Use enums for status values (TypeScript
enum), and reference them everywhere to avoid typos. - Use explicit status transitions and validate current state before moving to the next.
- Use
createdAt/updatedAttimestamps on records to help audit activity. - If the BRE grows, externalize rules to configuration files or a dedicated rule service. This allows rule changes without code changes.
- Log important actions (sanctioned by, disbursed by, repayment utr) for traceability.
Scaling suggestions
- If the project grows, split services into microservices (auth, loans, payments) and use an event queue for state transitions and auditing.
- Add an async worker for sending notifications, reconciling repayments, and heavy tasks like document OCR.
Prerequisites:
- Node.js (v16+ recommended)
- npm
- MongoDB running (local or remote)
Environment variables (create a .env file in the server folder):
MONGO_URI— MongoDB connection string. Example:mongodb://localhost:27017/creditseaJWT_SECRET— a string used to sign session tokens. Example:devsecretPORT— optional server port (default: 4000)
Steps to run server and client:
- Start MongoDB (if local):
# depending on your setup, for example:
mongod --dbpath /data/db- Start the server
cd server
npm install
npm run dev- Seed example users (optional) — there is a script in
server/scripts/seed.jsthat creates sample users like[email protected]and others. To run it:
cd server
node scripts/seed.js- Start the client
cd client
npm install
npm run dev- Open the app in the browser:
- Client at:
http://localhost:3000 - Server health:
http://localhost:4000/health
- Common test accounts (from seed):
- Sanction:
[email protected]/sanction123 - Use the signup page to create other roles, or run seed script.
Notes on development
- If you change server TypeScript files,
npm run devshould restart the server ifts-node-devor equivalent is configured. If you see no changes, restart manually. - The server serves uploaded files under
/uploadsand the client may link to them directly.
This README gives you a clear map of how the CreditSea Assignment project is built and why it is structured this way. It focuses on keeping business logic safely on the server (BRE), using simple and clear status transitions, and separating responsibilities between client and server.
If you want, I can also:
- Add a simple architecture diagram (ASCII or Mermaid) saved in this repo.
- Add an automated Postman collection or example curl commands for testing the main flows.
- Add unit tests for key services (BRE and sanction flow).
Tell me which of these you'd like next and I will implement it.