A complete leasing (hire-purchase) backend API built with Azure Functions v3, TypeScript, and Prisma.
Note: This project is a technical assessment and is not intended for production use.
- Backend: Azure Functions v3, Node.js 14+
- Language: TypeScript (strict mode)
- Database: Prisma ORM with SQLite/PostgreSQL
- Validation: Zod schema validation
- Architecture: Clean Architecture (Domain → Application → Infrastructure)
- Testing: Jest (unit + integration tests)
- Deployment: Azure Functions Core Tools
- Storage: Azurite (local Azure Storage emulation)
Clone and run immediately
git clone <repository-url>
cd leasing-api-azure-functions
npm test # Runs tests (auto-setup included)
npm start # Starts API (auto-setup included)# If you prefer to see the setup process
./script.init
./start-dev.sh# If you want to start manually after setup
npm run build
func start --script-root distImportant: Make sure you're using Node.js 14+ for Azure Functions v3:
nvm use 14 # Switch to Node.js 14
func start --script-root distThe API will be running on http://localhost:7071
What happens automatically:
- Installs dependencies if missing
- Sets up database and environment files
- Installs Azure Functions Core Tools if missing
- Builds the project
- Starts the API
- Clone repository
- Run
./script.init(creates.env.localandlocal.settings.json) - Edit
.env.localwith your secure API key - Run
./start-dev.shto start the API - Test health endpoint:
curl -H "x-api-key: YOUR_API_KEY" http://localhost:7071/api/health
For Azure-like local development with storage emulation:
# One-time setup
./scripts/setup-azurite.sh
# Start with Azure storage emulation
npm run dev:azure- Azure Storage Emulation: Blob, Queue, Table services
- Production-like Environment: Same APIs as Azure
- Free Development: No Azure subscription needed
- Offline Development: Works without internet
- Blob Storage:
http://localhost:10000 - Queue Storage:
http://localhost:10001 - Table Storage:
http://localhost:10002
# Regular development (SQLite)
npm start
# Azure-like development (Azurite + SQLite)
npm run dev:azure
# Stop Azurite
npm run dev:azure:stopAll endpoints require the API key header:
-H "x-api-key: YOUR_API_KEY"For Development:
- The setup script creates
.env.localfrom template - Update
.env.localwith your secure API key - Never commit
.env.localto version control
For Production:
- Use Azure Key Vault for secure secrets management
- Generate strong, unique API keys
- Implement key rotation policies
- Enable audit logging
# Generate a secure API key
openssl rand -hex 32
# Update your .env.local file
API_KEY=your-secure-api-key-hereAll endpoints require authentication via the x-api-key header.
-H "x-api-key: YOUR_API_KEY"| Method | Endpoint | Description | Parameters |
|---|---|---|---|
GET |
/api/health |
Health check and API status | None |
GET |
/api/quote |
Calculate lease quote | price, termMonths, nominalRatePct, startDate |
POST |
/api/leases |
Create new lease | Request body with lease details |
GET |
/api/leases/{id} |
Get specific lease | id (required UUID) |
POST |
/api/payments |
Record payment | Request body with payment details |
GET /api/health
Returns the API status and version information.
curl -H "x-api-key: YOUR_API_KEY" http://localhost:7071/api/healthResponse:
{
"status": "healthy",
"version": "1.0.0",
"timestamp": "2023-01-01T00:00:00Z"
}GET /api/quote
Calculates lease quote based on provided parameters.
Query Parameters:
price(number): Item pricetermMonths(number): Lease term in monthsnominalRatePct(number): Annual interest rate percentagestartDate(string): Lease start date (ISO 8601)
curl -H "x-api-key: YOUR_API_KEY" "http://localhost:7071/api/quote?price=1000&termMonths=6&nominalRatePct=12&startDate=2023-01-01"Response:
{
"monthlyPayment": 175.50,
"totalAmount": 1053.00,
"totalInterest": 53.00,
"paymentSchedule": [...]
}POST /api/leases
Creates a new lease agreement.
Request Body:
{
"companyId": "test-company",
"itemId": "test-item",
"price": 1000,
"termMonths": 6,
"nominalRatePct": 12,
"startDate": "2023-01-01",
"upfrontFee": 0,
"monthlyFee": 50
}curl -X POST -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"companyId":"test-company","itemId":"test-item","price":1000,"termMonths":6,"nominalRatePct":12,"startDate":"2023-01-01","upfrontFee":0,"monthlyFee":50}' \
http://localhost:7071/api/leasesResponse:
{
"id": "lease-uuid-here",
"companyId": "test-company",
"itemId": "test-item",
"price": 1000,
"termMonths": 6,
"nominalRatePct": 12,
"startDate": "2023-01-01",
"upfrontFee": 0,
"monthlyFee": 50,
"createdAt": "2023-01-01T00:00:00Z",
"schedule": [...],
"totals": {...}
}GET /api/leases/{id}
Retrieves lease information by ID.
Path Parameters:
id(required): Lease UUID
curl -H "x-api-key: YOUR_API_KEY" http://localhost:7071/api/leases/lease-uuid-hereResponse:
{
"id": "lease-uuid-here",
"companyId": "test-company",
"itemId": "test-item",
"price": 1000,
"termMonths": 6,
"nominalRatePct": 12,
"startDate": "2023-01-01",
"upfrontFee": 0,
"monthlyFee": 50,
"createdAt": "2023-01-01T00:00:00Z",
"schedule": [...],
"totals": {...}
}POST /api/payments
Records a payment against a lease.
Request Body:
{
"leaseId": "lease-uuid-here",
"amount": 200,
"paidAt": "2023-01-15T10:00:00Z"
}curl -X POST -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"leaseId":"lease-uuid-here","amount":200,"paidAt":"2023-01-15T10:00:00Z"}' \
http://localhost:7071/api/paymentsResponse:
{
"id": "payment-uuid-here",
"leaseId": "lease-uuid-here",
"amount": 200,
"paidAt": "2023-01-15T10:00:00Z"
}# 1. GET /api/quote - Calculate leasing quote (no persistence)
curl -H "x-api-key: YOUR_API_KEY" "http://localhost:7071/api/quote?price=1000&termMonths=6&nominalRatePct=12&startDate=2023-01-01"
# 2. POST /api/leases - Create new leasing contract for a company
curl -X POST -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"companyId":"test-company","itemId":"test-item","price":1000,"termMonths":6,"nominalRatePct":12,"startDate":"2023-01-01","upfrontFee":0,"monthlyFee":50}' \
http://localhost:7071/api/leases
# 3. GET /api/leases/{id} - Fetch lease details with remaining balance and payment schedule
curl -H "x-api-key: YOUR_API_KEY" http://localhost:7071/api/leases/LEASE_UUID_HERE
# 4. POST /api/payments - Register payment for existing lease, updating remaining balance
curl -X POST -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"leaseId":"LEASE_UUID_HERE","amount":200,"paidAt":"2023-01-15T10:00:00Z"}' \
http://localhost:7071/api/paymentsAll endpoints return appropriate HTTP status codes:
200- Success400- Bad Request (validation errors)401- Unauthorized (invalid API key)404- Not Found500- Internal Server Error
Error Response Format:
{
"error": "ValidationError",
"message": "Invalid input parameters",
"details": {...}
}After running ./script.init, you can use these commands:
# Start the API (after setup)
./start-dev.sh
# Additional development tools
npm run dev # Watch TypeScript compilation
npm test # Run all tests
npm run prisma:studio # Open database GUIThe project includes comprehensive test coverage across all layers:
- Domain Logic Tests: Core business calculations and validations
- Application Service Tests: Service layer logic with mocked dependencies
- Test Database: Separate SQLite database for isolated testing
- API Workflow Tests: Full end-to-end API workflows
- Database Integration: Real database operations and updates
- Cross-Service Integration: Lease creation + payment recording flows
- Monthly payment calculation using standard annuity formula
- Interest rate handling (0% and positive rates)
- Decimal precision and rounding validation
- Edge cases and boundary conditions
- Correct installment count generation
- Period numbering and due date calculations
- Fee allocation across installments
- Principal balance validation
- Allocation order: Fees → Interest → Principal
- Multi-installment payment distribution
- Partial payment handling
- Overpayment scenarios
- Already paid installment handling
- Complete lease creation workflow
- Payment recording and database updates
- Multiple payment scenarios
- Payment allocation across installments
- Error handling and edge cases
# Run all tests
npm test
# Run specific test categories
npm test -- tests/unit/domain/ # Domain logic tests
npm test -- tests/unit/application/ # Service layer tests
npm test -- tests/integration/ # Integration tests
# Run specific test files
npm test -- tests/unit/domain/lease.test.ts
npm test -- tests/unit/domain/payment.test.ts
npm test -- tests/integration/lease-api.test.ts
npm test -- tests/integration/payment-api.test.tstests/
├── unit/
│ ├── domain/
│ │ ├── lease.test.ts # Annuity & schedule tests
│ │ ├── payment.test.ts # Payment allocation tests
│ │ └── payment-simple.test.ts # Simplified payment tests
│ └── application/
│ ├── lease-service.test.ts # Lease service tests
│ └── payment-service.test.ts # Payment service tests
└── integration/
├── lease-api.test.ts # Lease API integration
└── payment-api.test.ts # Payment API integration
The setup script automatically handles Node.js 14 installation. If you encounter issues:
# Check Node.js version
node --version # Should show v14.x.x
# If using nvm
nvm use 14
# Then start manually
func start --script-root dist# Kill any processes using port 7071
lsof -ti:7071 | xargs kill -9# Rebuild the project
npm run build
./start-dev.sh# Reset database
rm -rf prisma/dev.db
npx prisma migrate dev --name initleasing-api-azure-functions/
├── 📁 functions/
│ ├── 📁 leases/
│ │ ├── 📄 function.json
│ │ └── 📄 index.ts
│ ├── 📁 payments/
│ │ ├── 📄 function.json
│ │ └── 📄 index.ts
│ ├── 📁 quote/
│ │ ├── 📄 function.json
│ │ └── 📄 index.ts
│ └── 📁 health-check/
│ ├── 📄 function.json
│ └── 📄 index.ts
├── 📁 src/
│ ├── 📁 application/
│ │ ├── 📄 lease-service.ts
│ │ ├── 📄 payment-service.ts
│ │ └── 📄 quote-service.ts
│ ├── 📁 domain/
│ │ ├── 📄 lease.ts
│ │ ├── 📄 payment.ts
│ │ └── 📄 quote.ts
│ ├── 📁 handlers/
│ │ ├── 📁 lease/
│ │ │ ├── 📄 create-lease.ts
│ │ │ └── 📄 get-lease.ts
│ │ ├── 📁 payment/
│ │ │ └── 📄 record-payment.ts
│ │ └── 📁 quote/
│ │ └── 📄 calculate-quote.ts
│ ├── 📁 lib/
│ │ ├── 📄 prisma.ts
│ │ ├── 📄 validation.ts
│ │ ├── 📄 api-key-middleware.ts
│ │ └── 📄 logger.ts
│ ├── 📁 persistence/
│ │ ├── 📄 lease-repository.ts
│ │ ├── 📄 payment-repository.ts
│ │ └── 📄 quote-repository.ts
│ └── 📄 index.ts
├── 📁 tests/
│ ├── 📁 unit/
│ │ ├── 📁 domain/
│ │ │ ├── 📄 lease.test.ts
│ │ │ └── 📄 payment.test.ts
│ │ └── 📁 application/
│ │ ├── 📄 lease-service.test.ts
│ │ └── 📄 payment-service.test.ts
│ └── 📁 integration/
│ ├── 📄 lease-api.test.ts
│ └── 📄 payment-api.test.ts
├── 📁 scripts/
│ ├── 📄 check-node-version.js
│ ├── 📄 setup-detector.js
│ ├── 📄 copy-function-configs.js
│ ├── 📄 setup-azurite.sh
│ └── 📄 deploy-azure-comprehensive.sh
├── 📄 package.json
├── 📄 tsconfig.json
├── 📄 host.json
└── 📄 README.md
- Monthly Payment: Standard annuity formula
- Payment Schedule: Generated for each lease term
- Payment Allocation: Fees → Interest → Principal
- Allocation Order: Fees first, then interest, then principal
- Multi-Installment: Payments can span multiple installments
- Overpayment: Handled gracefully
./start-dev.sh # Runs on http://localhost:7071# Deploy with comprehensive production setup
npm run azure:deploy:comprehensiveThe deployment script provides:
Security Features:
- All secrets stored in Azure Key Vault (API_KEY, JWT_SECRET, ENCRYPTION_KEY, DATABASE_URL)
- Managed Identity for secure resource access
- No hardcoded credentials in application code
- Production-grade security configuration
Manual Deployment:
# Custom deployment with specific parameters
./scripts/deploy-azure-comprehensive.sh <function-app-name> <resource-group> <location>
# Example:
./scripts/deploy-azure-comprehensive.sh my-leasing-api my-rg westeurope# Get API Key from Azure Key Vault
API_KEY=$(az keyvault secret show --vault-name <key-vault-name> --name API-KEY --query value --output tsv)
# Health Check
curl -H "x-api-key: $API_KEY" https://<function-app-name>.azurewebsites.net/api/health
# Get Quote
curl -H "x-api-key: $API_KEY" "https://<function-app-name>.azurewebsites.net/api/quote?price=1000&termMonths=6&nominalRatePct=12&startDate=2023-01-01"# View real-time logs
func azure functionapp logstream <function-app-name>
# View metrics in Azure Portal
# Navigate to: Azure Portal > Function App > Monitoring > MetricsThe API uses environment variables for configuration:
# .env.local (your secrets - never commit to git)
API_KEY=your-secure-api-key-here
DATABASE_URL=file:./prisma/dev.db
JWT_SECRET=your-jwt-secret-here
ENCRYPTION_KEY=your-encryption-key-here# Azure Key Vault or App Settings
API_KEY=managed-by-azure-key-vault
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=managed-by-azure-key-vault- Run
./script.init- Creates.env.localfrom template - Edit
.env.local- Add your secure values - Start development - Application loads from
.env.local
# 1. Setup creates template
./script.init
# 2. You edit with your secrets
nano .env.local
# Add: API_KEY=your-secure-key-here
# 3. Application reads from .env.local
./start-dev.shThe following commands have been tested and work perfectly:
# 1. One-command setup (does everything automatically)
./script.init
# 2. Start the API
./start-dev.sh
# 3. Test the API
curl -H "x-api-key: YOUR_API_KEY" http://localhost:7071/api/health
curl -H "x-api-key: YOUR_API_KEY" "http://localhost:7071/api/quote?price=1000&termMonths=6&nominalRatePct=12&startDate=2023-01-01"
# 4. Create a lease
curl -X POST -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"companyId":"test-company","itemId":"test-item","price":1000,"termMonths":6,"nominalRatePct":12,"startDate":"2023-01-01","upfrontFee":0,"monthlyFee":50}' \
http://localhost:7071/api/leases
# 5. Record a payment (use lease ID from above)
curl -X POST -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"leaseId":"LEASE_ID_HERE","amount":200,"paidAt":"2023-01-15T10:00:00Z"}' \
http://localhost:7071/api/payments- Upgrade to Node.js 16+ for the best experience with modern Azure Functions, Azurite and for better deployment.