Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions backend/src/asset-depreciation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Asset Depreciation Module

This module implements asset depreciation functionality using straight-line depreciation method to help companies estimate the current value of their assets.

## Features

- **Straight-line Depreciation**: Calculates depreciation using the formula: `(Purchase Price - Salvage Value) / Useful Life Years`
- **Automatic Calculation**: Current depreciated values are calculated automatically based on time elapsed
- **Comprehensive API**: Full CRUD operations plus specialized endpoints for depreciation data
- **Validation**: Input validation for purchase dates, salvage values, and useful life
- **Filtering**: Advanced filtering options for retrieving assets by depreciation status, method, and value ranges

## API Endpoints

### Basic CRUD Operations

- `POST /asset-depreciation` - Create a new asset depreciation record
- `GET /asset-depreciation` - Get all asset depreciation records with optional filters
- `GET /asset-depreciation/:id` - Get a specific asset by ID
- `PATCH /asset-depreciation/:id` - Update an asset depreciation record
- `DELETE /asset-depreciation/:id` - Delete an asset depreciation record

### Specialized Depreciation Endpoints

- `GET /asset-depreciation/current-values` - Get current depreciated values for all assets
- `GET /asset-depreciation/:id/current-value` - Get current depreciated value for a specific asset
- `GET /asset-depreciation/summary` - Get depreciation summary statistics
- `GET /asset-depreciation/fully-depreciated` - Get all fully depreciated assets
- `GET /asset-depreciation/nearing-end-of-life?threshold=1` - Get assets nearing end of useful life
- `GET /asset-depreciation/:id/projected-value?date=2025-12-31` - Get projected value at a future date

## Data Transfer Objects (DTOs)

### CreateAssetDepreciationDto
```typescript
{
assetName: string; // Required, max 255 chars
description?: string; // Optional description
purchasePrice: number; // Required, positive number with max 2 decimal places
purchaseDate: string; // Required, ISO date string (YYYY-MM-DD)
usefulLifeYears: number; // Required, 1-100 years
depreciationMethod?: DepreciationMethod; // Optional, defaults to STRAIGHT_LINE
salvageValue?: number; // Optional, must be less than purchase price
}
```

### UpdateAssetDepreciationDto
All fields are optional versions of CreateAssetDepreciationDto fields.

### DepreciatedValueResponseDto
```typescript
{
id: number;
assetName: string;
description?: string;
purchasePrice: number;
purchaseDate: string;
usefulLifeYears: number;
depreciationMethod: DepreciationMethod;
salvageValue?: number;
currentDepreciatedValue: number; // Calculated field
annualDepreciation: number; // Calculated field
totalDepreciationToDate: number; // Calculated field
remainingUsefulLife: number; // Calculated field
isFullyDepreciated: boolean; // Calculated field
createdAt: Date;
updatedAt: Date;
}
```

## Entity Methods

The `AssetDepreciation` entity provides several calculation methods:

- `getCurrentDepreciatedValue()`: Returns current value after depreciation
- `getAnnualDepreciation()`: Returns annual depreciation amount
- `getTotalDepreciationToDate()`: Returns total depreciation to current date
- `getRemainingUsefulLife()`: Returns remaining years of useful life
- `isFullyDepreciated()`: Returns true if asset is fully depreciated

## Usage Examples

### Creating an Asset
```typescript
POST /asset-depreciation
{
"assetName": "Dell Laptop",
"description": "Development laptop for engineering team",
"purchasePrice": 15000,
"purchaseDate": "2023-01-01",
"usefulLifeYears": 5,
"salvageValue": 2000
}
```

### Getting Current Depreciated Value
```typescript
GET /asset-depreciation/1/current-value

Response:
{
"id": 1,
"assetName": "Dell Laptop",
"purchasePrice": 15000,
"currentDepreciatedValue": 12400,
"annualDepreciation": 2600,
"totalDepreciationToDate": 2600,
"remainingUsefulLife": 4,
"isFullyDepreciated": false,
// ... other fields
}
```

### Getting Assets with Filters
```typescript
GET /asset-depreciation?isFullyDepreciated=false&minValue=10000&maxValue=50000
```

### Getting Summary Statistics
```typescript
GET /asset-depreciation/summary

Response:
{
"totalAssets": 10,
"totalPurchaseValue": 150000,
"totalCurrentValue": 85000,
"totalDepreciation": 65000,
"fullyDepreciatedAssets": 2,
"averageAge": 2.5
}
```

## Validation Rules

- Purchase date cannot be in the future
- Salvage value must be less than purchase price
- Useful life must be between 1 and 100 years
- Purchase price must be positive
- Asset names must be unique (database constraint)

## Depreciation Formula

The straight-line depreciation uses this formula:

```
Annual Depreciation = (Purchase Price - Salvage Value) / Useful Life Years
Current Value = Purchase Price - (Annual Depreciation Γ— Years Elapsed)
```

The current value will never go below the salvage value, ensuring realistic depreciation calculations.

## Testing

Run tests with:
```bash
npm test -- asset-depreciation.service.spec.ts
```

The test suite covers:
- CRUD operations with validation
- Depreciation calculations
- Edge cases (fully depreciated assets, zero salvage value)
- Error handling and exceptions
- Service business logic
196 changes: 196 additions & 0 deletions backend/src/asset-depreciation/asset-depreciation.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
HttpCode,
HttpStatus,
Query,
ValidationPipe,
BadRequestException,
} from '@nestjs/common';
import { AssetDepreciationService } from './asset-depreciation.service';
import {
CreateAssetDepreciationDto,
UpdateAssetDepreciationDto,
DepreciatedValueResponseDto,
AssetDepreciationSummaryDto,
} from './dto/asset-depreciation.dto';
import { AssetDepreciation } from './entities/asset-depreciation.entity';

