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
25 changes: 25 additions & 0 deletions backend/src/insurance/insurance-query.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IsOptional, IsString, IsInt, Min } from 'class-validator';
import { Type } from 'class-transformer';

export class QueryAssetInsuranceDto {
@IsOptional()
@IsString()
assetId?: string;

@IsOptional()
@IsString()
provider?: string;

// pagination
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
skip?: number;

@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
take?: number;
}
59 changes: 59 additions & 0 deletions backend/src/insurance/insurance.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
Controller,
Get,
Post,
Body,
Param,
Put,
Delete,
Query,
ParseUUIDPipe,
UsePipes,
ValidationPipe,
DefaultValuePipe,
ParseIntPipe,
} from '@nestjs/common';
import { AssetInsuranceService } from './asset-insurance.service';
import { CreateAssetInsuranceDto } from './dto/create-asset-insurance.dto';
import { UpdateAssetInsuranceDto } from './dto/update-asset-insurance.dto';
import { QueryAssetInsuranceDto } from './dto/query-asset-insurance.dto';

@Controller('asset-insurance')
export class AssetInsuranceController {
constructor(private readonly service: AssetInsuranceService) {}

@Post()
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
create(@Body() dto: CreateAssetInsuranceDto) {
return this.service.create(dto);
}

@Get()
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
findAll(@Query() q: QueryAssetInsuranceDto) {
const { assetId, provider, skip, take } = q;
return this.service.findAll({ assetId, provider, skip, take });
}

@Get(':id')
findOne(@Param('id', new ParseUUIDPipe()) id: string) {
return this.service.findOne(id);
}

@Put(':id')
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
update(@Param('id', new ParseUUIDPipe()) id: string, @Body() dto: UpdateAssetInsuranceDto) {
return this.service.update(id, dto);
}

@Delete(':id')
remove(@Param('id', new ParseUUIDPipe()) id: string) {
return this.service.remove(id);
}

// Optional quick endpoint to trigger the expiry check manually
@Post('run-expiry-check')
runExpiryCheck(@Query('days', new DefaultValuePipe(30), ParseIntPipe) days: number) {
return this.service.runExpiryNotifications(days);
}
}
25 changes: 25 additions & 0 deletions backend/src/insurance/insurance.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IsString, IsNotEmpty, IsOptional, IsDateString, MaxLength } from 'class-validator';

export class CreateAssetInsuranceDto {
@IsString()
@IsNotEmpty()
@MaxLength(100)
assetId: string;

@IsString()
@IsNotEmpty()
@MaxLength(200)
policyNumber: string;

@IsString()
@IsNotEmpty()
@MaxLength(200)
provider: string;

@IsDateString()
expiryDate: string; // ISO date string

@IsOptional()
@IsString()
notes?: string;
}
31 changes: 31 additions & 0 deletions backend/src/insurance/insurance.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';

@Entity({ name: 'asset_insurance' })
export class AssetInsurance {
@PrimaryGeneratedColumn('uuid')
id: string;

// Could be a foreign key to an assets table. We store it as UUID or number depending on your asset PK.
@Column({ name: 'asset_id', type: 'varchar', length: 100 })
@Index()
assetId: string;

@Column({ name: 'policy_number', type: 'varchar', length: 200, unique: true })
@Index()
policyNumber: string;

@Column({ name: 'provider', type: 'varchar', length: 200 })
provider: string;

@Column({ name: 'expiry_date', type: 'timestamptz' })
expiryDate: Date;

@Column({ name: 'notes', type: 'text', nullable: true })
notes?: string | null;

@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;

@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}
19 changes: 19 additions & 0 deletions backend/src/insurance/insurance.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ScheduleModule } from '@nestjs/schedule';
import { AssetInsurance } from './asset-insurance.entity';
import { AssetInsuranceService } from './asset-insurance.service';
import { AssetInsuranceController } from './asset-insurance.controller';
import { NotificationService } from './notification.service';
import { ExpirySchedulerService } from './expiry-scheduler.service';

@Module({
imports: [
TypeOrmModule.forFeature([AssetInsurance]),
ScheduleModule, // allow injection of scheduler, but make sure ScheduleModule.forRoot() is registered in AppModule
],
providers: [AssetInsuranceService, NotificationService, ExpirySchedulerService],
controllers: [AssetInsuranceController],
exports: [AssetInsuranceService],
})
export class AssetInsuranceModule {}
103 changes: 103 additions & 0 deletions backend/src/insurance/insurance.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
import { AssetInsurance } from './asset-insurance.entity';
import { CreateAssetInsuranceDto } from './dto/create-asset-insurance.dto';
import { UpdateAssetInsuranceDto } from './dto/update-asset-insurance.dto';
import { NotificationService } from './notification.service';

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

