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
17 changes: 17 additions & 0 deletions backend/licenses/dto/create-license.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { IsString, IsNotEmpty, IsDateString, IsUrl } from 'class-validator';

export class CreateLicenseDto {
@IsString()
@IsNotEmpty()
assetId: string;

@IsString()
@IsNotEmpty()
licenseType: string;

@IsDateString()
expiryDate: string;

@IsUrl()
documentUrl: string;
}
28 changes: 28 additions & 0 deletions backend/licenses/entities/license.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('licenses')
export class License {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
assetId: string; // The ID of the asset this license belongs to

@Column()
licenseType: string; // e.g., "Vehicle Registration", "Software License"

@Column({ type: 'date' })
expiryDate: Date;

@Column()
documentUrl: string; // URL to the stored document (e.g., in S3)

@Column({ default: false })
isExpiryNotified: boolean; // Flag to prevent sending repeated alerts

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
29 changes: 29 additions & 0 deletions backend/licenses/licenses.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Controller, Get, Post, Body, Param, Delete, UsePipes, ValidationPipe } from '@nestjs/common';
import { LicensesService } from './services/licenses.service';
import { CreateLicenseDto } from './dto/create-license.dto';

@Controller('licenses')
export class LicensesController {
constructor(private readonly licensesService: LicensesService) {}

@Post()
@UsePipes(new ValidationPipe({ whitelist: true }))
create(@Body() createDto: CreateLicenseDto) {
return this.licensesService.create(createDto);
}

@Get('asset/:assetId')
findAllForAsset(@Param('assetId') assetId: string) {
return this.licensesService.findAllForAsset(assetId);
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.licensesService.findOne(id);
}

@Delete(':id')
remove(@Param('id') id: string) {
return this.licensesService.remove(id);
}
}
17 changes: 17 additions & 0 deletions backend/licenses/licenses.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ScheduleModule } from '@nestjs/schedule';
import { License } from './entities/license.entity';
import { LicensesService } from './services/licenses.service';
import { LicensesController } from './licenses.controller';
import { LicenseExpiryTask } from './tasks/license-expiry.task';

@Module({
imports: [
TypeOrmModule.forFeature([License]),
ScheduleModule.forRoot(), // Initialize the scheduler
],
controllers: [LicensesController],
providers: [LicensesService, LicenseExpiryTask],
})
export class LicensesModule {}
58 changes: 58 additions & 0 deletions backend/licenses/service/licenses.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, LessThanOrEqual } from 'typeorm';
import { License } from '../entities/license.entity';
import { CreateLicenseDto } from '../dto/create-license.dto';
import { LessThan } from 'typeorm';
import { addDays } from 'date-fns';

@Injectable()
export class LicensesService {
constructor(
@InjectRepository(License)
private readonly licenseRepository: Repository<License>,
) {}

async create(createDto: CreateLicenseDto): Promise<License> {
const license = this.licenseRepository.create(createDto);
return this.licenseRepository.save(license);
}

async findAllForAsset(assetId: string): Promise<License[]> {
return this.licenseRepository.find({ where: { assetId } });
}

async findOne(id: string): Promise<License> {
const license = await this.licenseRepository.findOne({ where: { id } });
if (!license) {
throw new NotFoundException(`License with ID "${id}" not found.`);
}
return license;
}

async remove(id: string): Promise<{ deleted: boolean }> {
const result = await this.licenseRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`License with ID "${id}" not found.`);
}
return { deleted: true };
}

/**
* Finds all licenses that will expire within the next 30 days and have not been notified yet.
*/
async findLicensesNearingExpiry(): Promise<License[]> {
const thirtyDaysFromNow = addDays(new Date(), 30);
return this.licenseRepository.find({
where: {
expiryDate: LessThanOrEqual(thirtyDaysFromNow),
isExpiryNotified: false,
},
});
}

async markAsNotified(licenseIds: string[]): Promise<void> {
if (licenseIds.length === 0) return;
await this.licenseRepository.update(licenseIds, { isExpiryNotified: true });
}
}
42 changes: 42 additions & 0 deletions backend/licenses/tasks/license-expiry.task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { LicensesService } from '../services/licenses.service';
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class LicenseExpiryTask {
private readonly logger = new Logger(LicenseExpiryTask.name);

constructor(
private readonly licensesService: LicensesService,
private readonly eventEmitter: EventEmitter2,
) {}

@Cron(CronExpression.EVERY_DAY_AT_9AM, { name: 'checkLicenseExpiry' })
async handleCron() {
this.logger.log('Running scheduled job: Checking for expiring licenses...');

const expiringLicenses = await this.licensesService.findLicensesNearingExpiry();

if (expiringLicenses.length === 0) {
this.logger.log('No new licenses nearing expiry found.');
return;
}

this.logger.log(`Found ${expiringLicenses.length} licenses nearing expiry. Triggering alerts...`);

for (const license of expiringLicenses) {
// Emit an event for the notification module to handle
this.eventEmitter.emit('license.nearing_expiry', {
licenseId: license.id,
assetId: license.assetId,
expiryDate: license.expiryDate,
// Add user/admin details here for notification routing
});
}

// Mark these licenses as notified to prevent duplicate alerts
const idsToUpdate = expiringLicenses.map(l => l.id);
await this.licensesService.markAsNotified(idsToUpdate);
}
}
42 changes: 42 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^6.0.1",
"@nestjs/swagger": "^7.3.0",
"@nestjs/typeorm": "^10.0.2",
"@types/multer": "^2.0.0",
Expand Down