feat(migration): ADR-028 migration architecture refactor

- เพิ่ม POST /api/ai/jobs + GET /api/ai/jobs/:jobId endpoints (FR-001, FR-002)
- เพิ่ม BullMQ Worker MigrateDocumentWorker + OCR auto-detect (FR-003, FR-004)
- เพิ่ม cleanup-temp-files + expire-pending-reviews workers (FR-005, FR-005a/b)
- สร้าง SQL deltas: tags, correspondence_tags, alter migration_review_queue (FR-006, ADR-009)
- เพิ่ม MigrationReviewService.commitRecord() + SELECT FOR UPDATE (FR-007, FR-007a)
- เพิ่ม CASL permission migration.commit + MigrationReviewController (FR-007)
- สร้าง TagsModule + TagsService + TagsController (US3)
- สร้าง Migration Review Queue frontend page + ReviewQueueTable (US2)
- อัปเดต n8n guide: deterministic Idempotency-Key + token pre-flight (FR-001a, FR-010a/b)
- สร้าง spec.md, plan.md, tasks.md, data-model.md, contracts/, quickstart.md
- สร้าง ADR-028 document + validation-report.md (PASS 32/32 tasks, 173/173 tests)
This commit is contained in:
2026-05-22 17:10:07 +07:00
parent 990d80e16d
commit a2973be208
55 changed files with 4256 additions and 107 deletions
@@ -26,6 +26,7 @@ interface MigrationAiIssues {
sourceFilePath?: string;
keyPoints?: string[];
validationResults?: Array<{ message: string; severity: string }>;
tags?: string[];
}
const reviewFormSchema = z.object({
@@ -101,11 +102,9 @@ export default function MigrationReviewPage() {
const onSubmit = async (values: ReviewFormValues) => {
if (!item) return;
try {
setSubmitting(true);
const issues = item.aiIssues || {};
const issues = (item.aiIssues || {}) as unknown as MigrationAiIssues;
const payload = {
documentNumber: values.documentNumber,
subject: values.subject,
@@ -113,7 +112,7 @@ export default function MigrationReviewPage() {
sourceFilePath: issues.sourceFilePath || '',
migratedBy: 'SYSTEM_IMPORT',
batchId: 'MANUAL_REVIEW_BATCH',
projectId: 1, // Assumption or pulled from store
projectId: 1,
documentDate: values.documentDate,
issuedDate: values.issuedDate,
receivedDate: values.receivedDate,
@@ -124,15 +123,12 @@ export default function MigrationReviewPage() {
aiConfidence: item.aiConfidence,
},
};
if (!item?.id) {
toast.error('Invalid item ID');
return;
}
// Mock idempotency key based on timestamp to ensure uniqueness per approval retry
const idempotencyKey = `review-${item.id}-${Date.now()}`;
await migrationService.approveQueueItem(item.id, payload, idempotencyKey);
toast.success('Document approved and imported successfully');
router.push('/admin/migration');
} catch (error: unknown) {