Skip to content

EspiraMarvin/loan-ms

Repository files navigation

Loan Management System

Covers

  1. Account Management
  2. Transaction Processing
  3. Loan Lifecycle Operations
  4. Reporting Queries

Setup

$ pnpm install

Set Env

Create a new .env.development file copying the contents from .env.example file

cp .env.example .env.development

Compile and run the project

# development
$ pnpm run start

# watch mode
$ pnpm run start:dev

# production mode
$ pnpm run start:prod

Run tests

# unit tests
$ pnpm run test

# e2e tests
$ pnpm run test:e2e

# test coverage
$ pnpm run test:cov

#User Flow

Step 1. Seed Data

seed accounts

curl -X POST http://localhost:3000/api/accounting/setup/seed-accounts

seed lenders

curl -X POST http://localhost:3000/api/lenders/setup/seed-lenders

seed lenders capital contributions

curl -X POST http://localhost:3000/api/lenders/setup/seed-contributions

Step 2. List Accounts seeded

Step 2.1 All Accounts

curl -X GET http://localhost:3000/api/accounting/accounts | jq .

Step 2.2 KES Accounts

Filter accounts with currency=KES, we will use account ids from the seeded accounts list to perform next operations

curl 'http://localhost:3000/api/accounting/accounts?currency=KES' | jq .

Step 3. Create a loan/borrower applies for a loan

Request:

POST http://localhost:3000/api/loans
Content-Type: application/json

Payload:

{
  "borrowerId": "BORROWER-001",
  "amount": 50000,
  "currency": "KES",
  "termDays": 30
}

Response:

{
  "id": "3bfcdd4a-c372-4f3b-9043-1b290476d71b",
  "borrowerId": "BORROWER-001",
  "amount": 50000,
  "currency": "KES",
  "status": "PENDING",
  "disbursedAt": "2026-01-11T13:52:07.637Z",
  "dueDate": "2026-02-10T13:52:07.637Z",
  "createdAt": "2026-01-11T13:52:07.656Z",
  "updatedAt": "2026-01-11T13:52:07.656Z"
}

** store the loan-id (3bfcdd4a-c372-4f3b-9043-1b290476d71b) for next steps

Step 4. Disburse loan (with an origination fee)

Request:

POST http://localhost:3000/api/loans/loan-uuid-12345/disburse
Content-Type: application/json

Payload:

{
  "cashAccountId": "uuid-cash-kes",
  "loansReceivableAccountId": "uuid-loans-kes",
  "feeReceivableAccountId": "uuid-fee-recv-kes",
  "feeIncomeAccountId": "uuid-fee-inc-kes",
  "feeAmount": 2500
}

Note: Replace uuid-* with actual IDs from Step 2.2

Response:

{
  "id": "3bfcdd4a-c372-4f3b-9043-1b290476d71b",
  "borrowerId": "BORROWER-001",
  "amount": "50000.00",
  "currency": "KES",
  "status": "DISBURSED",
  "disbursedAt": "2026-01-11T16:39:48.688Z",
  "dueDate": "2026-02-10T13:52:07.637Z",
  "createdAt": "2026-01-11T13:52:07.656Z",
  "updatedAt": "2026-01-11T16:39:48.690Z"
}

Step 5. Loan repayment

Request:

POST http://localhost:3000/api/loans/loan-uuid-12345/repay
Content-Type: application/json

Payload:

{
  "principalAmount": 50000,
  "interestAmount": 3500,
  "cashAccountId": "uuid-cash-kes",
  "loansReceivableAccountId": "uuid-loans-kes",
  "interestIncomeAccountId": "uuid-int-inc-kes"
}

Response:

{
  "loanId": "loan-uuid-12345",
  "borrowerId": "BORROWER-001",
  "principalPaid": 50000,
  "interestPaid": 3500,
  "totalRepayment": 53500,
  "status": "CLOSED"
}

Step 6. Create & write off a defaulted loan

Step 6.1 Create second loan

Request:

POST http://localhost:3000/api/loans
Content-Type: application/json

Payload:

{
  "borrowerId": "BORROWER-002",
  "amount": 30000,
  "currency": "KES",
  "termDays": 30
}

Response:

{
  "id": "4bfcdd4a-c372-4f3b-9043-1b290476d71e",
  "borrowerId": "BORROWER-002",
  "amount": 30000,
  "currency": "KES",
  "status": "PENDING",
  "disbursedAt": "2026-01-11T13:52:07.637Z",
  "dueDate": "2026-02-10T13:52:07.637Z",
  "createdAt": "2026-01-11T13:52:07.656Z",
  "updatedAt": "2026-01-11T13:52:07.656Z"
}

save the loan id to be used to disburse loan, the next step

Step 6.2 Disburse second loan

Request:

POST http://localhost:3000/api/loans/loan-uuid-67890/disburse
Content-Type: application/json

Payload:

{
  "cashAccountId": "uuid-cash-kes",
  "loansReceivableAccountId": "uuid-loans-kes"
}

