Skip to content

Commit cbff52c

Browse files
authored
Merge pull request #576 from soundsng/feat/be-risk-report-dispute-submission-status-notifications
feat: add RiskReportService, DisputeSubmissionController, DisputeStatusController, and NotificationController
2 parents 529e56c + 5ec389b commit cbff52c

4 files changed

Lines changed: 137 additions & 0 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Controller, Get, Param, Patch, Body, Req, UseGuards, NotFoundException, ForbiddenException, ConflictException } from '@nestjs/common';
2+
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
3+
import { User, UserRole } from '../../users/entities/user.entity';
4+
5+
const TERMINAL = new Set(['RESOLVED', 'REJECTED']);
6+
const disputes = new Map<string, Record<string, unknown>>();
7+
8+
@Controller('module/disputes')
9+
@UseGuards(JwtAuthGuard)
10+
export class DisputeStatusController {
11+
@Get()
12+
list(@Req() req: { user: User }) {
13+
return [...disputes.values()].filter((d) => d['submittedBy'] === req.user.id);
14+
}
15+
16+
@Get(':id')
17+
get(@Param('id') id: string, @Req() req: { user: User }) {
18+
const d = disputes.get(id);
19+
if (!d) throw new NotFoundException();
20+
if (d['submittedBy'] !== req.user.id && req.user.role !== UserRole.ADMIN) throw new ForbiddenException();
21+
return d;
22+
}
23+
24+
@Patch(':id/status')
25+
updateStatus(@Param('id') id: string, @Body() body: { status: string }, @Req() req: { user: User }) {
26+
if (req.user.role !== UserRole.ADMIN) throw new ForbiddenException();
27+
const d = disputes.get(id);
28+
if (!d) throw new NotFoundException();
29+
if (TERMINAL.has(d['status'] as string)) throw new ConflictException('Dispute is in a terminal state');
30+
d['status'] = body.status;
31+
return d;
32+
}
33+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Body, Controller, Post, Req, UseGuards, NotFoundException } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { Repository } from 'typeorm';
4+
import { IsNotEmpty, IsUUID, MinLength } from 'class-validator';
5+
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
6+
import { Document } from '../../documents/entities/document.entity';
7+
import { User } from '../../users/entities/user.entity';
8+
9+
class CreateDisputeDto {
10+
@IsUUID() documentId: string;
11+
@MinLength(20) description: string;
12+
@IsNotEmpty() disputeType: string;
13+
}
14+
15+
@Controller('module/disputes')
16+
@UseGuards(JwtAuthGuard)
17+
export class DisputeSubmissionController {
18+
constructor(@InjectRepository(Document) private readonly docs: Repository<Document>) {}
19+
20+
@Post()
21+
async submit(@Body() dto: CreateDisputeDto, @Req() req: { user: User }) {
22+
const doc = await this.docs.findOneBy({ id: dto.documentId });
23+
if (!doc) throw new NotFoundException('Document not found');
24+
return {
25+
id: crypto.randomUUID(),
26+
documentId: dto.documentId,
27+
submittedBy: req.user.id,
28+
description: dto.description,
29+
disputeType: dto.disputeType,
30+
status: 'OPEN',
31+
createdAt: new Date(),
32+
};
33+
}
34+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Controller, Get, Patch, Param, Req, UseGuards } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Repository } from 'typeorm';
4+
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
5+
import { User } from '../../users/entities/user.entity';
6+
7+
export enum NotificationType { DOCUMENT_VERIFIED = 'DOCUMENT_VERIFIED', DOCUMENT_FLAGGED = 'DOCUMENT_FLAGGED', DISPUTE_UPDATED = 'DISPUTE_UPDATED' }
8+
9+
@Entity('notifications')
10+
export class Notification {
11+
@PrimaryGeneratedColumn('uuid') id: string;
12+
@Column() userId: string;
13+
@Column() message: string;
14+
@Column({ type: 'enum', enum: NotificationType }) type: NotificationType;
15+
@Column({ default: false }) isRead: boolean;
16+
@Column({ nullable: true }) resourceId: string;
17+
@Column({ nullable: true }) resourceType: string;
18+
@CreateDateColumn() createdAt: Date;
19+
}
20+
21+
@Controller('module/notifications')
22+
@UseGuards(JwtAuthGuard)
23+
export class NotificationController {
24+
constructor(@InjectRepository(Notification) private readonly repo: Repository<Notification>) {}
25+
26+
@Get()
27+
list(@Req() req: { user: User }) {
28+
return this.repo.find({ where: { userId: req.user.id }, order: { createdAt: 'DESC' } });
29+
}
30+
31+
@Get('unread-count')
32+
async unreadCount(@Req() req: { user: User }) {
33+
const count = await this.repo.count({ where: { userId: req.user.id, isRead: false } });
34+
return { count };
35+
}
36+
37+
@Patch(':id/read')
38+
async markRead(@Param('id') id: string) {
39+
await this.repo.update(id, { isRead: true });
40+
return this.repo.findOneByOrFail({ id });
41+
}
42+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Injectable, NotFoundException, UnprocessableEntityException, ForbiddenException } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { Repository } from 'typeorm';
4+
import { Document } from '../../documents/entities/document.entity';
5+
6+
const FLAG_DESCRIPTIONS: Record<string, { description: string; action: string }> = {
7+
duplicate: { description: 'This document appears to be a duplicate of an existing record.', action: 'Verify the document is unique before proceeding.' },
8+
tampered: { description: 'Signs of document tampering were detected.', action: 'Obtain a certified original copy.' },
9+
expired: { description: 'The document may be expired or outdated.', action: 'Renew or replace the document.' },
10+
};
11+
12+
@Injectable()
13+
export class RiskReportService {
14+
constructor(@InjectRepository(Document) private readonly docs: Repository<Document>) {}
15+
16+
async generateReport(documentId: string, userId: string) {
17+
const doc = await this.docs.findOneBy({ id: documentId });
18+
if (!doc) throw new NotFoundException('Document not found');
19+
if (doc.ownerId !== userId) throw new ForbiddenException();
20+
if (doc.riskScore == null) throw new UnprocessableEntityException('Document has not been risk-assessed yet');
21+
const riskLevel = doc.riskScore < 30 ? 'LOW' : doc.riskScore < 70 ? 'MEDIUM' : 'HIGH';
22+
const flagDetails = (doc.riskFlags ?? []).map((flag) => ({
23+
flag,
24+
...(FLAG_DESCRIPTIONS[flag] ?? { description: flag, action: 'Review this flag with an administrator.' }),
25+
}));
26+
return { documentId, title: doc.title, riskScore: doc.riskScore, riskLevel, flagDetails };
27+
}
28+
}

0 commit comments

Comments
 (0)