Skip to content

Commit 5c77291

Browse files
committed
feat(compliance): implement license and compliance module
1 parent 8de071b commit 5c77291

File tree

8 files changed

+234
-0
lines changed

8 files changed

+234
-0
lines changed
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+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { Cron, CronExpression } from '@nestjs/schedule';
3+
import { LicensesService } from '../services/licenses.service';
4+
import { EventEmitter2 } from '@nestjs/event-emitter';
5+
6+
@Injectable()
7+
export class LicenseExpiryTask {
8+
private readonly logger = new Logger(LicenseExpiryTask.name);
9+
10+
constructor(
11+
private readonly licensesService: LicensesService,
12+
private readonly eventEmitter: EventEmitter2,
13+
) {}
14+
15+
@Cron(CronExpression.EVERY_DAY_AT_9AM, { name: 'checkLicenseExpiry' })
16+
async handleCron() {
17+
this.logger.log('Running scheduled job: Checking for expiring licenses...');
18+
19+
const expiringLicenses = await this.licensesService.findLicensesNearingExpiry();
20+
21+
if (expiringLicenses.length === 0) {
22+
this.logger.log('No new licenses nearing expiry found.');
23+
return;
24+
}
25+
26+
this.logger.log(`Found ${expiringLicenses.length} licenses nearing expiry. Triggering alerts...`);
27+
28+
for (const license of expiringLicenses) {
29+
// Emit an event for the notification module to handle
30+
this.eventEmitter.emit('license.nearing_expiry', {
31+
licenseId: license.id,
32+
assetId: license.assetId,
33+
expiryDate: license.expiryDate,
34+
// Add user/admin details here for notification routing
35+
});
36+
}
37+
38+
// Mark these licenses as notified to prevent duplicate alerts
39+
const idsToUpdate = expiringLicenses.map(l => l.id);
40+
await this.licensesService.markAsNotified(idsToUpdate);
41+
}
42+
}

backend/package-lock.json

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@nestjs/mapped-types": "*",
2828
"@nestjs/passport": "^11.0.5",
2929
"@nestjs/platform-express": "^10.0.0",
30+
"@nestjs/schedule": "^6.0.1",
3031
"@nestjs/swagger": "^7.3.0",
3132
"@nestjs/typeorm": "^10.0.2",
3233
"@types/multer": "^2.0.0",

0 commit comments

Comments
 (0)