constructor(
@InjectRepository(AssetInsurance)
private readonly repo: Repository<AssetInsurance>,
private readonly notificationService: NotificationService,
) {}

async create(dto: CreateAssetInsuranceDto): Promise<AssetInsurance> {
const entity = this.repo.create({
assetId: dto.assetId,
policyNumber: dto.policyNumber,
provider: dto.provider,
expiryDate: new Date(dto.expiryDate),
notes: dto.notes ?? null,
});
return this.repo.save(entity);
}

async findOne(id: string): Promise<AssetInsurance> {
const found = await this.repo.findOne({ where: { id } });
if (!found) throw new NotFoundException(`AssetInsurance ${id} not found`);
return found;
}

async findAll(query: { assetId?: string; provider?: string; skip?: number; take?: number }) {
const qb = this.repo.createQueryBuilder('ai');

if (query.assetId) qb.andWhere('ai.assetId = :assetId', { assetId: query.assetId });
if (query.provider) qb.andWhere('ai.provider ILIKE :provider', { provider: `%${query.provider}%` });

if (typeof query.skip === 'number') qb.skip(query.skip);
if (typeof query.take === 'number') qb.take(query.take);

const [items, total] = await qb.getManyAndCount();
return { items, total };
}

async update(id: string, dto: UpdateAssetInsuranceDto): Promise<AssetInsurance> {
const entity = await this.findOne(id);

if (dto.assetId) entity.assetId = dto.assetId;
if (dto.policyNumber) entity.policyNumber = dto.policyNumber;
if (dto.provider) entity.provider = dto.provider;
if (dto.expiryDate) entity.expiryDate = new Date(dto.expiryDate);
if (dto.notes !== undefined) entity.notes = dto.notes;

return this.repo.save(entity);
}

async remove(id: string): Promise<void> {
const res = await this.repo.delete({ id });
if (res.affected === 0) throw new NotFoundException(`AssetInsurance ${id} not found`);
}

/**
* Return policies that will expire within the next `days` (inclusive)
*/
async findExpiringWithin(days: number) {
const now = new Date();
const until = new Date(now);
until.setUTCDate(until.getUTCDate() + days);

return this.repo.find({
where: {
expiryDate: MoreThanOrEqual(now) as any, // ensure expiry >= now
},
}).then(list => list.filter(item => item.expiryDate <= until));
}

/**
* Run the expiry check and send notifications via NotificationService
*/
async runExpiryNotifications(daysBeforeExpiry = 30) {
this.logger.log(`Checking policies expiring within ${daysBeforeExpiry} day(s)...`);
const now = new Date();
const until = new Date(now);
until.setUTCDate(until.getUTCDate() + daysBeforeExpiry);

const toNotify = await this.repo.createQueryBuilder('ai')
.where('ai.expiry_date BETWEEN :now AND :until', { now: now.toISOString(), until: until.toISOString() })
.getMany();

this.logger.log(`Found ${toNotify.length} policy(ies) to notify`);
for (const policy of toNotify) {
try {
await this.notificationService.notifyExpiry(policy, daysBeforeExpiry);
} catch (err) {
this.logger.error(`Failed to send notification for policy ${policy.id}: ${err?.message ?? err}`);
}
}
}
}
22 changes: 22 additions & 0 deletions backend/src/insurance/notification.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable, Logger } from '@nestjs/common';
import { AssetInsurance } from './asset-insurance.entity';

/**
* Replace/extend this service to integrate with your email/SMS/push provider.
* For now it logs and pretends to send.
*/
@Injectable()
export class NotificationService {
private readonly logger = new Logger(NotificationService.name);

async notifyExpiry(policy: AssetInsurance, daysBeforeExpiry: number) {
// Example payload; adapt to your notification template & recipients
const message = `Policy ${policy.policyNumber} for asset ${policy.assetId} expires on ${policy.expiryDate.toISOString()} (in ${daysBeforeExpiry} day(s) or less). Provider: ${policy.provider}.`;
// TODO: send email / sms / push using your provider
this.logger.warn(`[ExpiryNotification] ${message}`);

// If you integrate email, you may add:
// await this.mailerService.sendMail({ to: 'ops@example.com', subject: 'Policy expiry', text: message });
return Promise.resolve(true);
}
}
9 changes: 9 additions & 0 deletions backend/src/insurance/update-insurance.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateAssetInsuranceDto } from './create-asset-insurance.dto';
import { IsOptional, IsDateString } from 'class-validator';

export class UpdateAssetInsuranceDto extends PartialType(CreateAssetInsuranceDto) {
@IsOptional()
@IsDateString()
expiryDate?: string;
}