Skip to content

Commit 57b04e7

Browse files
Merge pull request #276 from RUKAYAT-CODER/feat-procurement-module
Feat procurement module
2 parents 2045f83 + 4ad4d78 commit 57b04e7

16 files changed

+3446
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Asset Depreciation Module
2+
3+
This module implements asset depreciation functionality using straight-line depreciation method to help companies estimate the current value of their assets.
4+
5+
## Features
6+
7+
- **Straight-line Depreciation**: Calculates depreciation using the formula: `(Purchase Price - Salvage Value) / Useful Life Years`
8+
- **Automatic Calculation**: Current depreciated values are calculated automatically based on time elapsed
9+
- **Comprehensive API**: Full CRUD operations plus specialized endpoints for depreciation data
10+
- **Validation**: Input validation for purchase dates, salvage values, and useful life
11+
- **Filtering**: Advanced filtering options for retrieving assets by depreciation status, method, and value ranges
12+
13+
## API Endpoints
14+
15+
### Basic CRUD Operations
16+
17+
- `POST /asset-depreciation` - Create a new asset depreciation record
18+
- `GET /asset-depreciation` - Get all asset depreciation records with optional filters
19+
- `GET /asset-depreciation/:id` - Get a specific asset by ID
20+
- `PATCH /asset-depreciation/:id` - Update an asset depreciation record
21+
- `DELETE /asset-depreciation/:id` - Delete an asset depreciation record
22+
23+
### Specialized Depreciation Endpoints
24+
25+
- `GET /asset-depreciation/current-values` - Get current depreciated values for all assets
26+
- `GET /asset-depreciation/:id/current-value` - Get current depreciated value for a specific asset
27+
- `GET /asset-depreciation/summary` - Get depreciation summary statistics
28+
- `GET /asset-depreciation/fully-depreciated` - Get all fully depreciated assets
29+
- `GET /asset-depreciation/nearing-end-of-life?threshold=1` - Get assets nearing end of useful life
30+
- `GET /asset-depreciation/:id/projected-value?date=2025-12-31` - Get projected value at a future date
31+
32+
## Data Transfer Objects (DTOs)
33+
34+
### CreateAssetDepreciationDto
35+
```typescript
36+
{
37+
assetName: string; // Required, max 255 chars
38+
description?: string; // Optional description
39+
purchasePrice: number; // Required, positive number with max 2 decimal places
40+
purchaseDate: string; // Required, ISO date string (YYYY-MM-DD)
41+
usefulLifeYears: number; // Required, 1-100 years
42+
depreciationMethod?: DepreciationMethod; // Optional, defaults to STRAIGHT_LINE
43+
salvageValue?: number; // Optional, must be less than purchase price
44+
}
45+
```
46+
47+
### UpdateAssetDepreciationDto
48+
All fields are optional versions of CreateAssetDepreciationDto fields.
49+
50+
### DepreciatedValueResponseDto
51+
```typescript
52+
{
53+
id: number;
54+
assetName: string;
55+
description?: string;
56+
purchasePrice: number;
57+
purchaseDate: string;
58+
usefulLifeYears: number;
59+
depreciationMethod: DepreciationMethod;
60+
salvageValue?: number;
61+
currentDepreciatedValue: number; // Calculated field
62+
annualDepreciation: number; // Calculated field
63+
totalDepreciationToDate: number; // Calculated field
64+
remainingUsefulLife: number; // Calculated field
65+
isFullyDepreciated: boolean; // Calculated field
66+
createdAt: Date;
67+
updatedAt: Date;
68+
}
69+
```
70+
71+
## Entity Methods
72+
73+
The `AssetDepreciation` entity provides several calculation methods:
74+
75+
- `getCurrentDepreciatedValue()`: Returns current value after depreciation
76+
- `getAnnualDepreciation()`: Returns annual depreciation amount
77+
- `getTotalDepreciationToDate()`: Returns total depreciation to current date
78+
- `getRemainingUsefulLife()`: Returns remaining years of useful life
79+
- `isFullyDepreciated()`: Returns true if asset is fully depreciated
80+
81+
## Usage Examples
82+
83+
### Creating an Asset
84+
```typescript
85+
POST /asset-depreciation
86+
{
87+
"assetName": "Dell Laptop",
88+
"description": "Development laptop for engineering team",
89+
"purchasePrice": 15000,
90+
"purchaseDate": "2023-01-01",
91+
"usefulLifeYears": 5,
92+
"salvageValue": 2000
93+
}
94+
```
95+
96+
### Getting Current Depreciated Value
97+
```typescript
98+
GET /asset-depreciation/1/current-value
99+
100+
Response:
101+
{
102+
"id": 1,
103+
"assetName": "Dell Laptop",
104+
"purchasePrice": 15000,
105+
"currentDepreciatedValue": 12400,
106+
"annualDepreciation": 2600,
107+
"totalDepreciationToDate": 2600,
108+
"remainingUsefulLife": 4,
109+
"isFullyDepreciated": false,
110+
// ... other fields
111+
}
112+
```
113+
114+
### Getting Assets with Filters
115+
```typescript
116+
GET /asset-depreciation?isFullyDepreciated=false&minValue=10000&maxValue=50000
117+
```
118+
119+
### Getting Summary Statistics
120+
```typescript
121+
GET /asset-depreciation/summary
122+
123+
Response:
124+
{
125+
"totalAssets": 10,
126+
"totalPurchaseValue": 150000,
127+
"totalCurrentValue": 85000,
128+
"totalDepreciation": 65000,
129+
"fullyDepreciatedAssets": 2,
130+
"averageAge": 2.5
131+
}
132+
```
133+
134+
## Validation Rules
135+
136+
- Purchase date cannot be in the future
137+
- Salvage value must be less than purchase price
138+
- Useful life must be between 1 and 100 years
139+
- Purchase price must be positive
140+
- Asset names must be unique (database constraint)
141+
142+
## Depreciation Formula
143+
144+
The straight-line depreciation uses this formula:
145+
146+
```
147+
Annual Depreciation = (Purchase Price - Salvage Value) / Useful Life Years
148+
Current Value = Purchase Price - (Annual Depreciation × Years Elapsed)
149+
```
150+
151+
The current value will never go below the salvage value, ensuring realistic depreciation calculations.
152+
153+
## Testing
154+
155+
Run tests with:
156+
```bash
157+
npm test -- asset-depreciation.service.spec.ts
158+
```
159+
160+
The test suite covers:
161+
- CRUD operations with validation
162+
- Depreciation calculations
163+
- Edge cases (fully depreciated assets, zero salvage value)
164+
- Error handling and exceptions
165+
- Service business logic
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import {
2+
Controller,
3+
Get,
4+
Post,
5+
Body,
6+
Patch,
7+
Param,
8+
Delete,
9+
ParseIntPipe,
10+
HttpCode,
11+
HttpStatus,
12+
Query,
13+
ValidationPipe,
14+
BadRequestException,
15+
} from '@nestjs/common';
16+
import { AssetDepreciationService } from './asset-depreciation.service';
17+
import {
18+
CreateAssetDepreciationDto,
19+
UpdateAssetDepreciationDto,
20+
DepreciatedValueResponseDto,
21+
AssetDepreciationSummaryDto,
22+
} from './dto/asset-depreciation.dto';
23+
import { AssetDepreciation } from './entities/asset-depreciation.entity';
24+
25+
@Controller('asset-depreciation')
26+
export class AssetDepreciationController {
27+
constructor(private readonly assetDepreciationService: AssetDepreciationService) {}
28+
29+
/**
30+
* Create a new asset depreciation record
31+
* POST /asset-depreciation
32+
*/
33+
@Post()
34+
@HttpCode(HttpStatus.CREATED)
35+
create(
36+
@Body(ValidationPipe) createAssetDepreciationDto: CreateAssetDepreciationDto,
37+
): Promise<AssetDepreciation> {
38+
return this.assetDepreciationService.create(createAssetDepreciationDto);
39+
}
40+
41+
/**
42+
* Get all asset depreciation records with optional filtering
43+
* GET /asset-depreciation?isFullyDepreciated=true&depreciationMethod=straight_line&minValue=1000&maxValue=50000
44+
*/
45+
@Get()
46+
findAll(
47+
@Query('isFullyDepreciated') isFullyDepreciated?: string,
48+
@Query('depreciationMethod') depreciationMethod?: string,
49+
@Query('minValue') minValue?: string,
50+
@Query('maxValue') maxValue?: string,
51+
): Promise<AssetDepreciation[]> {
52+
const filters: any = {};
53+
54+
if (isFullyDepreciated !== undefined) {
55+
if (isFullyDepreciated === 'true') {
56+
filters.isFullyDepreciated = true;
57+
} else if (isFullyDepreciated === 'false') {
58+
filters.isFullyDepreciated = false;
59+
}
60+
}
61+
62+
if (depreciationMethod) {
63+
filters.depreciationMethod = depreciationMethod;
64+
}
65+
66+
if (minValue) {
67+
const minVal = parseFloat(minValue);
68+
if (isNaN(minVal) || minVal < 0) {
69+
throw new BadRequestException('minValue must be a positive number');
70+
}
71+
filters.minValue = minVal;
72+
}
73+
74+
if (maxValue) {
75+
const maxVal = parseFloat(maxValue);
76+
if (isNaN(maxVal) || maxVal < 0) {
77+
throw new BadRequestException('maxValue must be a positive number');
78+
}
79+
filters.maxValue = maxVal;
80+
}
81+
82+
return this.assetDepreciationService.findAll(filters);
83+
}
84+
85+
/**
86+
* Get depreciation summary statistics
87+
* GET /asset-depreciation/summary
88+
*/
89+
@Get('summary')
90+
getSummary(): Promise<AssetDepreciationSummaryDto> {
91+
return this.assetDepreciationService.getSummary();
92+
}
93+
94+
/**
95+
* Get all current depreciated values
96+
* GET /asset-depreciation/current-values
97+
*/
98+
@Get('current-values')
99+
getAllCurrentValues(): Promise<DepreciatedValueResponseDto[]> {
100+
return this.assetDepreciationService.getAllCurrentValues();
101+
}
102+
103+
/**
104+
* Get fully depreciated assets
105+
* GET /asset-depreciation/fully-depreciated
106+
*/
107+
@Get('fully-depreciated')
108+
getFullyDepreciatedAssets(): Promise<DepreciatedValueResponseDto[]> {
109+
return this.assetDepreciationService.getFullyDepreciatedAssets();
110+
}
111+
112+
/**
113+
* Get assets nearing end of useful life
114+
* GET /asset-depreciation/nearing-end-of-life?threshold=1
115+
*/
116+
@Get('nearing-end-of-life')
117+
getAssetsNearingEndOfLife(
118+
@Query('threshold') threshold?: string,
119+
): Promise<DepreciatedValueResponseDto[]> {
120+
let thresholdYears = 1; // default
121+
if (threshold) {
122+
const parsedThreshold = parseFloat(threshold);
123+
if (isNaN(parsedThreshold) || parsedThreshold <= 0) {
124+
throw new BadRequestException('Threshold must be a positive number');
125+
}
126+
thresholdYears = parsedThreshold;
127+
}
128+
return this.assetDepreciationService.getAssetsNearingEndOfLife(thresholdYears);
129+
}
130+
131+
/**
132+
* Get specific asset depreciation record by ID
133+
* GET /asset-depreciation/:id
134+
*/
135+
@Get(':id')
136+
findOne(@Param('id', ParseIntPipe) id: number): Promise<AssetDepreciation> {
137+
return this.assetDepreciationService.findOne(id);
138+
}
139+
140+
/**
141+
* Get current depreciated value of a specific asset
142+
* GET /asset-depreciation/:id/current-value
143+
*/
144+
@Get(':id/current-value')
145+
getCurrentValue(@Param('id', ParseIntPipe) id: number): Promise<DepreciatedValueResponseDto> {
146+
return this.assetDepreciationService.getCurrentValue(id);
147+
}
148+
149+
/**
150+
* Get projected value of asset at future date
151+
* GET /asset-depreciation/:id/projected-value?date=2025-12-31
152+
*/
153+
@Get(':id/projected-value')
154+
async getProjectedValue(
155+
@Param('id', ParseIntPipe) id: number,
156+
@Query('date') date: string,
157+
): Promise<{
158+
assetName: string;
159+
currentValue: number;
160+
projectedValue: number;
161+
depreciationBetween: number;
162+
}> {
163+
if (!date) {
164+
throw new BadRequestException('Date query parameter is required (format: YYYY-MM-DD)');
165+
}
166+
167+
const futureDate = new Date(date);
168+
if (isNaN(futureDate.getTime())) {
169+
throw new BadRequestException('Invalid date format. Use YYYY-MM-DD');
170+
}
171+
172+
return this.assetDepreciationService.getProjectedValue(id, futureDate);
173+
}
174+
175+
/**
176+
* Update an asset depreciation record
177+
* PATCH /asset-depreciation/:id
178+
*/
179+
@Patch(':id')
180+
update(
181+
@Param('id', ParseIntPipe) id: number,
182+
@Body(ValidationPipe) updateAssetDepreciationDto: UpdateAssetDepreciationDto,
183+
): Promise<AssetDepreciation> {
184+
return this.assetDepreciationService.update(id, updateAssetDepreciationDto);
185+
}
186+
187+
/**
188+
* Delete an asset depreciation record
189+
* DELETE /asset-depreciation/:id
190+
*/
191+
@Delete(':id')
192+
@HttpCode(HttpStatus.NO_CONTENT)
193+
remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
194+
return this.assetDepreciationService.remove(id);
195+
}
196+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { AssetDepreciationService } from './asset-depreciation.service';
4+
import { AssetDepreciationController } from './asset-depreciation.controller';
5+
import { AssetDepreciation } from './entities/asset-depreciation.entity';
6+
7+
@Module({
8+
imports: [TypeOrmModule.forFeature([AssetDepreciation])],
9+
controllers: [AssetDepreciationController],
10+
providers: [AssetDepreciationService],
11+
exports: [AssetDepreciationService],
12+
})
13+
export class AssetDepreciationModule {}

0 commit comments

Comments
 (0)