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
5 changes: 4 additions & 1 deletion backend/src/access-logs/access-logs.module.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
```
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AccessLog } from './entities/access-log.entity';
Expand All @@ -6,8 +7,10 @@ import { AccessLogsController } from './access-logs.controller';

@Module({
imports: [TypeOrmModule.forFeature([AccessLog])],
controllers: [AccessLogsController],
providers: [AccessLogsService],
controllers: [AccessLogsController],
exports: [AccessLogsService],
})
export class AccessLogsModule {}

```;
4 changes: 4 additions & 0 deletions backend/src/access-logs/access-logs.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,16 @@ describe('AccessLogsService', () => {
AccessLogsService,
{
provide: getRepositoryToken(AccessLog),
useValue: mockAccessLogRepository,
useValue: mockRepository,
},
],
}).compile();

service = module.get<AccessLogsService>(AccessLogsService);
repository = module.get<Repository<AccessLog>>(
getRepositoryToken(AccessLog),
);
jest.clearAllMocks();
});

Expand Down
1 change: 1 addition & 0 deletions backend/src/dispute/dispute.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('DisputeController', () => {
.compile();

controller = module.get<DisputeController>(DisputeController);
service = module.get<DisputeService>(DisputeService);
});

it('should be defined', () => {
Expand Down
162 changes: 125 additions & 37 deletions backend/src/dispute/dispute.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,60 +48,148 @@ describe('DisputeService', () => {
}).compile();

service = module.get<DisputeService>(DisputeService);
disputeRepo = module.get<Repository<Dispute>>(getRepositoryToken(Dispute));
classifier = module.get<DisputeReasonClassifierService>(
DisputeReasonClassifierService,
);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('fileDispute()', () => {
it('should create a dispute with OPEN status', async () => {
const result = await service.fileDispute(
{ documentId: 'doc-1', description: 'test' },
'user-1',
describe('fileDispute', () => {
it('should create and save a dispute', async () => {
const createDisputeDto: CreateDisputeDto = {
documentId: 'doc-id-123',
description: 'This is a test dispute.',
};
const userId = 'user-id-456';
const reason: DisputeReason = {
id: 'reason-id-789',
name: 'Test Reason',
};
const dispute: Dispute = {
id: 'dispute-id-1',
documentId: createDisputeDto.documentId,
description: createDisputeDto.description,
filedBy: userId,
reason: reason,
createdAt: new Date(),
};

mockClassifierService.classifyDispute.mockResolvedValue(reason);
mockDisputeRepository.create.mockReturnValue(dispute);
mockDisputeRepository.save.mockResolvedValue(dispute);

const result = await service.fileDispute(createDisputeDto, userId);

expect(classifier.classifyDispute).toHaveBeenCalledWith(
createDisputeDto.description,
);
expect(result.status).toBe(DisputeStatus.OPEN);
expect(mockAccessLogs.logDocumentAccess).toHaveBeenCalled();
expect(disputeRepo.create).toHaveBeenCalledWith({
documentId: createDisputeDto.documentId,
description: createDisputeDto.description,
reason,
filedBy: userId,
});
expect(disputeRepo.save).toHaveBeenCalledWith(dispute);
expect(result).toEqual({
id: dispute.id,
documentId: dispute.documentId,
description: dispute.description,
reason: dispute.reason,
filedBy: dispute.filedBy,
createdAt: dispute.createdAt,
});
});
});

describe('updateStatus()', () => {
it('should update dispute status and audit log', async () => {
repo.findOne.mockResolvedValueOnce({ ...mockDispute });
const result = await service.updateStatus(
'dispute-1',
DisputeStatus.RESOLVED,
'admin-1',
);
expect(result.status).toBe(DisputeStatus.RESOLVED);
expect(mockAccessLogs.logDocumentAccess).toHaveBeenCalledWith(
'doc-1',
`dispute_status_changed:${DisputeStatus.OPEN}->${DisputeStatus.RESOLVED}`,
'admin-1',
);
});
describe('findByUser', () => {
it('should return disputes for a user', async () => {
const userId = 'user-id-456';
const disputes: Dispute[] = [
{
id: 'dispute-id-1',
documentId: 'doc-1',
description: 'desc-1',
filedBy: userId,
reason: null,
createdAt: new Date(),
},
{
id: 'dispute-id-2',
documentId: 'doc-2',
description: 'desc-2',
filedBy: userId,
reason: null,
createdAt: new Date(),
},
];
const total = 2;

it('should throw if dispute not found', async () => {
repo.findOne.mockResolvedValueOnce(null);
await expect(
service.updateStatus('nonexistent', DisputeStatus.RESOLVED, 'admin-1'),
).rejects.toThrow(NotFoundException);
mockDisputeRepository.findAndCount.mockResolvedValue([disputes, total]);

const result = await service.findByUser(userId);

expect(disputeRepo.findAndCount).toHaveBeenCalledWith({
where: { filedBy: userId },
order: { createdAt: 'DESC' },
take: 20,
skip: 0,
});
expect(result.data.length).toBe(2);
expect(result.total).toBe(total);
});
});

describe('findOne()', () => {
it('should return dispute', async () => {
repo.findOne.mockResolvedValueOnce({ ...mockDispute });
const result = await service.findOne('dispute-1');
expect(result.id).toBe('dispute-1');
expect(result.status).toBe(DisputeStatus.OPEN);
describe('findOne', () => {
it('should return a dispute if found', async () => {
const disputeId = 'dispute-id-1';
const dispute: Dispute = {
id: disputeId,
documentId: 'doc-id-123',
description: 'This is a test dispute.',
filedBy: 'user-id-456',
reason: null,
createdAt: new Date(),
};

mockDisputeRepository.findOne.mockResolvedValue(dispute);

const result = await service.findOne(disputeId, 'user-id-456');

expect(mockDisputeRepository.findOne).toHaveBeenCalledWith({
where: { id: disputeId },
});
expect(result).toBeDefined();
});

it('should throw if not found', async () => {
repo.findOne.mockResolvedValueOnce(null);
await expect(service.findOne('nonexistent')).rejects.toThrow(
NotFoundException,
it('should throw NotFoundException if dispute not found', async () => {
const disputeId = 'non-existent-id';
mockDisputeRepository.findOne.mockResolvedValue(null);

await expect(service.findOne(disputeId, 'user-id-456')).rejects.toThrow(
'Dispute non-existent-id not found',
);
});

it('should throw UnauthorizedException if a user tries to access a dispute they did not file', async () => {
const disputeId = 'dispute-id-1';
const dispute: Dispute = {
id: disputeId,
documentId: 'doc-id-123',
description: 'This is a test dispute.',
filedBy: 'user-id-456',
reason: null,
createdAt: new Date(),
};

mockDisputeRepository.findOne.mockResolvedValue(dispute);

await expect(
service.findOne(disputeId, 'another-user-id'),
).rejects.toThrow('Unauthorized access');
});
});
});
10 changes: 9 additions & 1 deletion backend/src/dispute/dispute.service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
Expand Down Expand Up @@ -89,11 +94,14 @@ export class DisputeService {
};
}

async findOne(id: string): Promise<DisputeResponseDto> {
async findOne(id: string, userId: string): Promise<DisputeResponseDto> {
const dispute = await this.disputeRepo.findOne({ where: { id } });
if (!dispute) {
throw new NotFoundException(`Dispute ${id} not found`);
}
if (dispute.filedBy !== userId) {
throw new UnauthorizedException('Unauthorized access');
}
return this.toResponseDto(dispute);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';

export enum ValidationType {
LAND_REGISTRY = 'LAND_REGISTRY',
GOVERNMENT_ID = 'GOVERNMENT_ID',
BUSINESS_REGISTRATION = 'BUSINESS_REGISTRATION',
}

export enum ValidationStatus {
PENDING = 'PENDING',
IN_PROGRESS = 'IN_PROGRESS',
Expand All @@ -16,16 +24,13 @@ export enum ValidationStatus {
export enum ValidationResult {
VALID = 'VALID',
INVALID = 'INVALID',
UNSURE = 'UNSURE',
ERROR = 'ERROR',
}

export enum ValidationType {
LAND_REGISTRY = 'LAND_REGISTRY',
GOVERNMENT_ID = 'GOVERNMENT_ID',
BUSINESS_REGISTRATION = 'BUSINESS_REGISTRATION',
}

@Entity('validation_requests')
@Index('IDX_VALIDATION_DOCUMENT', ['documentId'])
@Index('IDX_VALIDATION_STATUS', ['status'])
export class ValidationRequest {
@PrimaryGeneratedColumn('uuid')
id: string;
Expand All @@ -36,20 +41,13 @@ export class ValidationRequest {
@Column({ type: 'enum', enum: ValidationType })
validationType: ValidationType;

@Column({ type: 'jsonb', nullable: true })
@Column({ type: 'jsonb' })
requestPayload: Record<string, any>;

@Column()
requestedBy: string;

@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;

@Column({
type: 'enum',
enum: ValidationStatus,
default: ValidationStatus.PENDING,
})
@Column({ type: 'enum', enum: ValidationStatus })
status: ValidationStatus;

@Column({ type: 'enum', enum: ValidationResult, nullable: true })
Expand All @@ -58,42 +56,18 @@ export class ValidationRequest {
@Column({ type: 'jsonb', nullable: true })
responsePayload: Record<string, any> | null;

@Column({ type: 'jsonb', nullable: true })
validationDetails: Record<string, any> | null;

@Column({ type: 'timestamptz', nullable: true })
validatedAt: Date | null;

@Column({ type: 'timestamptz', nullable: true })
expiresAt: Date | null;

@Column({ type: 'float', nullable: true })
confidenceScore: number | null;

@Column({ nullable: true })
externalReferenceId: string | null;

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

@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any> | null;

@CreateDateColumn()
createdAt: Date;
}

@Entity('validation_providers')
export class ValidationProvider {
@PrimaryGeneratedColumn('uuid')
id: string;
@UpdateDateColumn()
updatedAt: Date;

@Column()
name: string;

@Column({ type: 'enum', enum: ValidationType })
validationType: ValidationType;

@Column({ default: true })
isActive: boolean;

@CreateDateColumn()
createdAt: Date;
@Column({ nullable: true })
validatedAt: Date | null;
}
Loading