Skip to content

Commit 4b336ca

Browse files
Merge branch 'main' into feat/BACKEND-Implement-Vendor-Contract-Management-Module-#295
2 parents b81fbae + f3a7490 commit 4b336ca

File tree

11 files changed

+357
-0
lines changed

11 files changed

+357
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Controller, Get, Post, Body, Param, Patch, UsePipes, ValidationPipe } from '@nestjs/common';
2+
import { AssetAssignmentsService } from './asset-assignments.service';
3+
import { CreateAssignmentDto } from './dto/create-assignment.dto';
4+
5+
@Controller('asset-assignments')
6+
export class AssetAssignmentsController {
7+
constructor(private readonly assignmentsService: AssetAssignmentsService) {}
8+
9+
@Post('assign')
10+
@UsePipes(new ValidationPipe({ whitelist: true }))
11+
assign(@Body() createDto: CreateAssignmentDto) {
12+
return this.assignmentsService.assign(createDto);
13+
}
14+
15+
@Patch('unassign/:assetId')
16+
unassign(@Param('assetId') assetId: string) {
17+
return this.assignmentsService.unassign(assetId);
18+
}
19+
20+
@Get('history/:assetId')
21+
getHistory(@Param('assetId') assetId: string) {
22+
return this.assignmentsService.getHistoryForAsset(assetId);
23+
}
24+
25+
@Get('current/:assetId')
26+
getCurrentAssignment(@Param('assetId') assetId: string) {
27+
return this.assignmentsService.getCurrentAssignmentForAsset(assetId);
28+
}
29+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { AssetAssignment } from './entities/asset-assignment.entity';
4+
import { AssetAssignmentsService } from './asset-assignments.service';
5+
import { AssetAssignmentsController } from './asset-assignments.controller';
6+
7+
@Module({
8+
imports: [TypeOrmModule.forFeature([AssetAssignment])],
9+
controllers: [AssetAssignmentsController],
10+
providers: [AssetAssignmentsService],
11+
})
12+
export class AssetAssignmentsModule {}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
Injectable,
3+
NotFoundException,
4+
ConflictException,
5+
} from '@nestjs/common';
6+
import { InjectRepository } from '@nestjs/typeorm';
7+
import { Repository, IsNull } from 'typeorm';
8+
import { AssetAssignment } from './entities/asset-assignment.entity';
9+
import { CreateAssignmentDto } from './dto/create-assignment.dto';
10+
11+
@Injectable()
12+
export class AssetAssignmentsService {
13+
constructor(
14+
@InjectRepository(AssetAssignment)
15+
private readonly assignmentRepository: Repository<AssetAssignment>,
16+
) {}
17+
18+
async assign(createDto: CreateAssignmentDto): Promise<AssetAssignment> {
19+
// 1. Check if the asset is already actively assigned
20+
const existingAssignment = await this.assignmentRepository.findOne({
21+
where: {
22+
assetId: createDto.assetId,
23+
unassignmentDate: IsNull(),
24+
},
25+
});
26+
27+
if (existingAssignment) {
28+
throw new ConflictException(
29+
`Asset with ID "${createDto.assetId}" is already assigned.`,
30+
);
31+
}
32+
33+
if (!createDto.assignedToUserId && !createDto.assignedToDepartmentId) {
34+
throw new ConflictException(
35+
'An assignment must have either a user ID or a department ID.',
36+
);
37+
}
38+
39+
// 2. Create the new assignment record
40+
const newAssignment = this.assignmentRepository.create({
41+
...createDto,
42+
assignmentDate: new Date(),
43+
});
44+
45+
return this.assignmentRepository.save(newAssignment);
46+
}
47+
48+
async unassign(assetId: string): Promise<AssetAssignment> {
49+
// Find the current active assignment for the asset
50+
const currentAssignment = await this.assignmentRepository.findOne({
51+
where: {
52+
assetId,
53+
unassignmentDate: IsNull(),
54+
},
55+
});
56+
57+
if (!currentAssignment) {
58+
throw new NotFoundException(
59+
`No active assignment found for asset with ID "${assetId}".`,
60+
);
61+
}
62+
63+
// Mark the assignment as historical by setting the unassignment date
64+
currentAssignment.unassignmentDate = new Date();
65+
return this.assignmentRepository.save(currentAssignment);
66+
}
67+
68+
async getHistoryForAsset(assetId: string): Promise<AssetAssignment[]> {
69+
return this.assignmentRepository.find({
70+
where: { assetId },
71+
order: { assignmentDate: 'DESC' }, // Show the most recent first
72+
});
73+
}
74+
75+
async getCurrentAssignmentForAsset(
76+
assetId: string,
77+
): Promise<AssetAssignment> {
78+
const assignment = await this.assignmentRepository.findOne({
79+
where: { assetId, unassignmentDate: IsNull() },
80+
});
81+
if (!assignment) {
82+
throw new NotFoundException(
83+
`No active assignment found for asset ID "${assetId}".`,
84+
);
85+
}
86+
return assignment;
87+
}
88+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2+
3+
export class CreateAssignmentDto {
4+
@IsString()
5+
@IsNotEmpty()
6+
assetId: string;
7+
8+
@IsString()
9+
@IsOptional()
10+
assignedToUserId?: string;
11+
12+
@IsString()
13+
@IsOptional()
14+
assignedToDepartmentId?: string;
15+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
2+
3+
@Entity('asset_assignments')
4+
export class AssetAssignment {
5+
@PrimaryGeneratedColumn('uuid')
6+
id: string;
7+
8+
@Column()
9+
assetId: string;
10+
11+
@Column({ nullable: true })
12+
assignedToUserId?: string;
13+
14+
@Column({ nullable: true })
15+
assignedToDepartmentId?: string;
16+
17+
@Column({ type: 'timestamp' })
18+
assignmentDate: Date;
19+
20+
@Column({ type: 'timestamp', nullable: true })
21+
unassignmentDate?: Date; // This field tracks the history. Null means it's the current assignment.
22+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { IsString, IsNotEmpty, IsDateString, IsUrl } from 'class-validator';
2+
3+
export class CreateLicenseDto {
4+
@IsString()
5+
@IsNotEmpty()
6+
assetId: string;
7+
8+
@IsString()
9+
@IsNotEmpty()
10+
licenseType: string;
11+
12+
@IsDateString()
13+
expiryDate: string;
14+
15+
@IsUrl()
16+
documentUrl: string;
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
2+
3+
@Entity('licenses')
4+
export class License {
5+
@PrimaryGeneratedColumn('uuid')
6+
id: string;
7+
8+
@Column()
9+
assetId: string; // The ID of the asset this license belongs to
10+
11+
@Column()
12+
licenseType: string; // e.g., "Vehicle Registration", "Software License"
13+
14+
@Column({ type: 'date' })
15+
expiryDate: Date;
16+
17+
@Column()
18+
documentUrl: string; // URL to the stored document (e.g., in S3)
19+
20+
@Column({ default: false })
21+
isExpiryNotified: boolean; // Flag to prevent sending repeated alerts
22+
23+
@CreateDateColumn()
24+
createdAt: Date;
25+
26+
@UpdateDateColumn()
27+
updatedAt: Date;
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Controller, Get, Post, Body, Param, Delete, UsePipes, ValidationPipe } from '@nestjs/common';
2+
import { LicensesService } from './services/licenses.service';
3+
import { CreateLicenseDto } from './dto/create-license.dto';
4+
5+
@Controller('licenses')
6+
export class LicensesController {
7+
constructor(private readonly licensesService: LicensesService) {}
8+
9+
@Post()
10+
@UsePipes(new ValidationPipe({ whitelist: true }))
11+
create(@Body() createDto: CreateLicenseDto) {
12+
return this.licensesService.create(createDto);
13+
}
14+
15+
@Get('asset/:assetId')
16+
findAllForAsset(@Param('assetId') assetId: string) {
17+
return this.licensesService.findAllForAsset(assetId);
18+
}
19+
20+
@Get(':id')
21+
findOne(@Param('id') id: string) {
22+
return this.licensesService.findOne(id);
23+
}
24+
25+
@Delete(':id')
26+
remove(@Param('id') id: string) {
27+
return this.licensesService.remove(id);
28+
}
29+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { ScheduleModule } from '@nestjs/schedule';
4+
import { License } from './entities/license.entity';
5+
import { LicensesService } from './services/licenses.service';
6+
import { LicensesController } from './licenses.controller';
7+
import { LicenseExpiryTask } from './tasks/license-expiry.task';
8+
9+
@Module({
10+
imports: [
11+
TypeOrmModule.forFeature([License]),
12+
ScheduleModule.forRoot(), // Initialize the scheduler
13+
],
14+
controllers: [LicensesController],
15+
providers: [LicensesService, LicenseExpiryTask],
16+
})
17+
export class LicensesModule {}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Injectable, NotFoundException } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { Repository, LessThanOrEqual } from 'typeorm';
4+
import { License } from '../entities/license.entity';
5+
import { CreateLicenseDto } from '../dto/create-license.dto';
6+
import { LessThan } from 'typeorm';
7+
import { addDays } from 'date-fns';
8+
9+
@Injectable()
10+
export class LicensesService {
11+
constructor(
12+
@InjectRepository(License)
13+
private readonly licenseRepository: Repository<License>,
14+
) {}
15+
16+
async create(createDto: CreateLicenseDto): Promise<License> {
17+
const license = this.licenseRepository.create(createDto);
18+
return this.licenseRepository.save(license);
19+
}
20+
21+
async findAllForAsset(assetId: string): Promise<License[]> {
22+
return this.licenseRepository.find({ where: { assetId } });
23+
}
24+
25+
async findOne(id: string): Promise<License> {
26+
const license = await this.licenseRepository.findOne({ where: { id } });
27+
if (!license) {
28+
throw new NotFoundException(`License with ID "${id}" not found.`);
29+
}
30+
return license;
31+
}
32+
33+
async remove(id: string): Promise<{ deleted: boolean }> {
34+
const result = await this.licenseRepository.delete(id);
35+
if (result.affected === 0) {
36+
throw new NotFoundException(`License with ID "${id}" not found.`);
37+
}
38+
return { deleted: true };
39+
}
40+
41+
/**
42+
* Finds all licenses that will expire within the next 30 days and have not been notified yet.
43+
*/
44+
async findLicensesNearingExpiry(): Promise<License[]> {
45+
const thirtyDaysFromNow = addDays(new Date(), 30);
46+
return this.licenseRepository.find({
47+
where: {
48+
expiryDate: LessThanOrEqual(thirtyDaysFromNow),
49+
isExpiryNotified: false,
50+
},
51+
});
52+
}
53+
54+
async markAsNotified(licenseIds: string[]): Promise<void> {
55+
if (licenseIds.length === 0) return;
56+
await this.licenseRepository.update(licenseIds, { isExpiryNotified: true });
57+
}
58+
}

0 commit comments

Comments
 (0)