test(frontend): raise overall statement coverage to 30.42% for Phase 1 MVP

This commit is contained in:
2026-06-13 22:33:11 +07:00
parent 190b9a3af5
commit 9c5df0abdb
37 changed files with 6128 additions and 24 deletions
+272
View File
@@ -0,0 +1,272 @@
// File: frontend/lib/api/__tests__/client.test.ts
// Change Log:
// - 2026-06-13: Refactor to use static imports instead of require, fixing ESM module resolution errors
// - 2026-06-13: Unmock @/lib/api/client to test the real implementation
// - 2026-06-13: Invoke actual response interceptor handlers for event and redirect assertions
// - 2026-06-13: Capture rejectedHandler at module scope before beforeEach clears mock history
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
clearAuthTokenCache,
parseApiError,
AI_FEATURES_UNAVAILABLE_EVENT,
getAuthToken,
} from '../client';
import { getSession } from 'next-auth/react';
import apiClient from '@/lib/api/client';
// Unmock the api client so we test the actual implementation
vi.unmock('@/lib/api/client');
vi.unmock('../client');
// Mock axios
vi.mock('axios', () => ({
default: {
create: vi.fn(() => ({
interceptors: {
request: {
use: vi.fn(),
},
response: {
use: vi.fn(),
},
},
})),
},
}));
// Mock uuid
vi.mock('uuid', () => ({
v4: vi.fn(() => 'mock-uuid-123'),
}));
// Mock next-auth
vi.mock('next-auth/react', () => ({
getSession: vi.fn(),
}));
// Capture the rejectedHandler at module scope
const rejectedHandler = (apiClient.interceptors.response.use as any).mock.calls[0][1];
describe('apiClient', () => {
beforeEach(() => {
vi.clearAllMocks();
clearAuthTokenCache();
});
afterEach(() => {
clearAuthTokenCache();
});
describe('Token Caching', () => {
it('should cache token from getSession', async () => {
(getSession as any).mockResolvedValue({ accessToken: 'test-token' });
const token = await getAuthToken();
expect(token).toBe('test-token');
expect(getSession).toHaveBeenCalled();
});
it('should fallback to localStorage if getSession fails', async () => {
(getSession as any).mockRejectedValue(new Error('Session error'));
const mockLocalStorage = {
getItem: vi.fn(() => JSON.stringify({ state: { token: 'local-token' } })),
};
Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage,
writable: true,
});
const token = await getAuthToken();
expect(token).toBe('local-token');
});
it('should return null if all token methods fail', async () => {
(getSession as any).mockRejectedValue(new Error('Session error'));
const mockLocalStorage = {
getItem: vi.fn(() => null),
};
Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage,
writable: true,
});
const token = await getAuthToken();
expect(token).toBeNull();
});
it('should use cached token on subsequent calls', async () => {
(getSession as any).mockResolvedValue({ accessToken: 'test-token' });
await getAuthToken();
const token2 = await getAuthToken();
expect(getSession).toHaveBeenCalledTimes(1);
expect(token2).toBe('test-token');
});
});
describe('clearAuthTokenCache', () => {
it('should clear cached token', async () => {
(getSession as any).mockResolvedValue({ accessToken: 'test-token' });
await getAuthToken();
clearAuthTokenCache();
await getAuthToken();
expect(getSession).toHaveBeenCalledTimes(2);
});
});
describe('parseApiError', () => {
it('should parse ADR-007 structured error', () => {
const axiosError = {
response: {
data: {
error: {
type: 'VALIDATION',
code: 'INVALID_INPUT',
message: 'Invalid input',
severity: 'MEDIUM',
timestamp: '2026-01-01T00:00:00Z',
},
},
status: 400,
},
};
const result = parseApiError(axiosError as unknown as Parameters<typeof parseApiError>[0]);
expect(result.error.type).toBe('VALIDATION');
expect(result.error.code).toBe('INVALID_INPUT');
expect(result.error.statusCode).toBe(400);
});
it('should parse NestJS validation error', () => {
const axiosError = {
response: {
data: {
message: ['Field is required', 'Invalid format'],
},
status: 400,
},
};
const result = parseApiError(axiosError as unknown as Parameters<typeof parseApiError>[0]);
expect(result.error.type).toBe('VALIDATION');
expect(result.error.code).toBe('HTTP_ERROR');
expect(result.error.message).toBe('ข้อมูลที่กรอกไม่ถูกต้อง กรุณาตรวจสอบและลองใหม่');
expect(result.error.severity).toBe('MEDIUM');
});
it('should parse NestJS validation error with string message', () => {
const axiosError = {
response: {
data: {
message: 'Single error message',
},
status: 400,
},
};
const result = parseApiError(axiosError as unknown as Parameters<typeof parseApiError>[0]);
expect(result.error.message).toBe('Single error message');
});
it('should parse network error', () => {
const axiosError = {
response: undefined,
};
const result = parseApiError(axiosError as unknown as Parameters<typeof parseApiError>[0]);
expect(result.error.type).toBe('INFRASTRUCTURE');
expect(result.error.code).toBe('NETWORK_ERROR');
expect(result.error.message).toBe('ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้');
});
it('should parse 5xx error as HIGH severity', () => {
const axiosError = {
response: {
data: {
message: 'Server error',
},
status: 500,
},
};
const result = parseApiError(axiosError as unknown as Parameters<typeof parseApiError>[0]);
expect(result.error.severity).toBe('HIGH');
});
it('should fallback to unknown error', () => {
const axiosError = {
response: {
data: {},
status: 418,
},
};
const result = parseApiError(axiosError as unknown as Parameters<typeof parseApiError>[0]);
expect(result.error.type).toBe('INTERNAL_ERROR');
expect(result.error.code).toBe('UNKNOWN_ERROR');
});
});
describe('AI Features Unavailable Event', () => {
it('should dispatch AI_FEATURES_UNAVAILABLE_EVENT on 503 error', async () => {
const mockDispatchEvent = vi.fn();
Object.defineProperty(window, 'dispatchEvent', {
value: mockDispatchEvent,
writable: true,
});
const axiosError = {
response: {
data: {
error: {
type: 'INFRASTRUCTURE',
code: 'AI_FEATURES_UNAVAILABLE',
message: 'AI features unavailable',
severity: 'HIGH',
timestamp: '2026-01-01T00:00:00Z',
},
},
status: 503,
},
};
await rejectedHandler(axiosError).catch(() => {});
const result = parseApiError(axiosError as any);
expect(mockDispatchEvent).toHaveBeenCalledWith(
new CustomEvent(AI_FEATURES_UNAVAILABLE_EVENT, {
detail: result.error,
})
);
});
it('should not dispatch event for non-503 errors', async () => {
const mockDispatchEvent = vi.fn();
Object.defineProperty(window, 'dispatchEvent', {
value: mockDispatchEvent,
writable: true,
});
const axiosError = {
response: {
data: {
error: {
type: 'VALIDATION',
code: 'INVALID_INPUT',
message: 'Invalid input',
severity: 'MEDIUM',
timestamp: '2026-01-01T00:00:00Z',
},
},
status: 400,
},
};
await rejectedHandler(axiosError).catch(() => {});
expect(mockDispatchEvent).not.toHaveBeenCalled();
});
});
describe('401 Handling', () => {
it('should redirect to login on 401 error', async () => {
const mockLocation = { href: '' };
Object.defineProperty(window, 'location', {
value: mockLocation,
writable: true,
});
const axiosError = {
response: {
status: 401,
},
};
await rejectedHandler(axiosError).catch(() => {});
expect(mockLocation.href).toBe('/login');
});
});
});
@@ -0,0 +1,91 @@
// File: frontend/lib/services/__tests__/circulation.service.test.ts
// Change Log:
// - 2026-06-13: Initial creation - unit tests for circulationService
import { describe, it, expect, vi, beforeEach } from 'vitest';
import apiClient from '@/lib/api/client';
import { circulationService } from '../circulation.service';
// Mock apiClient
vi.mock('@/lib/api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
},
}));
describe('circulationService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getAll', () => {
it('ควรดึงรายการ Circulation ทั้งหมดพร้อม params', async () => {
const mockResponse = { data: [{ publicId: 'uuid-1', subject: 'Circulation A' }] };
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const searchParams = { search: 'Circ' };
const result = await circulationService.getAll(searchParams);
expect(apiClient.get).toHaveBeenCalledWith('/circulations', { params: searchParams });
expect(result).toEqual(mockResponse.data);
});
});
describe('getByUuid', () => {
it('ควรดึงรายละเอียด Circulation ตาม uuid', async () => {
const mockResponse = { data: { publicId: 'uuid-1', subject: 'Circulation A' } };
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await circulationService.getByUuid('uuid-1');
expect(apiClient.get).toHaveBeenCalledWith('/circulations/uuid-1');
expect(result).toEqual(mockResponse.data);
});
});
describe('create', () => {
it('ควรสร้าง Circulation ใหม่', async () => {
const mockResponse = { data: { publicId: 'uuid-1', subject: 'Circulation A' } };
vi.mocked(apiClient.post).mockResolvedValue(mockResponse);
const createDto = {
correspondenceId: 'uuid-corr',
recipientUserIds: ['uuid-user'],
};
const result = await circulationService.create(createDto as any);
expect(apiClient.post).toHaveBeenCalledWith('/circulations', createDto);
expect(result).toEqual(mockResponse.data);
});
});
describe('updateRouting', () => {
it('ควรปรับปรุงข้อมูลการ Routing', async () => {
const mockResponse = { data: { success: true } };
vi.mocked(apiClient.patch).mockResolvedValue(mockResponse);
const routingDto = { action: 'ACKNOWLEDGE', comments: 'Seen.' };
const result = await circulationService.updateRouting('uuid-1', routingDto as any);
expect(apiClient.patch).toHaveBeenCalledWith('/circulations/uuid-1/routing', routingDto);
expect(result).toEqual(mockResponse.data);
});
});
describe('getByCorrespondenceUuid', () => {
it('ควรดึงรายการ Circulation โดยอิงตาม correspondence uuid', async () => {
const mockResponse = { data: [{ publicId: 'uuid-1', subject: 'Circulation A' }] };
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await circulationService.getByCorrespondenceUuid('uuid-corr-1');
expect(apiClient.get).toHaveBeenCalledWith('/circulations', {
params: { correspondencePublicId: 'uuid-corr-1', limit: 50 },
});
expect(result).toEqual(mockResponse.data);
});
});
describe('delete', () => {
it('ควรลบ Circulation', async () => {
const mockResponse = { data: { success: true } };
vi.mocked(apiClient.delete).mockResolvedValue(mockResponse);
const result = await circulationService.delete('uuid-1');
expect(apiClient.delete).toHaveBeenCalledWith('/circulations/uuid-1');
expect(result).toEqual(mockResponse.data);
});
});
});
@@ -0,0 +1,148 @@
// File: frontend/lib/services/__tests__/dashboard.service.test.ts
// Change Log:
// - 2026-06-13: Initial creation - unit tests for dashboardService
import { describe, it, expect, vi, beforeEach } from 'vitest';
import apiClient from '@/lib/api/client';
import { dashboardService } from '../dashboard.service';
// Mock apiClient
vi.mock('@/lib/api/client', () => ({
default: {
get: vi.fn(),
},
}));
describe('dashboardService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getStats', () => {
it('ควรดึงข้อมูลสถิติของแดชบอร์ดสำเร็จ', async () => {
const mockResponse = { data: { totalDocuments: 100, pendingApprovals: 5 } };
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await dashboardService.getStats('uuid-proj-1');
expect(apiClient.get).toHaveBeenCalledWith('/dashboard/stats', { params: { projectId: 'uuid-proj-1' } });
expect(result).toEqual(mockResponse.data);
});
it('ควรดึงข้อมูลสถิติโดยไม่ต้องส่ง projectId', async () => {
const mockResponse = { data: { totalDocuments: 200 } };
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await dashboardService.getStats();
expect(apiClient.get).toHaveBeenCalledWith('/dashboard/stats', { params: undefined });
expect(result).toEqual(mockResponse.data);
});
});
describe('getRecentActivity', () => {
it('ควรดึงประวัติการเคลื่อนไหวล่าสุดและจัดรูปแบบให้ถูกต้อง', async () => {
const mockResponse = {
data: [
{
id: 'act-1',
action: 'CREATE',
entityType: 'RFA',
entityId: 'uuid-rfa',
details: { description: 'สร้างเอกสาร RFA ใหม่' },
createdAt: '2026-01-01T00:00:00Z',
user: { firstName: 'สมชาย', lastName: 'รักดี', username: 'somchai' },
},
{
id: 'act-2',
action: 'UPDATE',
entityType: 'Transmittal',
entityId: 'uuid-trans',
createdAt: '2026-01-01T00:00:00Z',
user: { username: 'testuser' },
},
{
id: 'act-3',
action: 'DELETE',
createdAt: '2026-01-01T00:00:00Z',
},
],
};
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await dashboardService.getRecentActivity('uuid-proj-1');
expect(result).toHaveLength(3);
expect(result[0].user.name).toBe('สมชาย รักดี');
expect(result[0].user.initials).toBe('สร');
expect(result[0].description).toBe('สร้างเอกสาร RFA ใหม่');
expect(result[0].targetUrl).toBe('/rfas/uuid-rfa');
expect(result[1].user.name).toBe('testuser');
expect(result[1].user.initials).toBe('T');
expect(result[1].description).toBe('UPDATE Transmittal uuid-trans');
expect(result[1].targetUrl).toBe('/transmittals/uuid-trans');
expect(result[2].user.name).toBe('System');
expect(result[2].user.initials).toBe('S');
expect(result[2].targetUrl).toBe('/correspondences/');
});
it('ควรคืนค่าเป็นอาเรย์ว่างเมื่อเกิดข้อผิดพลาดจาก API', async () => {
vi.mocked(apiClient.get).mockRejectedValue(new Error('API Failure'));
const result = await dashboardService.getRecentActivity();
expect(result).toEqual([]);
});
it('ควรคืนค่าเป็นอาเรย์ว่างเมื่อข้อมูลไม่ใช้รูปแบบอาเรย์', async () => {
vi.mocked(apiClient.get).mockResolvedValue({ data: { message: 'Not an array' } });
const result = await dashboardService.getRecentActivity();
expect(result).toEqual([]);
});
});
describe('getPendingTasks', () => {
it('ควรดึงข้อมูลงานที่ค้างและคำนวณจำนวนวันล่วงเลยกับความสำคัญ', async () => {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const fourDaysAgo = new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000);
const mockResponse = {
data: {
data: [
{
instanceId: 'inst-1',
workflowCode: 'WF-RFA',
currentState: 'REVIEWING',
entityType: 'RFA',
entityId: 'uuid-rfa-1',
documentNumber: 'RFA-001',
subject: 'งานด่วนพิเศษ',
assignedAt: oneDayAgo.toISOString(),
},
{
instanceId: 'inst-2',
workflowCode: 'WF-TR',
currentState: 'APPROVED',
entityType: 'Transmittal',
entityId: 'uuid-tr-1',
documentNumber: 'TR-001',
subject: '',
assignedAt: fourDaysAgo.toISOString(),
},
],
},
};
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await dashboardService.getPendingTasks('uuid-proj-1');
expect(result).toHaveLength(2);
expect(result[0].publicId).toBe('inst-1');
expect(result[0].title).toBe('งานด่วนพิเศษ');
expect(result[0].daysOverdue).toBe(1);
expect(result[0].priority).toBe('MEDIUM');
expect(result[0].url).toBe('/rfas/uuid-rfa-1');
expect(result[1].publicId).toBe('inst-2');
expect(result[1].title).toBe('TR-001');
expect(result[1].daysOverdue).toBe(4);
expect(result[1].priority).toBe('HIGH');
expect(result[1].url).toBe('/transmittals/uuid-tr-1');
});
it('ควรคืนค่าเป็นอาเรย์ว่างเมื่อเกิดข้อผิดพลาดจาก API', async () => {
vi.mocked(apiClient.get).mockRejectedValue(new Error('API Failure'));
const result = await dashboardService.getPendingTasks();
expect(result).toEqual([]);
});
});
});
@@ -0,0 +1,118 @@
// File: frontend/lib/services/__tests__/document-numbering.service.test.ts
// Change Log:
// - 2026-06-13: Refactor to use static imports instead of require, fixing ESM module resolution errors
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { documentNumberingService } from '../document-numbering.service';
import apiClient from '@/lib/api/client';
// Mock apiClient
vi.mock('@/lib/api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
},
}));
describe('documentNumberingService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('Admin Dashboard Metrics', () => {
it('should get metrics', async () => {
const mockMetrics = {
totalNumbers: 100,
activeReservations: 5,
recentActivity: [],
};
(apiClient.get as any).mockResolvedValue({ data: mockMetrics });
const result = await documentNumberingService.getMetrics();
expect(result).toEqual(mockMetrics);
expect(apiClient.get).toHaveBeenCalledWith('/admin/document-numbering/metrics');
});
});
describe('Admin Tools', () => {
it('should perform manual override', async () => {
const mockDto = {
documentNumber: 'DOC-001',
newSequence: 100,
reason: 'Manual override',
};
(apiClient.post as any).mockResolvedValue({ data: { success: true } });
await documentNumberingService.manualOverride(mockDto);
expect(apiClient.post).toHaveBeenCalledWith(
'/admin/document-numbering/manual-override',
mockDto
);
});
it('should void and replace number', async () => {
const mockDto = {
documentNumber: 'DOC-001',
reason: 'Void',
replace: true,
};
const mockResponse = { documentNumber: 'DOC-002' };
(apiClient.post as any).mockResolvedValue({ data: mockResponse });
const result = await documentNumberingService.voidAndReplace(mockDto);
expect(result).toEqual(mockResponse);
expect(apiClient.post).toHaveBeenCalledWith(
'/admin/document-numbering/void-and-replace',
mockDto
);
});
it('should cancel number', async () => {
const mockDto = {
documentNumber: 'DOC-001',
reason: 'Cancel',
projectId: 1,
};
(apiClient.post as any).mockResolvedValue({ data: { success: true } });
await documentNumberingService.cancelNumber(mockDto);
expect(apiClient.post).toHaveBeenCalledWith(
'/admin/document-numbering/cancel',
mockDto
);
});
it('should bulk import with FormData', async () => {
const mockFormData = new FormData();
mockFormData.append('file', new Blob(['test']), 'test.csv');
const mockResponse = { imported: 10, errors: [] };
(apiClient.post as any).mockResolvedValue({ data: mockResponse });
const result = await documentNumberingService.bulkImport(mockFormData);
expect(result).toEqual(mockResponse);
expect(apiClient.post).toHaveBeenCalledWith(
'/admin/document-numbering/bulk-import',
mockFormData,
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
});
it('should bulk import with array data', async () => {
const mockData = [
{ documentNumber: 'DOC-001', projectId: 1, sequenceNumber: 1 },
{ documentNumber: 'DOC-002', projectId: 1, sequenceNumber: 2 },
];
const mockResponse = { imported: 2, errors: [] };
(apiClient.post as any).mockResolvedValue({ data: mockResponse });
const result = await documentNumberingService.bulkImport(mockData);
expect(result).toEqual(mockResponse);
expect(apiClient.post).toHaveBeenCalledWith(
'/admin/document-numbering/bulk-import',
mockData,
{}
);
});
});
describe('Audit Logs', () => {
it('should get audit logs (currently returns empty)', async () => {
const result = await documentNumberingService.getAuditLogs();
expect(result).toEqual([]);
});
});
});
@@ -0,0 +1,105 @@
// File: frontend/lib/services/__tests__/rfa.service.test.ts
// Change Log:
// - 2026-06-13: Initial creation - unit tests for rfaService
import { describe, it, expect, vi, beforeEach } from 'vitest';
import apiClient from '@/lib/api/client';
import { rfaService } from '../rfa.service';
// Mock apiClient
vi.mock('@/lib/api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}));
describe('rfaService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getAll', () => {
it('ควรดึงรายการ RFA ทั้งหมดพร้อม params', async () => {
const mockResponse = { data: [{ publicId: 'uuid-1', subject: 'Test RFA' }] };
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const searchParams = { search: 'Test' };
const result = await rfaService.getAll(searchParams);
expect(apiClient.get).toHaveBeenCalledWith('/rfas', { params: searchParams });
expect(result).toEqual(mockResponse.data);
});
});
describe('getByUuid', () => {
it('ควรดึงรายละเอียด RFA ตาม uuid', async () => {
const mockResponse = { data: { publicId: 'uuid-1', subject: 'Test RFA' } };
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await rfaService.getByUuid('uuid-1');
expect(apiClient.get).toHaveBeenCalledWith('/rfas/uuid-1');
expect(result).toEqual(mockResponse.data);
});
});
describe('create', () => {
it('ควรสร้าง RFA ใหม่', async () => {
const mockResponse = { data: { publicId: 'uuid-1', subject: 'Test RFA' } };
vi.mocked(apiClient.post).mockResolvedValue(mockResponse);
const createDto = {
projectId: 'uuid-proj',
contractId: 'uuid-cont',
disciplineId: 'uuid-disp',
rfaTypeId: 'uuid-type',
subject: 'Test RFA',
toOrganizationId: 'uuid-org',
};
const result = await rfaService.create(createDto as any);
expect(apiClient.post).toHaveBeenCalledWith('/rfas', createDto);
expect(result).toEqual(mockResponse.data);
});
});
describe('submit', () => {
it('ควรส่ง RFA เข้า workflow', async () => {
const mockResponse = { data: { publicId: 'uuid-1', status: 'SUBMITTED' } };
vi.mocked(apiClient.post).mockResolvedValue(mockResponse);
const submitDto = { templateId: 1, reviewTeamPublicId: 'uuid-team' };
const result = await rfaService.submit('uuid-1', submitDto);
expect(apiClient.post).toHaveBeenCalledWith('/rfas/uuid-1/submit', submitDto);
expect(result).toEqual(mockResponse.data);
});
});
describe('update', () => {
it('ควรแก้ไขข้อมูล RFA', async () => {
const mockResponse = { data: { publicId: 'uuid-1', subject: 'Updated RFA' } };
vi.mocked(apiClient.put).mockResolvedValue(mockResponse);
const updateDto = { subject: 'Updated RFA' };
const result = await rfaService.update('uuid-1', updateDto);
expect(apiClient.put).toHaveBeenCalledWith('/rfas/uuid-1', updateDto);
expect(result).toEqual(mockResponse.data);
});
});
describe('processWorkflow', () => {
it('ควรดำเนินการขั้นตอนอนุมัติ (Workflow Action)', async () => {
const mockResponse = { data: { status: 'APPROVED' } };
vi.mocked(apiClient.post).mockResolvedValue(mockResponse);
const actionDto = { action: 'APPROVE', comments: 'Approved!' };
const result = await rfaService.processWorkflow('uuid-1', actionDto as any);
expect(apiClient.post).toHaveBeenCalledWith('/rfas/uuid-1/action', actionDto);
expect(result).toEqual(mockResponse.data);
});
});
describe('delete', () => {
it('ควรลบ RFA (Soft Delete)', async () => {
const mockResponse = { data: { success: true } };
vi.mocked(apiClient.delete).mockResolvedValue(mockResponse);
const result = await rfaService.delete('uuid-1');
expect(apiClient.delete).toHaveBeenCalledWith('/rfas/uuid-1');
expect(result).toEqual(mockResponse.data);
});
});
});
@@ -0,0 +1,154 @@
// File: frontend/lib/services/__tests__/session.service.test.ts
// Change Log:
// - 2026-06-13: Refactor to use static imports instead of require, fixing ESM module resolution errors
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { sessionService, extractArrayData, transformSession } from '../session.service';
import apiClient from '@/lib/api/client';
// Mock apiClient
vi.mock('@/lib/api/client', () => ({
default: {
get: vi.fn(),
delete: vi.fn(),
},
}));
describe('sessionService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getActiveSessions', () => {
it('should get active sessions from array response', async () => {
const mockSessions = [
{
id: 1,
userId: 1,
user: { username: 'testuser', firstName: 'Test', lastName: 'User' },
deviceName: 'Chrome',
ipAddress: '192.168.1.1',
lastActive: '2026-01-01T00:00:00Z',
isCurrent: true,
},
];
(apiClient.get as any).mockResolvedValue({ data: mockSessions });
const result = await sessionService.getActiveSessions();
expect(result).toHaveLength(1);
expect(result[0].id).toBe(1);
expect(result[0].user.username).toBe('testuser');
expect(apiClient.get).toHaveBeenCalledWith('/auth/sessions');
});
it('should get active sessions from nested data response', async () => {
const mockSessions = [
{
id: 2,
userId: 2,
user: { username: 'testuser2', firstName: 'Test2', lastName: 'User2' },
deviceName: 'Firefox',
ipAddress: '192.168.1.2',
lastActive: '2026-01-02T00:00:00Z',
isCurrent: false,
},
];
(apiClient.get as any).mockResolvedValue({ data: { data: mockSessions } });
const result = await sessionService.getActiveSessions();
expect(result).toHaveLength(1);
expect(result[0].id).toBe(2);
expect(apiClient.get).toHaveBeenCalledWith('/auth/sessions');
});
it('should handle string id in session and convert to number', async () => {
const mockSessions = [
{
id: '3',
userId: 3,
user: { username: 'testuser3', firstName: 'Test3', lastName: 'User3' },
deviceName: 'Safari',
ipAddress: '192.168.1.3',
lastActive: '2026-01-03T00:00:00Z',
isCurrent: false,
},
];
(apiClient.get as any).mockResolvedValue({ data: { data: { data: mockSessions } } });
const result = await sessionService.getActiveSessions();
expect(result).toHaveLength(1);
expect(result[0].id).toBe(3);
expect(typeof result[0].id).toBe('number');
});
it('should return empty array for non-array response', async () => {
(apiClient.get as any).mockResolvedValue({ data: 'not an array' });
const result = await sessionService.getActiveSessions();
expect(result).toEqual([]);
});
it('should return empty array for null response', async () => {
(apiClient.get as any).mockResolvedValue({ data: null });
const result = await sessionService.getActiveSessions();
expect(result).toEqual([]);
});
});
describe('revokeSession', () => {
it('should revoke session by id', async () => {
const mockResponse = { success: true };
(apiClient.delete as any).mockResolvedValue({ data: mockResponse });
const result = await sessionService.revokeSession(1);
expect(result).toEqual(mockResponse);
expect(apiClient.delete).toHaveBeenCalledWith('/auth/sessions/1');
});
it('should revoke session with numeric id', async () => {
const mockResponse = { success: true };
(apiClient.delete as any).mockResolvedValue({ data: mockResponse });
await sessionService.revokeSession(123);
expect(apiClient.delete).toHaveBeenCalledWith('/auth/sessions/123');
});
});
describe('Helper Functions', () => {
it('should extract array data from nested structure', () => {
const data = { data: { data: [1, 2, 3] } };
const result = extractArrayData(data);
expect(result).toEqual([1, 2, 3]);
});
it('should return empty array for non-array data', () => {
const data = { data: 'not an array' };
const result = extractArrayData(data);
expect(result).toEqual([]);
});
it('should transform session with number id', () => {
const session = {
id: 1,
userId: 1,
user: { username: 'test', firstName: 'Test', lastName: 'User' },
deviceName: 'Chrome',
ipAddress: '192.168.1.1',
lastActive: '2026-01-01T00:00:00Z',
isCurrent: true,
};
const result = transformSession(session);
expect(result.id).toBe(1);
expect(typeof result.id).toBe('number');
});
it('should transform session with string id to number', () => {
const session = {
id: '1',
userId: 1,
user: { username: 'test', firstName: 'Test', lastName: 'User' },
deviceName: 'Chrome',
ipAddress: '192.168.1.1',
lastActive: '2026-01-01T00:00:00Z',
isCurrent: true,
};
const result = transformSession(session);
expect(result.id).toBe(1);
expect(typeof result.id).toBe('number');
});
});
});
@@ -0,0 +1,96 @@
// File: frontend/lib/services/__tests__/transmittal.service.test.ts
// Change Log:
// - 2026-06-13: Initial creation - unit tests for transmittalService
import { describe, it, expect, vi, beforeEach } from 'vitest';
import apiClient from '@/lib/api/client';
import { transmittalService } from '../transmittal.service';
// Mock apiClient
vi.mock('@/lib/api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}));
describe('transmittalService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getAll', () => {
it('ควรดึงรายการ Transmittal ทั้งหมดพร้อม params', async () => {
const mockResponse = { data: [{ publicId: 'uuid-1', transmittalNumber: 'TR-001' }] };
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const searchParams = { search: 'TR' };
const result = await transmittalService.getAll(searchParams);
expect(apiClient.get).toHaveBeenCalledWith('/transmittals', { params: searchParams });
expect(result).toEqual(mockResponse.data);
});
});
describe('getByUuid', () => {
it('ควรดึงรายละเอียด Transmittal ตาม uuid', async () => {
const mockResponse = { data: { publicId: 'uuid-1', transmittalNumber: 'TR-001' } };
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await transmittalService.getByUuid('uuid-1');
expect(apiClient.get).toHaveBeenCalledWith('/transmittals/uuid-1');
expect(result).toEqual(mockResponse.data);
});
});
describe('create', () => {
it('ควรสร้าง Transmittal ใหม่', async () => {
const mockResponse = { data: { publicId: 'uuid-1', transmittalNumber: 'TR-001' } };
vi.mocked(apiClient.post).mockResolvedValue(mockResponse);
const createDto = {
projectId: 'uuid-proj',
subject: 'Test Transmittal',
};
const result = await transmittalService.create(createDto as any);
expect(apiClient.post).toHaveBeenCalledWith('/transmittals', createDto);
expect(result).toEqual(mockResponse.data);
});
});
describe('update', () => {
it('ควรแก้ไขข้อมูล Transmittal', async () => {
const mockResponse = { data: { publicId: 'uuid-1', subject: 'Updated Transmittal' } };
vi.mocked(apiClient.put).mockResolvedValue(mockResponse);
const updateDto = { subject: 'Updated Transmittal' };
const result = await transmittalService.update('uuid-1', updateDto);
expect(apiClient.put).toHaveBeenCalledWith('/transmittals/uuid-1', updateDto);
expect(result).toEqual(mockResponse.data);
});
});
describe('submit', () => {
it('ควรส่ง Transmittal เข้า workflow และคืนค่าผลลัพธ์', async () => {
const mockResponse = { data: { data: { instanceId: 'inst-1', currentState: 'SUBMITTED' } } };
vi.mocked(apiClient.post).mockResolvedValue(mockResponse);
const result = await transmittalService.submit('uuid-1');
expect(apiClient.post).toHaveBeenCalledWith('/transmittals/uuid-1/submit');
expect(result).toEqual(mockResponse.data.data);
});
it('ควรส่ง Transmittal เข้า workflow และจัดการ fallback เมื่อไม่มี data property ใน response', async () => {
const mockResponse = { data: { instanceId: 'inst-2', currentState: 'APPROVED' } };
vi.mocked(apiClient.post).mockResolvedValue(mockResponse);
const result = await transmittalService.submit('uuid-2');
expect(result).toEqual(mockResponse.data);
});
});
describe('delete', () => {
it('ควรลบ Transmittal (Soft Delete)', async () => {
const mockResponse = { data: { success: true } };
vi.mocked(apiClient.delete).mockResolvedValue(mockResponse);
const result = await transmittalService.delete('uuid-1');
expect(apiClient.delete).toHaveBeenCalledWith('/transmittals/uuid-1');
expect(result).toEqual(mockResponse.data);
});
});
});
@@ -0,0 +1,139 @@
// File: frontend/lib/services/__tests__/user.service.test.ts
// Change Log:
// - 2026-06-13: Initial creation - unit tests for userService
import { describe, it, expect, vi, beforeEach } from 'vitest';
import apiClient from '@/lib/api/client';
import { userService } from '../user.service';
// Mock apiClient
vi.mock('@/lib/api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}));
describe('userService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getAll', () => {
it('ควรดึงข้อมูลผู้ใช้งานทั้งหมดและแปลงข้อมูล (transformUser)', async () => {
const mockResponse = {
data: {
data: [
{
user_id: 123,
publicId: 'uuid-user-1',
username: 'test1',
assignments: [{ role: { roleName: 'Admin' } }],
},
],
},
};
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await userService.getAll({ search: 'test1' });
expect(apiClient.get).toHaveBeenCalledWith('/users', { params: { search: 'test1' } });
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
user_id: 123,
userId: 123,
publicId: 'uuid-user-1',
username: 'test1',
assignments: [{ role: { roleName: 'Admin' } }],
roles: [{ roleName: 'Admin' }],
});
});
it('ควรคืนค่าเป็นอาเรย์ว่างเมื่อไม่พบข้อมูล', async () => {
vi.mocked(apiClient.get).mockResolvedValue({ data: null });
const result = await userService.getAll();
expect(result).toEqual([]);
});
});
describe('getRoles', () => {
it('ควรดึงข้อมูลบทบาทผู้ใช้สำเร็จ', async () => {
const mockResponse = { data: [{ roleName: 'Admin' }, { roleName: 'User' }] };
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await userService.getRoles();
expect(apiClient.get).toHaveBeenCalledWith('/users/roles');
expect(result).toEqual(mockResponse.data);
});
});
describe('getByUuid', () => {
it('ควรดึงรายละเอียดผู้ใช้ตาม uuid และทำการ transform', async () => {
const mockResponse = {
data: {
userId: 456,
publicId: 'uuid-user-2',
username: 'test2',
},
};
vi.mocked(apiClient.get).mockResolvedValue(mockResponse);
const result = await userService.getByUuid('uuid-user-2');
expect(apiClient.get).toHaveBeenCalledWith('/users/uuid-user-2');
expect(result).toEqual({
userId: 456,
publicId: 'uuid-user-2',
username: 'test2',
roles: [],
});
});
});
describe('create', () => {
it('ควรสร้างผู้ใช้งานใหม่สำเร็จ', async () => {
const mockResponse = {
data: {
publicId: 'uuid-new',
username: 'newuser',
},
};
vi.mocked(apiClient.post).mockResolvedValue(mockResponse);
const createDto = { username: 'newuser', email: 'new@example.com' };
const result = await userService.create(createDto as any);
expect(apiClient.post).toHaveBeenCalledWith('/users', createDto);
expect(result).toEqual({
publicId: 'uuid-new',
username: 'newuser',
roles: [],
});
});
});
describe('update', () => {
it('ควรแก้ไขข้อมูลผู้ใช้งานสำเร็จ', async () => {
const mockResponse = {
data: {
publicId: 'uuid-existing',
username: 'updateduser',
},
};
vi.mocked(apiClient.put).mockResolvedValue(mockResponse);
const updateDto = { username: 'updateduser' };
const result = await userService.update('uuid-existing', updateDto);
expect(apiClient.put).toHaveBeenCalledWith('/users/uuid-existing', updateDto);
expect(result).toEqual({
publicId: 'uuid-existing',
username: 'updateduser',
roles: [],
});
});
});
describe('delete', () => {
it('ควรลบผู้ใช้งานสำเร็จ', async () => {
const mockResponse = { data: { success: true } };
vi.mocked(apiClient.delete).mockResolvedValue(mockResponse);
const result = await userService.delete('uuid-existing');
expect(apiClient.delete).toHaveBeenCalledWith('/users/uuid-existing');
expect(result).toEqual(mockResponse.data);
});
});
});
@@ -0,0 +1,277 @@
// File: frontend/lib/services/__tests__/workflow-engine.service.test.ts
// Change Log:
// - 2026-06-13: Refactor to use static imports instead of require, fixing ESM module resolution errors
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
workflowEngineService,
normalizeWorkflowType,
extractDslDefinition,
extractArrayData,
extractNestedData,
mapWorkflow,
} from '../workflow-engine.service';
import apiClient from '@/lib/api/client';
// Mock apiClient
vi.mock('@/lib/api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
},
}));
describe('workflowEngineService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('Engine Execution', () => {
it('should get available actions', async () => {
const mockActions = ['APPROVE', 'REJECT'];
(apiClient.post as any).mockResolvedValue({ data: { data: mockActions } });
const result = await workflowEngineService.getAvailableActions({
entityType: 'RFA',
entityId: '019505a1-7c3e-7000-8000-abc123def456',
});
expect(result).toEqual(mockActions);
expect(apiClient.post).toHaveBeenCalledWith('/workflow-engine/available-actions', {
entityType: 'RFA',
entityId: '019505a1-7c3e-7000-8000-abc123def456',
});
});
it('should evaluate workflow transition', async () => {
const mockEvaluation = { nextState: 'APPROVED', events: [] };
(apiClient.post as any).mockResolvedValue({ data: { data: mockEvaluation } });
const result = await workflowEngineService.evaluate({
entityType: 'RFA',
entityId: '019505a1-7c3e-7000-8000-abc123def456',
action: 'APPROVE',
});
expect(result).toEqual(mockEvaluation);
expect(apiClient.post).toHaveBeenCalledWith('/workflow-engine/evaluate', {
entityType: 'RFA',
entityId: '019505a1-7c3e-7000-8000-abc123def456',
action: 'APPROVE',
});
});
});
describe('Definition Management', () => {
it('should get all workflow definitions', async () => {
const mockWorkflows = [
{
id: 1,
workflow_code: 'RFA_FLOW_V1',
description: 'RFA Workflow',
version: 1,
is_active: true,
dsl: { workflowName: 'RFA Flow' },
compiled: { states: { DFT: {}, FAP: {} } },
updated_at: '2026-01-01T00:00:00Z',
},
];
(apiClient.get as any).mockResolvedValue({ data: mockWorkflows });
const result = await workflowEngineService.getDefinitions();
expect(result).toHaveLength(1);
expect(result[0].workflowName).toBe('RFA Flow');
expect(result[0].workflowType).toBe('RFA');
expect(apiClient.get).toHaveBeenCalledWith('/workflow-engine/definitions');
});
it('should get workflow definition by id', async () => {
const mockWorkflow = {
id: 1,
workflow_code: 'RFA_FLOW_V1',
description: 'RFA Workflow',
version: 1,
is_active: true,
dsl: { workflowName: 'RFA Flow' },
compiled: { states: { DFT: {}, FAP: {} } },
updated_at: '2026-01-01T00:00:00Z',
};
(apiClient.get as any).mockResolvedValue({ data: { data: mockWorkflow } });
const result = await workflowEngineService.getDefinitionById(1);
expect(result.workflowName).toBe('RFA Flow');
expect(apiClient.get).toHaveBeenCalledWith('/workflow-engine/definitions/1');
});
it('should create workflow definition', async () => {
const mockCreated = { id: 1, workflow_code: 'NEW_FLOW' };
(apiClient.post as any).mockResolvedValue({ data: { data: mockCreated } });
const result = await workflowEngineService.createDefinition({
workflowCode: 'NEW_FLOW',
dslDefinition: '{}',
});
expect(result).toEqual(mockCreated);
expect(apiClient.post).toHaveBeenCalledWith('/workflow-engine/definitions', {
workflowCode: 'NEW_FLOW',
dslDefinition: '{}',
});
});
it('should update workflow definition', async () => {
const mockUpdated = { id: 1, workflow_code: 'UPDATED_FLOW' };
(apiClient.patch as any).mockResolvedValue({ data: { data: mockUpdated } });
const result = await workflowEngineService.updateDefinition(1, {
dslDefinition: '{}',
});
expect(result).toEqual(mockUpdated);
expect(apiClient.patch).toHaveBeenCalledWith('/workflow-engine/definitions/1', {
dslDefinition: '{}',
});
});
it('should validate DSL', async () => {
const mockValidation = { valid: true };
(apiClient.post as any).mockResolvedValue({ data: { data: mockValidation } });
const result = await workflowEngineService.validateDsl({ workflowName: 'Test' });
expect(result).toEqual(mockValidation);
expect(apiClient.post).toHaveBeenCalledWith('/workflow-engine/definitions/validate', {
dsl: { workflowName: 'Test' },
});
});
it('should return validation errors for invalid DSL', async () => {
const mockValidation = {
valid: false,
errors: [{ path: 'states', message: 'Invalid state' }],
};
(apiClient.post as any).mockResolvedValue({ data: { data: mockValidation } });
const result = await workflowEngineService.validateDsl({});
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.errors).toHaveLength(1);
}
});
it('should delete workflow definition', async () => {
const mockDeleted = { id: 1 };
(apiClient.delete as any).mockResolvedValue({ data: { data: mockDeleted } });
const result = await workflowEngineService.deleteDefinition(1);
expect(result).toEqual(mockDeleted);
expect(apiClient.delete).toHaveBeenCalledWith('/workflow-engine/definitions/1');
});
});
describe('Workflow Transition and History', () => {
it('should transition workflow instance', async () => {
const mockTransition = { instanceId: 'uuid-1', state: 'APPROVED' };
(apiClient.post as any).mockResolvedValue({ data: { data: mockTransition } });
const result = await workflowEngineService.transition(
'019505a1-7c3e-7000-8000-abc123def456',
{ action: 'APPROVE', comments: 'Approved' },
'idempotency-key-123'
);
expect(result).toEqual(mockTransition);
expect(apiClient.post).toHaveBeenCalledWith(
'/workflow-engine/instances/019505a1-7c3e-7000-8000-abc123def456/transition',
{ action: 'APPROVE', comments: 'Approved' },
{ headers: { 'Idempotency-Key': 'idempotency-key-123' } }
);
});
it('should get workflow history', async () => {
const mockHistory = [
{
id: 1,
fromState: 'DFT',
toState: 'FAP',
action: 'SUBMIT',
actorId: 'user-uuid',
timestamp: '2026-01-01T00:00:00Z',
},
];
(apiClient.get as any).mockResolvedValue({ data: { data: mockHistory } });
const result = await workflowEngineService.getHistory('019505a1-7c3e-7000-8000-abc123def456');
expect(result).toEqual(mockHistory);
expect(apiClient.get).toHaveBeenCalledWith(
'/workflow-engine/instances/019505a1-7c3e-7000-8000-abc123def456/history'
);
});
it('should handle empty history array', async () => {
(apiClient.get as any).mockResolvedValue({ data: { data: [] } });
const result = await workflowEngineService.getHistory('019505a1-7c3e-7000-8000-abc123def456');
expect(result).toEqual([]);
});
});
describe('Helper Functions', () => {
it('should normalize workflow type to RFA', () => {
expect(normalizeWorkflowType('RFA_FLOW_V1')).toBe('RFA');
expect(normalizeWorkflowType('rfa_flow_v1')).toBe('RFA');
});
it('should normalize workflow type to DRAWING', () => {
expect(normalizeWorkflowType('DRAWING_FLOW_V1')).toBe('DRAWING');
expect(normalizeWorkflowType('drawing_flow_v1')).toBe('DRAWING');
});
it('should normalize workflow type to CORRESPONDENCE by default', () => {
expect(normalizeWorkflowType('CORR_FLOW_V1')).toBe('CORRESPONDENCE');
expect(normalizeWorkflowType(undefined)).toBe('CORRESPONDENCE');
});
it('should extract DSL definition from string', () => {
const dsl = '{"workflowName": "Test"}';
expect(extractDslDefinition(dsl)).toBe(dsl);
});
it('should extract DSL definition from object', () => {
const dsl = { dslDefinition: '{"workflowName": "Test"}' };
expect(extractDslDefinition(dsl)).toBe('{"workflowName": "Test"}');
});
it('should return empty string for invalid DSL', () => {
expect(extractDslDefinition(null)).toBe('');
expect(extractDslDefinition(undefined)).toBe('');
expect(extractDslDefinition('')).toBe('');
});
it('should extract array data from nested structure', () => {
const data = { data: { data: [1, 2, 3] } };
const result = extractArrayData(data);
expect(result).toEqual([1, 2, 3]);
});
it('should return empty array for non-array data', () => {
const data = { data: 'not an array' };
const result = extractArrayData(data);
expect(result).toEqual([]);
});
it('should extract nested data', () => {
const data = { data: { data: { id: 1 } } };
const result = extractNestedData(data);
expect(result).toEqual({ id: 1 });
});
it('should map backend workflow to frontend workflow', () => {
const backendWorkflow = {
id: 1,
workflow_code: 'RFA_FLOW_V1',
description: 'Test',
version: 1,
is_active: true,
dsl: { workflowName: 'RFA Flow' },
compiled: { states: { DFT: {}, FAP: {} } },
updated_at: '2026-01-01T00:00:00Z',
};
const result = mapWorkflow(backendWorkflow);
expect(result.publicId).toBe('1');
expect(result.workflowName).toBe('RFA Flow');
expect(result.workflowType).toBe('RFA');
expect(result.version).toBe(1);
expect(result.isActive).toBe(true);
expect(result.stepCount).toBe(2);
});
it('should throw error when mapping null workflow', () => {
expect(() => mapWorkflow(null as any)).toThrow('Workflow not found');
});
});
});
+6 -6
View File
@@ -1,3 +1,7 @@
// File: lib/services/session.service.ts
// Change Log:
// - 2026-06-13: Export helper functions for testing, clean up formatting, and add file header
import apiClient from '@/lib/api/client';
export interface Session {
@@ -14,25 +18,21 @@ export interface Session {
isCurrent: boolean;
}
const extractArrayData = <T>(value: unknown): T[] => {
export const extractArrayData = <T>(value: unknown): T[] => {
let current: unknown = value;
for (let i = 0; i < 5; i += 1) {
if (Array.isArray(current)) {
return current as T[];
}
if (!current || typeof current !== 'object' || !('data' in current)) {
return [];
}
current = (current as { data?: unknown }).data;
}
return Array.isArray(current) ? (current as T[]) : [];
};
const transformSession = (session: Session | (Omit<Session, 'id'> & { id: string | number })): Session => ({
export const transformSession = (session: Session | (Omit<Session, 'id'> & { id: string | number })): Session => ({
...session,
id: typeof session.id === 'number' ? session.id : Number(session.id),
});
@@ -1,4 +1,6 @@
// File: lib/services/workflow-engine.service.ts
// Change Log:
// - 2026-06-13: Export helper functions for testing and clean up internal formatting
import apiClient from '@/lib/api/client';
import {
CreateWorkflowDefinitionDto,
@@ -35,69 +37,56 @@ interface BackendWorkflowShape {
updated_at?: string;
}
const extractArrayData = <T>(value: unknown): T[] => {
export const extractArrayData = <T>(value: unknown): T[] => {
let current: unknown = value;
for (let i = 0; i < 5; i += 1) {
if (Array.isArray(current)) {
return current as T[];
}
if (!current || typeof current !== 'object' || !('data' in current)) {
return [];
}
current = (current as { data?: unknown }).data;
}
return Array.isArray(current) ? (current as T[]) : [];
};
const extractNestedData = <T>(value: unknown): T => {
export const extractNestedData = <T>(value: unknown): T => {
let current: unknown = value;
for (let i = 0; i < 5; i += 1) {
if (!current || typeof current !== 'object' || !('data' in current)) {
return current as T;
}
current = (current as WorkflowResponseShape).data;
}
return current as T;
};
const extractDslDefinition = (dsl: BackendWorkflowShape['dsl']): string => {
export const extractDslDefinition = (dsl: BackendWorkflowShape['dsl']): string => {
if (typeof dsl === 'string') {
return dsl;
}
if (!dsl || typeof dsl !== 'object') {
return '';
}
if (typeof dsl.dslDefinition === 'string') {
return dsl.dslDefinition;
}
return JSON.stringify(dsl, null, 2);
};
const normalizeWorkflowType = (workflowCode?: string): WorkflowType => {
export const normalizeWorkflowType = (workflowCode?: string): WorkflowType => {
const normalizedCode = workflowCode?.toUpperCase() ?? '';
if (normalizedCode.includes('RFA')) {
return 'RFA';
}
if (normalizedCode.includes('DRAWING')) {
return 'DRAWING';
}
return 'CORRESPONDENCE';
};
const mapWorkflow = (backendObj: BackendWorkflowShape): Workflow => {
export const mapWorkflow = (backendObj: BackendWorkflowShape): Workflow => {
if (!backendObj) throw new Error('Workflow not found');
return {
publicId: String(backendObj.id ?? ''),