260314:1705 20260314:1700 Refactor Migration
Build and Deploy / deploy (push) Successful in 3m25s

This commit is contained in:
admin
2026-03-14 17:05:08 +07:00
parent 81f9609ca2
commit 0211f01aa8
10 changed files with 350 additions and 261 deletions
@@ -0,0 +1,74 @@
import {
IsString,
IsNotEmpty,
IsOptional,
IsNumber,
IsBoolean,
IsArray,
} from 'class-validator';
export class EnqueueMigrationDto {
@IsString()
@IsNotEmpty()
document_number!: string;
@IsString()
@IsOptional()
title?: string;
@IsString()
@IsOptional()
original_title?: string;
@IsString()
@IsOptional()
category?: string;
@IsString()
@IsOptional()
ai_summary?: string;
@IsNumber()
@IsOptional()
project_id?: number;
@IsNumber()
@IsOptional()
sender_org_id?: number;
@IsNumber()
@IsOptional()
receiver_org_id?: number;
@IsString()
@IsOptional()
issued_date?: string;
@IsString()
@IsOptional()
received_date?: string;
@IsString()
@IsOptional()
remarks?: string;
@IsArray()
@IsOptional()
extracted_tags?: any[];
@IsNumber()
@IsOptional()
temp_attachment_id?: number;
@IsBoolean()
@IsOptional()
is_valid?: boolean;
@IsNumber()
@IsOptional()
confidence?: number;
@IsArray()
@IsOptional()
ai_issues?: any[];
}
@@ -20,8 +20,12 @@ export class ImportCorrespondenceDto {
category!: string;
@IsString()
@IsNotEmpty()
source_file_path!: string;
@IsOptional()
source_file_path?: string;
@IsNumber()
@IsOptional()
temp_attachment_id?: number;
@IsNumber()
@IsOptional()
@@ -56,6 +56,33 @@ export class MigrationReviewQueue {
@Column({ name: 'reviewed_at', type: 'timestamp', nullable: true })
reviewedAt?: Date;
@Column({ name: 'project_id', type: 'int', nullable: true })
projectId?: number;
@Column({ name: 'sender_organization_id', type: 'int', nullable: true })
senderOrganizationId?: number;
@Column({ name: 'receiver_organization_id', type: 'int', nullable: true })
receiverOrganizationId?: number;
@Column({ name: 'received_date', type: 'date', nullable: true })
receivedDate?: Date;
@Column({ name: 'issued_date', type: 'date', nullable: true })
issuedDate?: Date;
@Column({ type: 'text', nullable: true })
remarks?: string;
@Column({ name: 'ai_summary', type: 'text', nullable: true })
aiSummary?: string;
@Column({ name: 'extracted_tags', type: 'json', nullable: true })
extractedTags?: any;
@Column({ name: 'temp_attachment_id', type: 'int', nullable: true })
tempAttachmentId?: number;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
}
@@ -1,6 +1,7 @@
import { Controller, Post, Body, Headers, UseGuards, Get, Param, Query, Res, ParseIntPipe } from '@nestjs/common';
import { MigrationService } from './migration.service';
import { ImportCorrespondenceDto } from './dto/import-correspondence.dto';
import { EnqueueMigrationDto } from './dto/enqueue-migration.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiHeader, ApiQuery, ApiParam } from '@nestjs/swagger';
@@ -30,6 +31,13 @@ export class MigrationController {
return this.migrationService.importCorrespondence(dto, idempotencyKey, userId);
}
@Post('queue')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Enqueue a record into the staging migration review queue' })
async enqueueRecord(@Body() dto: EnqueueMigrationDto) {
return this.migrationService.enqueueRecord(dto);
}
@Get('queue')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Get migration review queue' })
@@ -8,6 +8,7 @@ import {
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { ImportCorrespondenceDto } from './dto/import-correspondence.dto';
import { EnqueueMigrationDto } from './dto/enqueue-migration.dto';
import { ImportTransaction } from './entities/import-transaction.entity';
import { Correspondence } from '../correspondence/entities/correspondence.entity';
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
@@ -21,6 +22,7 @@ import {
} from './entities/migration-review-queue.entity';
import { MigrationError } from './entities/migration-error.entity';
import { MigrationQueueQueryDto } from './dto/migration-queue-query.dto';
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
import { createReadStream, existsSync } from 'fs';
import * as path from 'path';
@Injectable()
@@ -177,7 +179,20 @@ export class MigrationService {
// 4. File Handling
let attachmentId: number | null = null;
if (dto.source_file_path) {
if (dto.temp_attachment_id) {
attachmentId = dto.temp_attachment_id;
try {
// Mark attachment as permanent
await queryRunner.manager.update(
Attachment,
{ id: attachmentId },
{ isTemporary: false }
);
} catch (fileError: unknown) {
const errMsg = fileError instanceof Error ? fileError.message : String(fileError);
this.logger.warn(`Failed to update temp_file [id:${attachmentId}]: ${errMsg}`);
}
} else if (dto.source_file_path) {
try {
const attachment = await this.fileStorageService.importStagingFile(
dto.source_file_path,
@@ -360,6 +375,63 @@ export class MigrationService {
await queryRunner.release();
}
}
async enqueueRecord(dto: EnqueueMigrationDto) {
if (!dto.document_number) {
throw new BadRequestException('document_number is required');
}
// Determine status based on confidence policy in ADR-017
let autoStatus = MigrationReviewStatus.PENDING;
if (dto.is_valid === false || (dto.confidence != null && dto.confidence < 0.60)) {
autoStatus = MigrationReviewStatus.REJECTED;
}
// Upsert or create new queue item
let queueItem = await this.reviewQueueRepo.findOne({
where: { documentNumber: dto.document_number },
});
if (!queueItem) {
queueItem = this.reviewQueueRepo.create({
documentNumber: dto.document_number,
});
}
queueItem.title = dto.title;
queueItem.originalTitle = dto.original_title;
queueItem.aiSuggestedCategory = dto.category;
queueItem.aiConfidence = dto.confidence;
queueItem.aiIssues = dto.ai_issues;
queueItem.projectId = dto.project_id;
queueItem.senderOrganizationId = dto.sender_org_id;
queueItem.receiverOrganizationId = dto.receiver_org_id;
queueItem.remarks = dto.remarks;
queueItem.aiSummary = dto.ai_summary;
queueItem.extractedTags = dto.extracted_tags;
queueItem.tempAttachmentId = dto.temp_attachment_id;
queueItem.status = autoStatus;
if (dto.issued_date) {
const parsed = new Date(dto.issued_date);
if (!isNaN(parsed.getTime())) queueItem.issuedDate = parsed;
}
if (dto.received_date) {
const parsed = new Date(dto.received_date);
if (!isNaN(parsed.getTime())) queueItem.receivedDate = parsed;
}
await this.reviewQueueRepo.save(queueItem);
this.logger.log(`Enqueued document [${dto.document_number}] to staging queue with status [${autoStatus}]`);
return {
message: 'Document enqueued successfully',
id: queueItem.id,
status: autoStatus,
};
}
async getReviewQueue(query: MigrationQueueQueryDto) {
const { page = 1, limit = 10, status } = query;
const skip = (page - 1) * limit;