@Controller('asset-depreciation')
export class AssetDepreciationController {
constructor(private readonly assetDepreciationService: AssetDepreciationService) {}

/**
* Create a new asset depreciation record
* POST /asset-depreciation
*/
@Post()
@HttpCode(HttpStatus.CREATED)
create(
@Body(ValidationPipe) createAssetDepreciationDto: CreateAssetDepreciationDto,
): Promise<AssetDepreciation> {
return this.assetDepreciationService.create(createAssetDepreciationDto);
}

/**
* Get all asset depreciation records with optional filtering
* GET /asset-depreciation?isFullyDepreciated=true&depreciationMethod=straight_line&minValue=1000&maxValue=50000
*/
@Get()
findAll(
@Query('isFullyDepreciated') isFullyDepreciated?: string,
@Query('depreciationMethod') depreciationMethod?: string,
@Query('minValue') minValue?: string,
@Query('maxValue') maxValue?: string,
): Promise<AssetDepreciation[]> {
const filters: any = {};

if (isFullyDepreciated !== undefined) {
if (isFullyDepreciated === 'true') {
filters.isFullyDepreciated = true;
} else if (isFullyDepreciated === 'false') {
filters.isFullyDepreciated = false;
}
}

if (depreciationMethod) {
filters.depreciationMethod = depreciationMethod;
}

if (minValue) {
const minVal = parseFloat(minValue);
if (isNaN(minVal) || minVal < 0) {
throw new BadRequestException('minValue must be a positive number');
}
filters.minValue = minVal;
}

if (maxValue) {
const maxVal = parseFloat(maxValue);
if (isNaN(maxVal) || maxVal < 0) {
throw new BadRequestException('maxValue must be a positive number');
}
filters.maxValue = maxVal;
}

return this.assetDepreciationService.findAll(filters);
}

/**
* Get depreciation summary statistics
* GET /asset-depreciation/summary
*/
@Get('summary')
getSummary(): Promise<AssetDepreciationSummaryDto> {
return this.assetDepreciationService.getSummary();
}

/**
* Get all current depreciated values
* GET /asset-depreciation/current-values
*/
@Get('current-values')
getAllCurrentValues(): Promise<DepreciatedValueResponseDto[]> {
return this.assetDepreciationService.getAllCurrentValues();
}

/**
* Get fully depreciated assets
* GET /asset-depreciation/fully-depreciated
*/
@Get('fully-depreciated')
getFullyDepreciatedAssets(): Promise<DepreciatedValueResponseDto[]> {
return this.assetDepreciationService.getFullyDepreciatedAssets();
}

/**
* Get assets nearing end of useful life
* GET /asset-depreciation/nearing-end-of-life?threshold=1
*/
@Get('nearing-end-of-life')
getAssetsNearingEndOfLife(
@Query('threshold') threshold?: string,
): Promise<DepreciatedValueResponseDto[]> {
let thresholdYears = 1; // default
if (threshold) {
const parsedThreshold = parseFloat(threshold);
if (isNaN(parsedThreshold) || parsedThreshold <= 0) {
throw new BadRequestException('Threshold must be a positive number');
}
thresholdYears = parsedThreshold;
}
return this.assetDepreciationService.getAssetsNearingEndOfLife(thresholdYears);
}

/**
* Get specific asset depreciation record by ID
* GET /asset-depreciation/:id
*/
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number): Promise<AssetDepreciation> {
return this.assetDepreciationService.findOne(id);
}

/**
* Get current depreciated value of a specific asset
* GET /asset-depreciation/:id/current-value
*/
@Get(':id/current-value')
getCurrentValue(@Param('id', ParseIntPipe) id: number): Promise<DepreciatedValueResponseDto> {
return this.assetDepreciationService.getCurrentValue(id);
}

/**
* Get projected value of asset at future date
* GET /asset-depreciation/:id/projected-value?date=2025-12-31
*/
@Get(':id/projected-value')
async getProjectedValue(
@Param('id', ParseIntPipe) id: number,
@Query('date') date: string,
): Promise<{
assetName: string;
currentValue: number;
projectedValue: number;
depreciationBetween: number;
}> {
if (!date) {
throw new BadRequestException('Date query parameter is required (format: YYYY-MM-DD)');
}

const futureDate = new Date(date);
if (isNaN(futureDate.getTime())) {
throw new BadRequestException('Invalid date format. Use YYYY-MM-DD');
}

return this.assetDepreciationService.getProjectedValue(id, futureDate);
}

/**
* Update an asset depreciation record
* PATCH /asset-depreciation/:id
*/
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body(ValidationPipe) updateAssetDepreciationDto: UpdateAssetDepreciationDto,
): Promise<AssetDepreciation> {
return this.assetDepreciationService.update(id, updateAssetDepreciationDto);
}

/**
* Delete an asset depreciation record
* DELETE /asset-depreciation/:id
*/
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
return this.assetDepreciationService.remove(id);
}
}
13 changes: 13 additions & 0 deletions backend/src/asset-depreciation/asset-depreciation.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetDepreciationService } from './asset-depreciation.service';
import { AssetDepreciationController } from './asset-depreciation.controller';
import { AssetDepreciation } from './entities/asset-depreciation.entity';

@Module({
imports: [TypeOrmModule.forFeature([AssetDepreciation])],
controllers: [AssetDepreciationController],
providers: [AssetDepreciationService],
exports: [AssetDepreciationService],
})
export class AssetDepreciationModule {}
Loading