A production-grade NestJS microservice for managing employee time-off requests with real-time balance synchronization and a mock HCM server.
This is a NestJS monorepo containing:
- time-off-monorepo: Main time-off service (port 3000)
- mock-hcm: Mock HCM server for testing (port 3001)
- common: Shared library with TypeORM entities
- NestJS (TypeScript)
- SQLite with TypeORM
- better-sqlite3 driver
- Jest for testing
- Axios for HTTP requests
- Create, approve, reject, and cancel time-off requests
- Real-time balance synchronization with HCM
- Batch balance synchronization
- Stale balance detection and auto-refresh
- Transaction-based balance deductions
- Race condition protection
- In-memory balance storage
- Deduct and restore balance operations
- Batch balance retrieval
- Anniversary bonus simulation
- Reset functionality for testing
npm installEnvironment variables are configured in .env:
PORT=3000
HCM_BASE_URL=http://localhost:3001
BALANCE_STALE_THRESHOLD_MINUTES=10
DB_PATH=./database.sqlite
SYNC_JOB_INTERVAL_MS=300000
npm run startnpm run start:mock-hcmnpm run start:dev-
POST /time-off/requests- Create a new time-off request{ "employeeId": "EMP001", "locationId": "LOC001", "requestedDays": 5.0 } -
GET /time-off/requests/:id- Get request by ID -
GET /time-off/requests?employeeId=X- List requests by employee -
PATCH /time-off/requests/:id/approve- Approve a request -
PATCH /time-off/requests/:id/reject- Reject a request -
PATCH /time-off/requests/:id/cancel- Cancel a request
-
GET /time-off/balances?employeeId=X&locationId=Y- Get cached balance -
POST /time-off/balances/sync/realtime- Sync single employee balance from HCM{ "employeeId": "EMP001", "locationId": "LOC001" } -
POST /time-off/balances/sync/batch- Batch sync all balances[ { "employeeId": "EMP001", "locationId": "LOC001", "availableDays": 15.0 } ]
GET /hcm/balances?employeeId=X&locationId=Y- Get balance from HCMPOST /hcm/balances/deduct- Deduct days from balancePOST /hcm/balances/restore- Restore days to balancePOST /hcm/balances/batch- Get all balancesPOST /hcm/simulate/anniversary- Add bonus daysPOST /hcm/reset- Reset HCM to initial state
- Race Condition Protection: Transactions ensure only one approval succeeds for concurrent requests
- Defensive Local Validation: Balance is always checked locally before calling HCM
- Stale Balance Detection: Balances older than 10 minutes trigger automatic refresh
- Batch Sync Idempotency: Running the same batch twice produces identical results
- Cancellation Logic:
- PENDING requests: No balance change, no HCM call
- APPROVED requests: HCM restore first, then local balance restore
- Status Transitions:
- Valid: PENDING→APPROVED, PENDING→REJECTED, PENDING→CANCELLED, APPROVED→CANCELLED
- All others return 409 Conflict
npm run testnpm run test -- --coveragenpm run test:watchThe test suite includes 23 comprehensive test cases covering:
- Unit tests (9 tests): Balance validation, status transitions, batch upsert, stale detection
- Integration tests (11 tests): Happy path, error handling, HCM integration, race conditions
- E2E tests (3 tests): Full lifecycle workflows
Current Coverage: 88.64% (exceeds 85% requirement)
The service uses SQLite with TypeORM. The database schema is automatically synchronized on startup.
- TimeOffBalance: Employee balance cache with sync timestamps
- TimeOffRequest: Time-off request records with status tracking
- SyncLog: Audit log for synchronization operations
npm run buildnpm run lintnpm run formattime-off-monorepo/
├── apps/
│ ├── time-off-monorepo/ # Main service
│ │ └── src/
│ │ ├── balances/ # Balance management module
│ │ ├── requests/ # Request management module
│ │ └── app.module.ts
│ └── mock-hcm/ # Mock HCM server
│ └── src/
├── libs/
│ └── common/ # Shared entities
│ └── src/
│ └── entities/
├── test/ # E2E tests
└── package.json
- The mock HCM server must be running for the main service to function properly
- All timestamps are stored in UTC
- Balance amounts use DECIMAL(6,2) precision
- The service uses transactions to ensure data consistency
- Stale balance threshold is configurable via environment variables