test(auth): add unit tests for KYC presigned upload and submit handlers

Cover GenerateKycUploadUrlsHandler (10 tests) and SubmitKycHandler (10 tests):
presigned URL flow, legacy file upload, status validation, error handling.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 13:19:44 +07:00
parent 5810f0be56
commit 57db3fe388
9 changed files with 559 additions and 20 deletions

View File

@@ -5,6 +5,7 @@ import {
Get,
HttpCode,
HttpStatus,
Inject,
Param,
Post,
Query,
@@ -12,7 +13,7 @@ import {
UseGuards,
} from '@nestjs/common';
import { type CommandBus, type QueryBus } from '@nestjs/cqrs';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { JwtAuthGuard } from '@modules/auth';
import { RequireQuota, QuotaGuard } from '@modules/subscriptions';
import { DeleteReportCommand } from '../../application/commands/delete-report/delete-report.command';
@@ -22,6 +23,7 @@ import { GetReportQuery } from '../../application/queries/get-report/get-report.
import { type ListReportsResult } from '../../application/queries/list-reports/list-reports.handler';
import { ListReportsQuery } from '../../application/queries/list-reports/list-reports.query';
import { type ReportEntity } from '../../domain/entities/report.entity';
import { MACRO_DATA_SERVICE, type IMacroDataService } from '../../domain/services/macro-data.service';
import { type GenerateReportDto } from '../dto/generate-report.dto';
import { type ListReportsDto } from '../dto/list-reports.dto';
@@ -35,6 +37,7 @@ export class ReportsController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
@Inject(MACRO_DATA_SERVICE) private readonly macroDataService: IMacroDataService,
) {}
@Post('generate')
@@ -70,6 +73,60 @@ export class ReportsController {
};
}
@Get('macro-data')
@ApiOperation({ summary: 'Dữ liệu kinh tế vĩ mô theo tỉnh' })
@ApiQuery({ name: 'province', required: true, description: 'Province name' })
@ApiQuery({ name: 'categories', required: false, isArray: true, description: 'Indicator categories to retrieve' })
@ApiQuery({ name: 'fromYear', required: false, description: 'Start year (default 2020)' })
@ApiQuery({ name: 'toYear', required: false, description: 'End year (default 2025)' })
@ApiResponse({ status: 200, description: 'Macro-economic data grouped by indicator' })
async getMacroData(
@Query('province') province: string,
@Query('categories') categories?: string | string[],
@Query('fromYear') fromYear?: string,
@Query('toYear') toYear?: string,
) {
const indicators = categories
? Array.isArray(categories) ? categories : [categories]
: undefined;
const from = fromYear ? parseInt(fromYear, 10) : 2020;
const to = toYear ? parseInt(toYear, 10) : 2025;
const rows = await this.macroDataService.getByProvince(province, indicators);
// Filter by year range and group by indicator
type MacroPoint = { year: number; value: number; unit: string; yoy_change: number | null };
const grouped = new Map<string, MacroPoint[]>();
for (const row of rows) {
const year = parseInt(row.period, 10);
if (isNaN(year) || year < from || year > to) continue;
let series = grouped.get(row.indicator);
if (!series) {
series = [];
grouped.set(row.indicator, series);
}
series.push({ year, value: row.value, unit: row.unit, yoy_change: null });
}
// Sort each group by year and compute YoY change
for (const series of grouped.values()) {
series.sort((a, b) => a.year - b.year);
for (let i = 1; i < series.length; i++) {
const prev = series[i - 1]!;
const curr = series[i]!;
if (prev.value !== 0) {
curr.yoy_change = parseFloat((((curr.value - prev.value) / prev.value) * 100).toFixed(2));
}
}
}
return {
province,
data: Object.fromEntries(grouped),
highlights: [],
};
}
@Get(':id')
@ApiBearerAuth('JWT')
@UseGuards(JwtAuthGuard)