Response:

{
{
	"id": "12fcdd4a-c372-4f3b-9043-1b290476d71b",
	"borrowerId": "BORROWER-002",
	"amount": "30000.00",
	"currency": "KES",
	"status": "DISBURSED",
	"disbursedAt": "2026-01-11T16:39:48.688Z",
	"dueDate": "2026-02-10T13:52:07.637Z",
	"createdAt": "2026-01-11T13:52:07.656Z",
	"updatedAt": "2026-01-11T16:39:48.690Z"
}
}

Step 6.3 Loan writeoff

Request:

POST http://localhost:3000/api/loans/loan-uuid-67890/writeoff
Content-Type: application/json

Payload:

{
  "loansReceivableAccountId": "uuid-loans-kes",
  "badDebtExpenseAccountId": "uuid-bad-debt-exp-kes"
}

Response:

{
  "message": "Loan written off successfully",
  "loanId": "loan-uuid-67890",
  "writeOffAmount": "30000.00",
  "updatedStatus": "DEFAULTED"
}

Step 7 Reporting

Step 7.1 Trial balance

Request:

GET http://localhost:3000/api/accounting/reports/trial-balance

Step 7.2 Balance sheet

Request:

GET http://localhost:3000/api/accounting/reports/balance-sheet

Step 7.3 Transaction History per account (CASH-KES)

Request:

GET http://localhost:3000/api/accounting/reports/transaction-history/uuid-cash-kes?page=1&limit=10

Step 7.4 Loan aging report

Request:

GET http://localhost:3000/api/loans/reports/aging

Step 8 Transaction reversal

Step 8.1 Create transaction

Request:

POST http://localhost:3000/api/accounting/transactions
Content-Type: application/json

Payload:

{
  "idempotencyKey": "manual-adjustment-001",
  "description": "Manual adjustment - test reversal",
  "type": "ADJUSTMENT",
  "entries": [
    {
      "accountId": "uuid-cash-kes",
      "debit": 5000,
      "credit": 0
    },
    {
      "accountId": "uuid-retained-kes",
      "debit": 0,
      "credit": 5000
    }
  ]
}

Response:

{
  "id": "journal-uuid-abc123",
  "idempotencyKey": "manual-adjustment-001",
  "description": "Manual adjustment - test reversal",
  "status": "POSTED",
  "type": "ADJUSTMENT",
  "currency": "KES",
  "entries": [...],
  "version": 1,
  "postedAt": "2026-01-11T13:00:00.000Z",
  "createdAt": "2026-01-11T13:00:00.000Z"
}

** Store the journal-uuid-abc123 for reversal

Step 8.2 Reverse transaction

Request:

POST http://localhost:3000/api/accounting/transactions/journal-uuid-abc123/reverse
Content-Type: application/json

Payload:

{
  "reason": "Administrative correction - wrong amount",
  "expectedVersion": 1
}

Response:

{
  "originalTransaction": {
    "id": "journal-uuid-abc123",
    "status": "REVERSED",
    "version": 2,
    "description": "Manual adjustment - test reversal"
  },
  "reversalTransaction": {
    "id": "journal-uuid-xyz789",
    "idempotencyKey": "reversal-journal-uuid-abc123",
    "description": "REVERSAL: Manual adjustment- wrong amount)",
    "status": "POSTED",
    "type": "ADJUSTMENT",
    "entries": [
      {
        "accountId": "uuid-cash-kes",
        "debit": 0,
        "credit": 5000
      },
      {
        "accountId": "uuid-retained-kes",
        "debit": 5000,
        "credit": 0
      }
    ],
    "reversalOf": {
      "id": "journal-uuid-abc123"
    },
    "postedAt": "2026-01-11T13:05:00.000Z"
  }
}

Entities

  1. Account ↔ Account: may have or lack parent id (to support this hierarchy: Assets → Current Assets → Cash) - Self-referential parentid

  2. Journal → LedgerEntry: One-to-Many (each journal entry contains ledger lines)

  3. LedgerEntry → Account: Many-to-One (each ledger line posts to one account)

  4. Journal ↔ Journal: Self-referential for transaction reversals (reversalOf points to original journal)

  5. Lender: Lender capital contributions are tracked through Journal entries with type CAPITAL_CONTRIBUTION

  6. Loan: Loan operations (disbursement, repayment, write-off) are tracked through Journal entries

Entity Descriptions

Entity Purpose Key Features
Account Chart of Accounts Supports 5 types (ASSET, LIABILITY, EQUITY, INCOME, EXPENSE), multi-currency, hierarchical structure
Journal Transaction header Idempotency, versioning, reversal tracking, multi-type support
LedgerEntry Individual debit/credit lines Ensures double-entry bookkeeping, linked to accounts and journals
Lender Capital providers Tracks lender identity, contributions via journal entries
Loan Borrower loans Tracks loan lifecycle (PENDING → DISBURSED → CLOSED/DEFAULTED)

About

loan management system

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors