[ { "filePath": "D:\\nap-dms.lcbp3\\.agents\\scripts\\start-mcp.js", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\analyze-any.cjs", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\eslint.config.mjs", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\app.controller.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\app.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\app.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\app.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\auth.controller.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\auth.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\auth.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\auth.service.spec.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 21, "column": 7, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 21, "endColumn": 33 }, { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'jwtService' is assigned a value but never used. Allowed unused vars must match /^_/u.", "line": 26, "column": 7, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 26, "endColumn": 17 }, { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'tokenRepo' is assigned a value but never used. Allowed unused vars must match /^_/u.", "line": 27, "column": 7, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 27, "endColumn": 16 }, { "ruleId": "@typescript-eslint/no-unsafe-call", "severity": 2, "message": "Unsafe call of an `any` typed value.", "line": 56, "column": 5, "nodeType": "MemberExpression", "messageId": "unsafeCall", "endLine": 56, "endColumn": 37 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .compare on an `any` value.", "line": 56, "column": 12, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 56, "endColumn": 19 }, { "ruleId": "@typescript-eslint/no-unsafe-call", "severity": 2, "message": "Unsafe call of an `any` typed value.", "line": 131, "column": 7, "nodeType": "MemberExpression", "messageId": "unsafeCall", "endLine": 131, "endColumn": 43 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .compare on an `any` value.", "line": 131, "column": 14, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 131, "endColumn": 21 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 165, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 165, "endColumn": 32 } ], "suppressedMessages": [ { "ruleId": "@typescript-eslint/no-require-imports", "severity": 2, "message": "A `require()` style import is forbidden.", "line": 21, "column": 16, "nodeType": "CallExpression", "messageId": "noRequireImports", "endLine": 21, "endColumn": 33, "suppressions": [{ "kind": "directive", "justification": "" }] } ], "errorCount": 8, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { AuthService } from './auth.service';\r\nimport { UserService } from '../../modules/user/user.service';\r\nimport { JwtService } from '@nestjs/jwt';\r\nimport { ConfigService } from '@nestjs/config';\r\nimport { CACHE_MANAGER } from '@nestjs/cache-manager';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { User } from '../../modules/user/entities/user.entity';\r\nimport { RefreshToken } from './entities/refresh-token.entity';\r\nimport { Repository } from 'typeorm';\r\nimport { UnauthorizedException } from '@nestjs/common';\r\n\r\n// Mock bcrypt at top level\r\njest.mock('bcrypt', () => ({\r\n compare: jest.fn(),\r\n hash: jest.fn().mockResolvedValue('hashedpassword'),\r\n genSalt: jest.fn().mockResolvedValue('salt'),\r\n}));\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-require-imports\r\nconst bcrypt = require('bcrypt');\r\n\r\ndescribe('AuthService', () => {\r\n let service: AuthService;\r\n let userService: UserService;\r\n let jwtService: JwtService;\r\n let tokenRepo: Repository;\r\n\r\n const mockUser = {\r\n user_id: 1,\r\n username: 'testuser',\r\n password: 'hashedpassword',\r\n primaryOrganizationId: 1,\r\n };\r\n\r\n const mockQueryBuilder = {\r\n addSelect: jest.fn().mockReturnThis(),\r\n leftJoinAndSelect: jest.fn().mockReturnThis(),\r\n where: jest.fn().mockReturnThis(),\r\n getOne: jest.fn().mockResolvedValue(mockUser),\r\n };\r\n\r\n const mockUserRepo = {\r\n createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),\r\n };\r\n\r\n const mockTokenRepo = {\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n findOne: jest.fn(),\r\n update: jest.fn(),\r\n };\r\n\r\n beforeEach(async () => {\r\n // Reset bcrypt mocks\r\n bcrypt.compare.mockResolvedValue(true);\r\n\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n AuthService,\r\n {\r\n provide: UserService,\r\n useValue: {\r\n findOneByUsername: jest.fn(),\r\n create: jest.fn(),\r\n findOne: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: JwtService,\r\n useValue: {\r\n signAsync: jest.fn().mockResolvedValue('jwt_token'),\r\n decode: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: ConfigService,\r\n useValue: {\r\n get: jest.fn().mockImplementation((key: string) => {\r\n if (key.includes('EXPIRATION')) return '1h';\r\n return 'secret';\r\n }),\r\n },\r\n },\r\n {\r\n provide: CACHE_MANAGER,\r\n useValue: {\r\n set: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: getRepositoryToken(User),\r\n useValue: mockUserRepo,\r\n },\r\n {\r\n provide: getRepositoryToken(RefreshToken),\r\n useValue: mockTokenRepo,\r\n },\r\n ],\r\n }).compile();\r\n\r\n service = module.get(AuthService);\r\n userService = module.get(UserService);\r\n jwtService = module.get(JwtService);\r\n tokenRepo = module.get(getRepositoryToken(RefreshToken));\r\n });\r\n\r\n afterEach(() => {\r\n jest.clearAllMocks();\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('validateUser', () => {\r\n it('should return user without password if validation succeeds', async () => {\r\n const result = await service.validateUser('testuser', 'password');\r\n expect(result).toBeDefined();\r\n expect(result).not.toHaveProperty('password');\r\n expect(result.username).toBe('testuser');\r\n });\r\n\r\n it('should return null if user not found', async () => {\r\n mockQueryBuilder.getOne.mockResolvedValueOnce(null);\r\n const result = await service.validateUser('unknown', 'password');\r\n expect(result).toBeNull();\r\n });\r\n\r\n it('should return null if password mismatch', async () => {\r\n bcrypt.compare.mockResolvedValueOnce(false);\r\n const result = await service.validateUser('testuser', 'wrongpassword');\r\n expect(result).toBeNull();\r\n });\r\n });\r\n\r\n describe('login', () => {\r\n it('should return access and refresh tokens', async () => {\r\n mockTokenRepo.create.mockReturnValue({ id: 1 });\r\n mockTokenRepo.save.mockResolvedValue({ id: 1 });\r\n\r\n const result = await service.login(mockUser);\r\n\r\n expect(result).toHaveProperty('access_token');\r\n expect(result).toHaveProperty('refresh_token');\r\n expect(mockTokenRepo.save).toHaveBeenCalled();\r\n });\r\n });\r\n\r\n describe('register', () => {\r\n it('should register a new user', async () => {\r\n (userService.findOneByUsername as jest.Mock).mockResolvedValue(null);\r\n (userService.create as jest.Mock).mockResolvedValue(mockUser);\r\n\r\n const dto = {\r\n username: 'newuser',\r\n password: 'password',\r\n email: 'test@example.com',\r\n firstName: 'Test',\r\n lastName: 'User',\r\n };\r\n\r\n const result = await service.register(dto);\r\n expect(result).toBeDefined();\r\n expect(userService.create).toHaveBeenCalled();\r\n });\r\n });\r\n\r\n describe('refreshToken', () => {\r\n it('should return new tokens if valid', async () => {\r\n const mockStoredToken = {\r\n tokenHash: 'somehash',\r\n isRevoked: false,\r\n expiresAt: new Date(Date.now() + 10000),\r\n };\r\n mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);\r\n (userService.findOne as jest.Mock).mockResolvedValue(mockUser);\r\n\r\n const result = await service.refreshToken(1, 'valid_refresh_token');\r\n\r\n expect(result.access_token).toBeDefined();\r\n expect(result.refresh_token).toBeDefined();\r\n // Should mark old token as revoked\r\n expect(mockTokenRepo.save).toHaveBeenCalledWith(\r\n expect.objectContaining({ isRevoked: true })\r\n );\r\n });\r\n\r\n it('should throw UnauthorizedException if token revoked', async () => {\r\n const mockStoredToken = {\r\n tokenHash: 'somehash',\r\n isRevoked: true,\r\n expiresAt: new Date(Date.now() + 10000),\r\n };\r\n mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);\r\n\r\n await expect(service.refreshToken(1, 'revoked_token')).rejects.toThrow(\r\n UnauthorizedException\r\n );\r\n });\r\n\r\n it('should allow refresh within 30s grace period if already revoked', async () => {\r\n const updatedAt = new Date(Date.now() - 5000); // 5 seconds ago\r\n const mockStoredToken = {\r\n tokenHash: 'somehash',\r\n isRevoked: true,\r\n updatedAt: updatedAt,\r\n replacedByToken: 'new_token_hash',\r\n expiresAt: new Date(Date.now() + 10000),\r\n };\r\n mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);\r\n (userService.findOne as jest.Mock).mockResolvedValue(mockUser);\r\n mockTokenRepo.create.mockReturnValue({ token_id: 2 });\r\n mockTokenRepo.save.mockResolvedValue({ token_id: 2 });\r\n\r\n const result = await service.refreshToken(1, 'valid_refresh_token');\r\n\r\n expect(result.access_token).toBeDefined();\r\n expect(result.refresh_token).toBeDefined();\r\n // Should not call revokeAllUserTokens\r\n expect(mockTokenRepo.update).not.toHaveBeenCalled();\r\n });\r\n\r\n it('should throw UnauthorizedException if token revoked more than 30s ago', async () => {\r\n const updatedAt = new Date(Date.now() - 35000); // 35 seconds ago\r\n const mockStoredToken = {\r\n tokenHash: 'somehash',\r\n isRevoked: true,\r\n updatedAt: updatedAt,\r\n replacedByToken: 'new_token_hash',\r\n expiresAt: new Date(Date.now() + 10000),\r\n };\r\n mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);\r\n\r\n await expect(service.refreshToken(1, 'revoked_token')).rejects.toThrow(\r\n UnauthorizedException\r\n );\r\n });\r\n });\r\n});\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\auth.service.ts", "messages": [], "suppressedMessages": [ { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'password' is assigned a value but never used. Allowed unused vars must match /^_/u.", "line": 82, "column": 15, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 82, "endColumn": 23, "suppressions": [{ "kind": "directive", "justification": "" }] } ], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\casl\\ability.factory.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\casl\\ability.factory.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\casl\\casl.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\dto\\login.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\dto\\register.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\entities\\refresh-token.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\guards\\permissions.guard.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\session.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\strategies\\jwt-refresh.strategy.ts", "messages": [ { "ruleId": "@typescript-eslint/require-await", "severity": 2, "message": "Async method 'validate' has no 'await' expression.", "line": 27, "column": 3, "nodeType": "FunctionExpression", "messageId": "missingAwait", "endLine": 27, "endColumn": 17, "suggestions": [ { "messageId": "removeAsync", "fix": { "range": [960, 966], "text": "" }, "desc": "Remove 'async'." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/common/auth/strategies/jwt-refresh.strategy.ts\r\n// บันทึกการแก้ไข: Strategy สำหรับ Refresh Token (T1.2)\r\n// บันทึกการแก้ไข: แก้ไข TS2345 โดยยืนยันค่า secretOrKey ด้วย ! (Non-null assertion)\r\n\r\nimport { ExtractJwt, Strategy } from 'passport-jwt';\r\nimport { PassportStrategy } from '@nestjs/passport';\r\nimport { Injectable } from '@nestjs/common';\r\nimport { ConfigService } from '@nestjs/config';\r\nimport { Request } from 'express';\r\nimport type { JwtPayload } from './jwt.strategy';\r\n\r\n@Injectable()\r\nexport class JwtRefreshStrategy extends PassportStrategy(\r\n Strategy,\r\n 'jwt-refresh'\r\n) {\r\n constructor(configService: ConfigService) {\r\n super({\r\n jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\r\n ignoreExpiration: false,\r\n // ✅ Fix: ใส่ ! เพื่อบอก TS ว่าค่านี้มีอยู่จริง (จาก env validation)\r\n secretOrKey: configService.get('JWT_REFRESH_SECRET')!,\r\n passReqToCallback: true,\r\n });\r\n }\r\n\r\n async validate(req: Request, payload: JwtPayload) {\r\n const refreshToken = ExtractJwt.fromAuthHeaderAsBearerToken()(req);\r\n return {\r\n ...payload,\r\n refreshToken,\r\n };\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\strategies\\jwt.strategy.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\common.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\config\\env.validation.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\config\\redis.config.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\audit.decorator.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\bypass-maintenance.decorator.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\circuit-breaker.decorator.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\current-user.decorator.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\idempotency.decorator.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\require-permission.decorator.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\retry.decorator.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\entities\\audit-log.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\entities\\base.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\entities\\uuid-base.entity.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\entities\\uuid-base.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\exceptions\\http-exception.filter.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\entities\\attachment.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-cleanup.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-storage.controller.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-storage.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-storage.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-storage.service.spec.ts", "messages": [ { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 88, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 88, "endColumn": 35 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 89, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 89, "endColumn": 33 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 121, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 121, "endColumn": 33 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 139, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 139, "endColumn": 35 } ], "suppressedMessages": [], "errorCount": 4, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { FileStorageService } from './file-storage.service';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { Attachment } from './entities/attachment.entity';\r\nimport { ConfigService } from '@nestjs/config';\r\nimport * as fs from 'fs-extra';\r\nimport {\r\n BadRequestException,\r\n NotFoundException,\r\n ForbiddenException,\r\n} from '@nestjs/common';\r\nimport { Repository } from 'typeorm';\r\n\r\n// Mock fs-extra\r\njest.mock('fs-extra');\r\n\r\ndescribe('FileStorageService', () => {\r\n let service: FileStorageService;\r\n let attachmentRepo: Repository;\r\n\r\n const mockAttachment = {\r\n id: 1,\r\n originalFilename: 'test.pdf',\r\n storedFilename: 'uuid.pdf',\r\n filePath: '/permanent/2024/12/uuid.pdf',\r\n fileSize: 1024,\r\n uploadedByUserId: 1,\r\n } as Attachment;\r\n\r\n const mockFile = {\r\n originalname: 'test.pdf',\r\n mimetype: 'application/pdf',\r\n size: 1024,\r\n buffer: Buffer.from('test-content'),\r\n } as Express.Multer.File;\r\n\r\n beforeEach(async () => {\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n FileStorageService,\r\n {\r\n provide: getRepositoryToken(Attachment),\r\n useValue: {\r\n create: jest.fn().mockReturnValue(mockAttachment),\r\n save: jest.fn().mockResolvedValue(mockAttachment),\r\n find: jest.fn(),\r\n findOne: jest.fn(),\r\n remove: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: ConfigService,\r\n useValue: {\r\n get: jest.fn((key) => {\r\n if (key === 'NODE_ENV') return 'test';\r\n return null;\r\n }),\r\n },\r\n },\r\n ],\r\n }).compile();\r\n\r\n service = module.get(FileStorageService);\r\n attachmentRepo = module.get(getRepositoryToken(Attachment));\r\n\r\n jest.clearAllMocks();\r\n (fs.ensureDirSync as unknown as jest.Mock).mockReturnValue(true);\r\n (fs.writeFile as unknown as jest.Mock).mockResolvedValue(undefined);\r\n (fs.pathExists as unknown as jest.Mock).mockResolvedValue(true);\r\n (fs.move as unknown as jest.Mock).mockResolvedValue(undefined);\r\n (fs.remove as unknown as jest.Mock).mockResolvedValue(undefined);\r\n (fs.readFile as unknown as jest.Mock).mockResolvedValue(\r\n Buffer.from('test')\r\n );\r\n (fs.stat as unknown as jest.Mock).mockResolvedValue({ size: 1024 });\r\n (fs.ensureDir as unknown as jest.Mock).mockResolvedValue(undefined);\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('upload', () => {\r\n it('should save file to temp and create DB record', async () => {\r\n const result = await service.upload(mockFile, 1);\r\n\r\n expect(fs.writeFile).toHaveBeenCalled();\r\n expect(attachmentRepo.create).toHaveBeenCalled();\r\n expect(attachmentRepo.save).toHaveBeenCalled();\r\n expect(result).toBeDefined();\r\n });\r\n\r\n it('should throw BadRequestException if write fails', async () => {\r\n (fs.writeFile as unknown as jest.Mock).mockRejectedValueOnce(\r\n new Error('Write error')\r\n );\r\n await expect(service.upload(mockFile, 1)).rejects.toThrow(\r\n BadRequestException\r\n );\r\n });\r\n });\r\n\r\n describe('commit', () => {\r\n it('should move files to permanent storage', async () => {\r\n const tempIds = ['uuid-1'];\r\n const mockAttachments = [\r\n {\r\n ...mockAttachment,\r\n isTemporary: true,\r\n tempId: 'uuid-1',\r\n filePath: '/temp/uuid.pdf',\r\n },\r\n ];\r\n\r\n (attachmentRepo.find as jest.Mock).mockResolvedValue(mockAttachments);\r\n\r\n await service.commit(tempIds);\r\n\r\n expect(fs.ensureDir).toHaveBeenCalled();\r\n expect(fs.move).toHaveBeenCalled();\r\n expect(attachmentRepo.save).toHaveBeenCalled();\r\n });\r\n\r\n it('should show warning if file counts mismatch', async () => {\r\n (attachmentRepo.find as jest.Mock).mockResolvedValue([]);\r\n await expect(service.commit(['uuid-1'])).rejects.toThrow(\r\n NotFoundException\r\n );\r\n });\r\n });\r\n\r\n describe('delete', () => {\r\n it('should delete file if user owns it', async () => {\r\n (attachmentRepo.findOne as jest.Mock).mockResolvedValue(mockAttachment);\r\n\r\n await service.delete(1, 1);\r\n\r\n expect(fs.remove).toHaveBeenCalled();\r\n expect(attachmentRepo.remove).toHaveBeenCalled();\r\n });\r\n\r\n it('should throw ForbiddenException if user does not own file', async () => {\r\n (attachmentRepo.findOne as jest.Mock).mockResolvedValue(mockAttachment);\r\n await expect(service.delete(1, 999)).rejects.toThrow(ForbiddenException);\r\n });\r\n });\r\n});\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-storage.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\guards\\jwt-auth.guard.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\guards\\jwt-refresh.guard.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\guards\\maintenance-mode.guard.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\guards\\rbac.guard.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\interceptors\\audit-log.interceptor.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\interceptors\\idempotency.interceptor.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\interceptors\\performance.interceptor.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\interceptors\\transform.interceptor.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\interfaces\\request-with-user.interface.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\pipes\\parse-uuid.pipe.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\pipes\\parse-uuid.pipe.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\resilience\\resilience.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\services\\crypto.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\services\\request-context.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\services\\uuid-resolver.service.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\services\\uuid-resolver.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\utils\\uuid-guard.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\config\\database.config.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\migrations\\1701676800000-V1_5_1_Schema_Update.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\migrations\\InitialSchema.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\seeds\\organization.seed.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\seeds\\run-seed.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `DataSourceOptions`.", "line": 7, "column": 37, "nodeType": "TSAsExpression", "messageId": "unsafeArgument", "endLine": 7, "endColumn": 58 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 7, "column": 55, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 7, "endColumn": 58, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [288, 291], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [288, 291], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 11, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 11, "endColumn": 16, "suggestions": [ { "fix": { "range": [344, 382], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 16, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 16, "endColumn": 16, "suggestions": [ { "fix": { "range": [468, 504], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 18, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 18, "endColumn": 18, "suggestions": [ { "fix": { "range": [531, 573], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "error" }, "desc": "Remove the console.error()." } ] }, { "ruleId": "@typescript-eslint/no-floating-promises", "severity": 2, "message": "Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.", "line": 24, "column": 1, "nodeType": "ExpressionStatement", "messageId": "floatingVoid", "endLine": 24, "endColumn": 12, "suggestions": [ { "messageId": "floatingFixVoid", "fix": { "range": [633, 633], "text": "void " }, "desc": "Add void operator to ignore." }, { "messageId": "floatingFixAwait", "fix": { "range": [633, 633], "text": "await " }, "desc": "Add await operator." } ] } ], "suppressedMessages": [], "errorCount": 6, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { DataSource } from 'typeorm';\r\nimport { databaseConfig } from '../../config/database.config';\r\nimport { seedOrganizations } from './organization.seed';\r\nimport { seedUsers } from './user.seed';\r\n\r\nasync function runSeeds() {\r\n const dataSource = new DataSource(databaseConfig as any);\r\n await dataSource.initialize();\r\n\r\n try {\r\n console.log('🌱 Seeding database...');\r\n\r\n await seedOrganizations(dataSource);\r\n await seedUsers(dataSource);\r\n\r\n console.log('✅ Seeding completed!');\r\n } catch (error) {\r\n console.error('❌ Seeding failed:', error);\r\n } finally {\r\n await dataSource.destroy();\r\n }\r\n}\r\n\r\nrunSeeds();\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\seeds\\user.seed.ts", "messages": [ { "ruleId": "@typescript-eslint/ban-ts-comment", "severity": 2, "message": "Use \"@ts-expect-error\" instead of \"@ts-ignore\", as \"@ts-ignore\" will do nothing if the following line is error-free.", "line": 61, "column": 7, "nodeType": "Line", "messageId": "tsIgnoreInsteadOfExpectError", "endLine": 61, "endColumn": 20, "suggestions": [ { "messageId": "replaceTsIgnoreWithTsExpectError", "fix": { "range": [2088, 2101], "text": "// @ts-expect-error" }, "desc": "Replace \"@ts-ignore\" with \"@ts-expect-error\"." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 115, "column": 13, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 115, "endColumn": 43 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 119, "column": 11, "nodeType": "Property", "messageId": "anyAssignment", "endLine": 119, "endColumn": 15 } ], "suppressedMessages": [], "errorCount": 3, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { DataSource } from 'typeorm';\r\nimport { User } from '../../modules/user/entities/user.entity';\r\nimport { Role, RoleScope } from '../../modules/user/entities/role.entity';\r\nimport { UserAssignment } from '../../modules/user/entities/user-assignment.entity';\r\nimport * as bcrypt from 'bcrypt';\r\n\r\nexport async function seedUsers(dataSource: DataSource) {\r\n const userRepo = dataSource.getRepository(User);\r\n const roleRepo = dataSource.getRepository(Role);\r\n const assignmentRepo = dataSource.getRepository(UserAssignment);\r\n\r\n // Create Roles\r\n const rolesData = [\r\n {\r\n roleName: 'Superadmin',\r\n scope: RoleScope.GLOBAL,\r\n description:\r\n 'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global',\r\n },\r\n {\r\n roleName: 'Org Admin',\r\n scope: RoleScope.ORGANIZATION,\r\n description:\r\n 'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร',\r\n },\r\n {\r\n roleName: 'Document Control',\r\n scope: RoleScope.ORGANIZATION,\r\n description:\r\n 'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร',\r\n },\r\n {\r\n roleName: 'Editor',\r\n scope: RoleScope.PROJECT,\r\n description:\r\n 'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย',\r\n },\r\n {\r\n roleName: 'Viewer',\r\n scope: RoleScope.PROJECT,\r\n description: 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น',\r\n },\r\n {\r\n roleName: 'Project Manager',\r\n scope: RoleScope.PROJECT,\r\n description:\r\n 'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ',\r\n },\r\n {\r\n roleName: 'Contract Admin',\r\n scope: RoleScope.CONTRACT,\r\n description:\r\n 'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา',\r\n },\r\n ];\r\n\r\n const roleMap = new Map();\r\n for (const r of rolesData) {\r\n let role = await roleRepo.findOneBy({ roleName: r.roleName });\r\n if (!role) {\r\n // @ts-ignore\r\n role = await roleRepo.save(roleRepo.create(r));\r\n }\r\n roleMap.set(r.roleName, role);\r\n }\r\n\r\n // Create Users\r\n const usersData = [\r\n {\r\n username: 'superadmin',\r\n email: 'superadmin@example.com',\r\n firstName: 'Super',\r\n lastName: 'Admin',\r\n roleName: 'Superadmin',\r\n },\r\n {\r\n username: 'admin',\r\n email: 'admin@example.com',\r\n firstName: 'Admin',\r\n lastName: 'คคง.',\r\n roleName: 'Org Admin',\r\n },\r\n {\r\n username: 'editor01',\r\n email: 'editor01@example.com',\r\n firstName: 'DC',\r\n lastName: 'C1',\r\n roleName: 'Editor',\r\n },\r\n {\r\n username: 'viewer01',\r\n email: 'viewer01@example.com',\r\n firstName: 'Viewer',\r\n lastName: 'สคฉ.03',\r\n roleName: 'Viewer',\r\n },\r\n ];\r\n\r\n const salt = await bcrypt.genSalt();\r\n const password = await bcrypt.hash('password123', salt); // Default password\r\n\r\n for (const u of usersData) {\r\n let user = await userRepo.findOneBy({ username: u.username });\r\n if (!user) {\r\n user = userRepo.create({\r\n username: u.username,\r\n email: u.email,\r\n firstName: u.firstName,\r\n lastName: u.lastName,\r\n password, // Fixed: password instead of passwordHash\r\n });\r\n user = await userRepo.save(user);\r\n\r\n // Create Assignment\r\n const role = roleMap.get(u.roleName);\r\n if (role) {\r\n const assignment = assignmentRepo.create({\r\n user,\r\n role,\r\n assignedAt: new Date(),\r\n });\r\n await assignmentRepo.save(assignment);\r\n }\r\n }\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\seeds\\workflow-definitions.seed.ts", "messages": [ { "ruleId": "prettier/prettier", "severity": 2, "message": "Insert `········`", "line": 133, "column": 1, "nodeType": null, "messageId": "insert", "endLine": 133, "endColumn": 1, "fix": { "range": [4038, 4038], "text": " " } }, { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'error' is defined but never used.", "line": 134, "column": 16, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 134, "endColumn": 21 }, { "ruleId": "prettier/prettier", "severity": 2, "message": "Insert `········`", "line": 135, "column": 1, "nodeType": null, "messageId": "insert", "endLine": 135, "endColumn": 1, "fix": { "range": [4142, 4142], "text": " " } }, { "ruleId": "prettier/prettier", "severity": 2, "message": "Insert `······`", "line": 138, "column": 1, "nodeType": null, "messageId": "insert", "endLine": 138, "endColumn": 1, "fix": { "range": [4245, 4245], "text": " " } }, { "ruleId": "prettier/prettier", "severity": 2, "message": "Insert `······`", "line": 139, "column": 1, "nodeType": null, "messageId": "insert", "endLine": 139, "endColumn": 1, "fix": { "range": [4268, 4268], "text": " " } }, { "ruleId": "prettier/prettier", "severity": 2, "message": "Insert `······`", "line": 140, "column": 1, "nodeType": null, "messageId": "insert", "endLine": 140, "endColumn": 1, "fix": { "range": [4343, 4343], "text": " " } } ], "suppressedMessages": [], "errorCount": 6, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 5, "fixableWarningCount": 0, "source": "// src/database/seeds/workflow-definitions.seed.ts\r\n\r\nimport { DataSource } from 'typeorm';\r\nimport { WorkflowDefinition } from '../../modules/workflow-engine/entities/workflow-definition.entity';\r\nimport { WorkflowDslService } from '../../modules/workflow-engine/workflow-dsl.service';\r\n\r\nexport const seedWorkflowDefinitions = async (dataSource: DataSource) => {\r\n const repo = dataSource.getRepository(WorkflowDefinition);\r\n const dslService = new WorkflowDslService();\r\n\r\n // 1. RFA Workflow (Standard)\r\n const rfaDsl = {\r\n workflow: 'RFA_FLOW_V1', // [FIX] เปลี่ยนชื่อให้ตรงกับค่าใน RfaWorkflowService\r\n version: 1,\r\n description: 'Standard RFA Approval Workflow',\r\n states: [\r\n {\r\n name: 'DRAFT',\r\n initial: true,\r\n on: {\r\n SUBMIT: {\r\n to: 'IN_REVIEW',\r\n require: { role: 'Editor' }, // [FIX] แก้ไข Syntax เป็น Object\r\n },\r\n },\r\n },\r\n {\r\n name: 'IN_REVIEW',\r\n on: {\r\n APPROVE_1: {\r\n to: 'APPROVED', // [FIX] ชี้ไปที่ State ที่มีอยู่จริง\r\n require: { role: 'Contract Admin' },\r\n condition: \"context.priority === 'HIGH'\",\r\n },\r\n APPROVE_2: {\r\n to: 'APPROVED', // [FIX] ชี้ไปที่ State ที่มีอยู่จริง\r\n require: { role: 'Contract Admin' },\r\n condition: \"context.priority === 'NORMAL'\",\r\n },\r\n REJECT: {\r\n to: 'REJECTED',\r\n require: { role: 'Contract Admin' },\r\n },\r\n COMMENT: {\r\n to: 'DRAFT',\r\n require: { role: 'Contract Admin' },\r\n }, // ส่งกลับแก้ไข\r\n },\r\n },\r\n { name: 'APPROVED', terminal: true },\r\n { name: 'REJECTED', terminal: true },\r\n ],\r\n };\r\n\r\n // 2. Circulation Workflow\r\n const circulationDsl = {\r\n workflow: 'CIRCULATION_INTERNAL_V1', // [FIX] เปลี่ยนชื่อให้ตรงกับค่าใน CirculationWorkflowService\r\n version: 1,\r\n description: 'Internal Document Circulation',\r\n states: [\r\n {\r\n name: 'OPEN',\r\n initial: true,\r\n on: {\r\n START: {\r\n // [FIX] เปลี่ยนชื่อ Action ให้ตรงกับที่ Service เรียกใช้ ('START')\r\n to: 'IN_REVIEW',\r\n },\r\n },\r\n },\r\n {\r\n name: 'IN_REVIEW',\r\n on: {\r\n COMPLETE_TASK: {\r\n // [FIX] เปลี่ยนให้สอดคล้องกับ Action ที่ใช้จริง\r\n to: 'COMPLETED',\r\n },\r\n CANCEL: { to: 'CANCELLED' },\r\n },\r\n },\r\n { name: 'COMPLETED', terminal: true },\r\n { name: 'CANCELLED', terminal: true },\r\n ],\r\n };\r\n\r\n // 3. Correspondence Workflow (Optional - ถ้ามี)\r\n const correspondenceDsl = {\r\n workflow: 'CORRESPONDENCE_FLOW_V1',\r\n version: 1,\r\n description: 'Standard Correspondence Routing',\r\n states: [\r\n {\r\n name: 'DRAFT',\r\n initial: true,\r\n on: { SUBMIT: { to: 'IN_REVIEW' } },\r\n },\r\n {\r\n name: 'IN_REVIEW',\r\n on: {\r\n APPROVE: { to: 'APPROVED' },\r\n REJECT: { to: 'REJECTED' },\r\n },\r\n },\r\n { name: 'APPROVED', terminal: true },\r\n { name: 'REJECTED', terminal: true },\r\n ],\r\n };\r\n\r\n const workflows = [rfaDsl, circulationDsl, correspondenceDsl];\r\n\r\n for (const dsl of workflows) {\r\n const exists = await repo.findOne({\r\n where: { workflow_code: dsl.workflow, version: dsl.version },\r\n });\r\n\r\n if (!exists) {\r\n try {\r\n // Compile เพื่อ Validate และ Normalize ก่อนบันทึก\r\n const compiled = dslService.compile(\r\n dsl as unknown as import('../../modules/workflow-engine/workflow-dsl.service').RawWorkflowDSL\r\n );\r\n\r\n await repo.save(\r\n repo.create({\r\n workflow_code: dsl.workflow,\r\n version: dsl.version,\r\n description: dsl.description,\r\n dsl: dsl as unknown as Record,\r\n compiled: compiled as unknown as Record,\r\n is_active: true,\r\n })\r\n );\r\n// console.log(`✅ Seeded Workflow: ${dsl.workflow} v${dsl.version}`);\r\n } catch (error) {\r\n// console.error(`❌ Failed to seed workflow ${dsl.workflow}:`, error);\r\n }\r\n } else {\r\n// console.log(\r\n// `⏭️ Workflow already exists: ${dsl.workflow} v${dsl.version}`\r\n// );\r\n }\r\n }\r\n};\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\main.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\audit-log\\audit-log.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\audit-log\\audit-log.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\audit-log\\audit-log.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\auth\\entities\\role.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\circulation-workflow.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\circulation.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\circulation.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\circulation.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\dto\\create-circulation.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\dto\\search-circulation.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\dto\\update-circulation-routing.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\entities\\circulation-routing.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\entities\\circulation-status-code.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\entities\\circulation.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\contract.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\contract.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\contract.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\dto\\create-contract.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\dto\\search-contract.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\dto\\update-contract.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\entities\\contract-organization.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\entities\\contract.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence-workflow.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence.controller.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence.service.spec.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'CorrespondenceRecipient' is defined but never used. Allowed unused vars must match /^_/u.", "line": 11, "column": 10, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 11, "endColumn": 33, "suggestions": [ { "messageId": "removeUnusedImportDeclaration", "data": { "varName": "CorrespondenceRecipient" }, "fix": { "range": [693, 780], "text": "" }, "desc": "Remove unused import declaration." } ] }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 22, "column": 27, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 22, "endColumn": 30, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [1400, 1403], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [1400, 1403], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 23, "column": 21, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 23, "endColumn": 24, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [1426, 1429], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [1426, 1429], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'dataSource' is assigned a value but never used. Allowed unused vars must match /^_/u.", "line": 24, "column": 7, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 24, "endColumn": 17 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 24, "column": 19, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 24, "endColumn": 22, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [1450, 1453], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [1450, 1453], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 133, "column": 5, "nodeType": "AssignmentExpression", "messageId": "anyAssignment", "endLine": 133, "endColumn": 72 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 134, "column": 5, "nodeType": "AssignmentExpression", "messageId": "anyAssignment", "endLine": 134, "endColumn": 74 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 135, "column": 5, "nodeType": "AssignmentExpression", "messageId": "anyAssignment", "endLine": 135, "endColumn": 40 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 144, "column": 13, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 144, "endColumn": 72 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 144, "column": 69, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 144, "endColumn": 72, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [5152, 5155], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [5152, 5155], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'mockStatus' is assigned a value but never used. Allowed unused vars must match /^_/u.", "line": 154, "column": 13, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 154, "endColumn": 23 }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto`.", "line": 177, "column": 31, "nodeType": "TSAsExpression", "messageId": "unsafeArgument", "endLine": 177, "endColumn": 47 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 177, "column": 44, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 177, "endColumn": 47, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [6388, 6391], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [6388, 6391], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `User`.", "line": 177, "column": 49, "nodeType": "Identifier", "messageId": "unsafeArgument", "endLine": 177, "endColumn": 57 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 180, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 180, "endColumn": 51 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 184, "column": 13, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 184, "endColumn": 72 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 184, "column": 69, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 184, "endColumn": 72, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [6692, 6695], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [6692, 6695], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto`.", "line": 208, "column": 31, "nodeType": "TSAsExpression", "messageId": "unsafeArgument", "endLine": 208, "endColumn": 47 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 208, "column": 44, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 208, "endColumn": 47, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [7419, 7422], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [7419, 7422], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `User`.", "line": 208, "column": 49, "nodeType": "Identifier", "messageId": "unsafeArgument", "endLine": 208, "endColumn": 57 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 210, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 210, "endColumn": 51 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 213, "column": 13, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 213, "endColumn": 72 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 213, "column": 69, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 213, "endColumn": 72, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [7663, 7666], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [7663, 7666], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto`.", "line": 237, "column": 31, "nodeType": "TSAsExpression", "messageId": "unsafeArgument", "endLine": 237, "endColumn": 47 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 237, "column": 44, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 237, "endColumn": 47, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [8372, 8375], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [8372, 8375], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `User`.", "line": 237, "column": 49, "nodeType": "Identifier", "messageId": "unsafeArgument", "endLine": 237, "endColumn": 57 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 239, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 239, "endColumn": 51 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 243, "column": 13, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 243, "endColumn": 72 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 243, "column": 69, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 243, "endColumn": 72, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [8627, 8630], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [8627, 8630], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-call", "severity": 2, "message": "Unsafe call of a type that could not be resolved.", "line": 262, "column": 7, "nodeType": "MemberExpression", "messageId": "errorCall", "endLine": 264, "endColumn": 27 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .mockResolvedValue on a type that cannot be resolved.", "line": 264, "column": 10, "nodeType": "Identifier", "messageId": "errorMemberExpression", "endLine": 264, "endColumn": 27 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 264, "column": 71, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 264, "endColumn": 74, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [9355, 9358], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [9355, 9358], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto`.", "line": 270, "column": 31, "nodeType": "TSAsExpression", "messageId": "unsafeArgument", "endLine": 270, "endColumn": 47 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 270, "column": 44, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 270, "endColumn": 47, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [9525, 9528], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [9525, 9528], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `User`.", "line": 270, "column": 49, "nodeType": "Identifier", "messageId": "unsafeArgument", "endLine": 270, "endColumn": 57 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 272, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 272, "endColumn": 51 } ], "suppressedMessages": [], "errorCount": 36, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { DataSource } from 'typeorm';\r\nimport { CorrespondenceService } from './correspondence.service';\r\nimport { Correspondence } from './entities/correspondence.entity';\r\nimport { CorrespondenceRevision } from './entities/correspondence-revision.entity';\r\nimport { CorrespondenceType } from './entities/correspondence-type.entity';\r\nimport { CorrespondenceStatus } from './entities/correspondence-status.entity';\r\nimport { CorrespondenceReference } from './entities/correspondence-reference.entity';\r\nimport { Organization } from '../organization/entities/organization.entity';\r\nimport { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';\r\nimport { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';\r\nimport { JsonSchemaService } from '../json-schema/json-schema.service';\r\nimport { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';\r\nimport { UserService } from '../user/user.service';\r\nimport { SearchService } from '../search/search.service';\r\nimport { FileStorageService } from '../../common/file-storage/file-storage.service';\r\n\r\ndescribe('CorrespondenceService', () => {\r\n let service: CorrespondenceService;\r\n let numberingService: DocumentNumberingService;\r\n let correspondenceRepo: any;\r\n let revisionRepo: any;\r\n let dataSource: any;\r\n\r\n const createMockRepository = () => ({\r\n find: jest.fn(),\r\n findOne: jest.fn(),\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n update: jest.fn(),\r\n delete: jest.fn(),\r\n softDelete: jest.fn(),\r\n createQueryBuilder: jest.fn(() => ({\r\n leftJoinAndSelect: jest.fn().mockReturnThis(),\r\n where: jest.fn().mockReturnThis(),\r\n andWhere: jest.fn().mockReturnThis(),\r\n orderBy: jest.fn().mockReturnThis(),\r\n skip: jest.fn().mockReturnThis(),\r\n take: jest.fn().mockReturnThis(),\r\n getOne: jest.fn().mockResolvedValue(null),\r\n getMany: jest.fn().mockResolvedValue([]),\r\n getManyAndCount: jest.fn().mockResolvedValue([[], 0]),\r\n })),\r\n });\r\n\r\n const mockDataSource = {\r\n createQueryRunner: jest.fn(() => ({\r\n connect: jest.fn(),\r\n startTransaction: jest.fn(),\r\n commitTransaction: jest.fn(),\r\n rollbackTransaction: jest.fn(),\r\n release: jest.fn(),\r\n manager: {\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n findOne: jest.fn(),\r\n },\r\n })),\r\n getRepository: jest.fn(() => createMockRepository()),\r\n };\r\n\r\n beforeEach(async () => {\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n CorrespondenceService,\r\n {\r\n provide: getRepositoryToken(Correspondence),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: getRepositoryToken(CorrespondenceRevision),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: getRepositoryToken(CorrespondenceType),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: getRepositoryToken(CorrespondenceStatus),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: getRepositoryToken(CorrespondenceReference),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: getRepositoryToken(Organization),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: DocumentNumberingService,\r\n useValue: {\r\n generateNextNumber: jest.fn(),\r\n updateNumberForDraft: jest.fn(),\r\n previewNextNumber: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: JsonSchemaService,\r\n useValue: { validate: jest.fn() },\r\n },\r\n {\r\n provide: WorkflowEngineService,\r\n useValue: { createInstance: jest.fn() },\r\n },\r\n {\r\n provide: UserService,\r\n useValue: {\r\n findOne: jest.fn(),\r\n getUserPermissions: jest.fn().mockResolvedValue([]),\r\n },\r\n },\r\n {\r\n provide: DataSource,\r\n useValue: mockDataSource,\r\n },\r\n {\r\n provide: SearchService,\r\n useValue: { indexDocument: jest.fn() },\r\n },\r\n {\r\n provide: FileStorageService,\r\n useValue: { commit: jest.fn().mockResolvedValue([]) },\r\n },\r\n ],\r\n }).compile();\r\n\r\n service = module.get(CorrespondenceService);\r\n numberingService = module.get(\r\n DocumentNumberingService\r\n );\r\n correspondenceRepo = module.get(getRepositoryToken(Correspondence));\r\n revisionRepo = module.get(getRepositoryToken(CorrespondenceRevision));\r\n dataSource = module.get(DataSource);\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('update', () => {\r\n it('should NOT regenerate number if critical fields unchanged', async () => {\r\n const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;\r\n const mockRevision = {\r\n id: 100,\r\n correspondenceId: 1,\r\n isCurrent: true,\r\n statusId: 5,\r\n }; // Status 5 = Draft handled by logic?\r\n // Mock status repo to return DRAFT\r\n // But strict logic: revision.statusId check\r\n jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);\r\n const mockStatus = { id: 5, statusCode: 'DRAFT' };\r\n // Need to set statusRepo mock behavior... simplified here for brevity or assume defaults\r\n // Injecting internal access to statusRepo is hard without `module.get` if I didn't save it.\r\n // Let's assume it passes check for now.\r\n\r\n const mockCorr = {\r\n id: 1,\r\n projectId: 1,\r\n correspondenceTypeId: 2,\r\n disciplineId: 3,\r\n originatorId: 10,\r\n correspondenceNumber: 'OLD-NUM',\r\n recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],\r\n };\r\n jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);\r\n\r\n // Update DTO with same values\r\n const updateDto = {\r\n projectId: 1,\r\n disciplineId: 3,\r\n // recipients missing -> imply no change\r\n };\r\n\r\n await service.update(1, updateDto as any, mockUser);\r\n\r\n // Check that updateNumberForDraft was NOT called\r\n expect(numberingService.updateNumberForDraft).not.toHaveBeenCalled();\r\n });\r\n\r\n it('should regenerate number if Project ID changes', async () => {\r\n const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;\r\n const mockRevision = {\r\n id: 100,\r\n correspondenceId: 1,\r\n isCurrent: true,\r\n statusId: 5,\r\n };\r\n jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);\r\n\r\n const mockCorr = {\r\n id: 1,\r\n projectId: 1, // Old Project\r\n correspondenceTypeId: 2,\r\n disciplineId: 3,\r\n originatorId: 10,\r\n correspondenceNumber: 'OLD-NUM',\r\n recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],\r\n };\r\n jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);\r\n\r\n const updateDto = {\r\n projectId: 2, // New Project -> Change!\r\n };\r\n\r\n await service.update(1, updateDto as any, mockUser);\r\n\r\n expect(numberingService.updateNumberForDraft).toHaveBeenCalled();\r\n });\r\n it('should regenerate number if Document Type changes', async () => {\r\n const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;\r\n const mockRevision = {\r\n id: 100,\r\n correspondenceId: 1,\r\n isCurrent: true,\r\n statusId: 5,\r\n };\r\n jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);\r\n\r\n const mockCorr = {\r\n id: 1,\r\n projectId: 1,\r\n correspondenceTypeId: 2, // Old Type\r\n disciplineId: 3,\r\n originatorId: 10,\r\n correspondenceNumber: 'OLD-NUM',\r\n recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],\r\n };\r\n jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);\r\n\r\n const updateDto = {\r\n typeId: 999, // New Type\r\n };\r\n\r\n await service.update(1, updateDto as any, mockUser);\r\n\r\n expect(numberingService.updateNumberForDraft).toHaveBeenCalled();\r\n });\r\n\r\n it('should regenerate number if Recipient Organization changes', async () => {\r\n const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;\r\n const mockRevision = {\r\n id: 100,\r\n correspondenceId: 1,\r\n isCurrent: true,\r\n statusId: 5,\r\n };\r\n jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);\r\n\r\n const mockCorr = {\r\n id: 1,\r\n projectId: 1,\r\n correspondenceTypeId: 2,\r\n disciplineId: 3,\r\n originatorId: 10,\r\n correspondenceNumber: 'OLD-NUM',\r\n recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }], // Old Recipient 99\r\n };\r\n jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);\r\n jest\r\n .spyOn(service['orgRepo'], 'findOne')\r\n .mockResolvedValue({ id: 88, organizationCode: 'NEW-ORG' } as any);\r\n\r\n const updateDto = {\r\n recipients: [{ type: 'TO', organizationId: 88 }], // New Recipient 88\r\n };\r\n\r\n await service.update(1, updateDto as any, mockUser);\r\n\r\n expect(numberingService.updateNumberForDraft).toHaveBeenCalled();\r\n });\r\n });\r\n});\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\add-reference.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\create-correspondence.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\create-routing-template.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\search-correspondence.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\submit-correspondence.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\update-correspondence.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\workflow-action.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-recipient.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-reference.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-revision.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-routing.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-status.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-sub-type.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-type.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\routing-template-step.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\routing-template.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dashboard.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dashboard.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dashboard.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dto\\dashboard-stats.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dto\\get-activity.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dto\\get-pending.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dto\\index.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\controllers\\document-numbering-admin.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\controllers\\document-numbering.controller.ts", "messages": [ { "ruleId": null, "nodeType": null, "fatal": true, "severity": 2, "message": "Parsing error: Declaration or statement expected.", "line": 122, "column": 4 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 1, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import {\r\n Controller,\r\n Get,\r\n Post,\r\n Patch,\r\n Param,\r\n Body,\r\n UseGuards,\r\n Query,\r\n ParseIntPipe,\r\n} from '@nestjs/common';\r\nimport {\r\n ApiTags,\r\n ApiOperation,\r\n ApiResponse,\r\n ApiBearerAuth,\r\n ApiQuery,\r\n} from '@nestjs/swagger';\r\nimport { JwtAuthGuard } from '../../../common/guards/jwt-auth.guard';\r\nimport { RbacGuard } from '../../../common/guards/rbac.guard';\r\nimport { RequirePermission } from '../../../common/decorators/require-permission.decorator';\r\nimport { DocumentNumberingService } from '../services/document-numbering.service';\r\nimport { PreviewNumberDto } from '../dto/preview-number.dto';\r\n\r\n@ApiTags('Document Numbering')\r\n@ApiBearerAuth()\r\n@Controller('document-numbering')\r\n@UseGuards(JwtAuthGuard, RbacGuard)\r\nexport class DocumentNumberingController {\r\n constructor(private readonly numberingService: DocumentNumberingService) {}\r\n\r\n // ----------------------------------------------------------\r\n // Logs\r\n // ----------------------------------------------------------\r\n\r\n @Get('logs/audit')\r\n @ApiOperation({ summary: 'Get document generation audit logs' })\r\n @ApiResponse({ status: 200, description: 'List of audit logs' })\r\n @ApiQuery({ name: 'limit', required: false, type: Number })\r\n @RequirePermission('system.view_logs')\r\n getAuditLogs(@Query('limit') limit?: number) {\r\n return this.numberingService.getAuditLogs(limit ? Number(limit) : 100);\r\n }\r\n\r\n @Get('logs/errors')\r\n @ApiOperation({ summary: 'Get document generation error logs' })\r\n @ApiResponse({ status: 200, description: 'List of error logs' })\r\n @ApiQuery({ name: 'limit', required: false, type: Number })\r\n @RequirePermission('system.view_logs')\r\n getErrorLogs(@Query('limit') limit?: number) {\r\n return this.numberingService.getErrorLogs(limit ? Number(limit) : 100);\r\n }\r\n\r\n // ----------------------------------------------------------\r\n // Sequences / Counters\r\n // ----------------------------------------------------------\r\n\r\n @Get('sequences')\r\n @ApiOperation({ summary: 'Get all number sequences/counters' })\r\n @ApiResponse({ status: 200, description: 'List of counter sequences' })\r\n @ApiQuery({ name: 'projectId', required: false, type: Number })\r\n @RequirePermission('correspondence.read')\r\n getSequences(@Query('projectId') projectId?: number) {\r\n return this.numberingService.getSequences(\r\n projectId ? Number(projectId) : undefined\r\n );\r\n }\r\n\r\n @Patch('counters/:id')\r\n @ApiOperation({ summary: 'Update counter sequence value (Admin only)' })\r\n @RequirePermission('numbering.manage_formats')\r\n async updateCounter(\r\n @Param('id', ParseIntPipe) id: number,\r\n @Body('sequence') sequence: number\r\n ) {\r\n return this.numberingService.setCounterValue(id, sequence);\r\n }\r\n\r\n // ----------------------------------------------------------\r\n // Preview / Test\r\n // ----------------------------------------------------------\r\n\r\n @Post('preview')\r\n @ApiOperation({ summary: 'Preview what a document number would look like' })\r\n @ApiResponse({\r\n status: 200,\r\n description: 'Preview result without incrementing counter',\r\n })\r\n @RequirePermission('correspondence.read')\r\n async previewNumber(@Body() dto: PreviewNumberDto) {\r\n // ADR-019: Resolve UUID→INT for project and organization IDs\r\n const resolvedProjectId = await this.numberingService.resolveIdForPreview(\r\n 'project',\r\n dto.projectId\r\n );\r\n const resolvedOriginatorId =\r\n await this.numberingService.resolveIdForPreview(\r\n 'organization',\r\n dto.originatorOrganizationId\r\n );\r\n const resolvedRecipientId = dto.recipientOrganizationId\r\n ? await this.numberingService.resolveIdForPreview(\r\n 'organization',\r\n dto.recipientOrganizationId\r\n )\r\n : undefined;\r\n\r\n const result = await this.numberingService.previewNumber({\r\n projectId: resolvedProjectId,\r\n originatorOrganizationId: resolvedOriginatorId,\r\n typeId: dto.correspondenceTypeId,\r\n subTypeId: dto.subTypeId,\r\n rfaTypeId: dto.rfaTypeId,\r\n disciplineId: dto.disciplineId,\r\n recipientOrganizationId: resolvedRecipientId,\r\n year: dto.year,\r\n customTokens: dto.customTokens,\r\n });\r\n// console.log(\r\n '[DocumentNumberingController] Preview result:',\r\n JSON.stringify(result)\r\n );\r\n return result;\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\controllers\\numbering-metrics.controller.ts", "messages": [ { "ruleId": "@typescript-eslint/require-await", "severity": 2, "message": "Async method 'getMetrics' has no 'await' expression.", "line": 13, "column": 3, "nodeType": "FunctionExpression", "messageId": "missingAwait", "endLine": 13, "endColumn": 19, "suggestions": [ { "messageId": "removeAsync", "fix": { "range": [513, 519], "text": "" }, "desc": "Remove 'async'." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Controller, Get, _UseGuards } from '@nestjs/common';\nimport { MetricsService } from '../services/metrics.service';\n// import { PermissionGuard } from '../../auth/guards/permission.guard';\n// import { Permissions } from '../../auth/decorators/permissions.decorator';\n\n@Controller('admin/document-numbering/metrics')\n// @UseGuards(PermissionGuard)\nexport class NumberingMetricsController {\n constructor(private readonly metricsService: MetricsService) {}\n\n @Get()\n // @Permissions('system.view_logs')\n async getMetrics() {\n // Determine how to return metrics.\n // Standard Prometheus metrics are usually exposed via a separate /metrics endpoint processing all metrics.\n // If the frontend needs JSON data, we might need to query the current values from the registry or metrics service.\n\n // For now, returning a simple status or aggregated view if supported by MetricsService,\n // otherwise this might be a placeholder for a custom dashboard API.\n return {\n status: 'Metrics are being collected',\n // TODO: Implement custom JSON export of metric values if needed for custom dashboard\n };\n }\n}\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\document-numbering.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\document-numbering.service.spec.ts", "messages": [ { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 127, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 127, "endColumn": 45 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 128, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 128, "endColumn": 34 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 145, "column": 13, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 145, "endColumn": 76 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .findOne on an `any` value.", "line": 146, "column": 18, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 146, "endColumn": 25 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .save on an `any` value.", "line": 151, "column": 18, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 151, "endColumn": 22 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .save on an `any` value.", "line": 159, "column": 24, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 159, "endColumn": 28 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 163, "column": 13, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 163, "endColumn": 76 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .findOne on an `any` value.", "line": 164, "column": 18, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 164, "endColumn": 25 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .save on an `any` value.", "line": 168, "column": 18, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 168, "endColumn": 22 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .save on an `any` value.", "line": 176, "column": 24, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 176, "endColumn": 28 } ], "suppressedMessages": [], "errorCount": 10, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { DocumentNumberingService } from './services/document-numbering.service';\r\nimport { CounterService } from './services/counter.service';\r\nimport { ReservationService } from './services/reservation.service';\r\nimport { FormatService } from './services/format.service';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { ConfigService } from '@nestjs/config';\r\nimport { DocumentNumberFormat } from './entities/document-number-format.entity';\r\nimport { DocumentNumberAudit } from './entities/document-number-audit.entity';\r\nimport { DocumentNumberError } from './entities/document-number-error.entity';\r\n\r\nimport { DocumentNumberingLockService } from './services/document-numbering-lock.service';\r\nimport { ManualOverrideService } from './services/manual-override.service';\r\nimport { MetricsService } from './services/metrics.service';\r\n\r\ndescribe('DocumentNumberingService', () => {\r\n let service: DocumentNumberingService;\r\n let module: TestingModule;\r\n let counterService: CounterService;\r\n let formatService: FormatService;\r\n\r\n const mockContext = {\r\n projectId: 1,\r\n originatorOrganizationId: 1,\r\n recipientOrganizationId: 1,\r\n typeId: 1,\r\n subTypeId: 1,\r\n rfaTypeId: 1,\r\n disciplineId: 1,\r\n year: 2025,\r\n customTokens: { TYPE_CODE: 'COR', ORG_CODE: 'GGL' },\r\n };\r\n\r\n beforeEach(async () => {\r\n module = await Test.createTestingModule({\r\n providers: [\r\n DocumentNumberingService,\r\n {\r\n provide: ConfigService,\r\n useValue: { get: jest.fn().mockReturnValue('localhost') },\r\n },\r\n {\r\n provide: CounterService,\r\n useValue: {\r\n incrementCounter: jest.fn().mockResolvedValue(1),\r\n getCurrentSequence: jest.fn().mockResolvedValue(0),\r\n },\r\n },\r\n {\r\n provide: ReservationService,\r\n useValue: {\r\n reserve: jest.fn(),\r\n confirm: jest.fn(),\r\n cancel: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: FormatService,\r\n useValue: {\r\n format: jest.fn().mockResolvedValue('0001'),\r\n },\r\n },\r\n {\r\n provide: DocumentNumberingLockService,\r\n useValue: {\r\n acquireLock: jest.fn().mockResolvedValue({ release: jest.fn() }),\r\n releaseLock: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: ManualOverrideService,\r\n useValue: { applyOverride: jest.fn() },\r\n },\r\n {\r\n provide: MetricsService,\r\n useValue: {\r\n numbersGenerated: { inc: jest.fn() },\r\n lockFailures: { inc: jest.fn() },\r\n },\r\n },\r\n {\r\n provide: getRepositoryToken(DocumentNumberFormat),\r\n useValue: { findOne: jest.fn() },\r\n },\r\n {\r\n provide: getRepositoryToken(DocumentNumberAudit),\r\n useValue: {\r\n create: jest.fn().mockReturnValue({ id: 1 }),\r\n save: jest.fn().mockResolvedValue({ id: 1 }),\r\n findOne: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: getRepositoryToken(DocumentNumberError),\r\n useValue: {\r\n create: jest.fn().mockReturnValue({}),\r\n save: jest.fn().mockResolvedValue({}),\r\n },\r\n },\r\n ],\r\n }).compile();\r\n\r\n service = module.get(DocumentNumberingService);\r\n counterService = module.get(CounterService);\r\n formatService = module.get(FormatService);\r\n });\r\n\r\n afterEach(() => {\r\n jest.clearAllMocks();\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('generateNextNumber', () => {\r\n it('should generate a new number successfully', async () => {\r\n (counterService.incrementCounter as jest.Mock).mockResolvedValue(1);\r\n (formatService.format as jest.Mock).mockResolvedValue('DOC-0001');\r\n\r\n const result = await service.generateNextNumber(mockContext);\r\n\r\n // Service returns object with number and auditId\r\n expect(result).toHaveProperty('number');\r\n expect(result).toHaveProperty('auditId');\r\n expect(result.number).toBe('DOC-0001');\r\n expect(counterService.incrementCounter).toHaveBeenCalled();\r\n expect(formatService.format).toHaveBeenCalled();\r\n });\r\n\r\n it('should throw error when increment fails', async () => {\r\n // Mock CounterService to throw error\r\n (counterService.incrementCounter as jest.Mock).mockRejectedValue(\r\n new Error('Transaction failed')\r\n );\r\n\r\n await expect(service.generateNextNumber(mockContext)).rejects.toThrow(\r\n 'Transaction failed'\r\n );\r\n });\r\n });\r\n\r\n describe('Admin Operations', () => {\r\n it('voidAndReplace should verify audit log exists', async () => {\r\n const auditRepo = module.get(getRepositoryToken(DocumentNumberAudit));\r\n (auditRepo.findOne as jest.Mock).mockResolvedValue({\r\n documentNumber: 'DOC-001',\r\n counterKey: JSON.stringify({ projectId: 1, correspondenceTypeId: 1 }),\r\n templateUsed: 'test',\r\n });\r\n (auditRepo.save as jest.Mock).mockResolvedValue({ id: 2 });\r\n\r\n const result = await service.voidAndReplace({\r\n documentNumber: 'DOC-001',\r\n reason: 'test',\r\n replace: false,\r\n });\r\n expect(result.status).toBe('VOIDED');\r\n expect(auditRepo.save).toHaveBeenCalled();\r\n });\r\n\r\n it('cancelNumber should log cancellation', async () => {\r\n const auditRepo = module.get(getRepositoryToken(DocumentNumberAudit));\r\n (auditRepo.findOne as jest.Mock).mockResolvedValue({\r\n documentNumber: 'DOC-002',\r\n counterKey: {},\r\n });\r\n (auditRepo.save as jest.Mock).mockResolvedValue({ id: 3 });\r\n\r\n const result = await service.cancelNumber({\r\n documentNumber: 'DOC-002',\r\n reason: 'bad',\r\n projectId: 1,\r\n });\r\n expect(result.status).toBe('CANCELLED');\r\n expect(auditRepo.save).toHaveBeenCalled();\r\n });\r\n });\r\n});\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\dto\\confirm-reservation.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\dto\\counter-key.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\dto\\manual-override.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\dto\\preview-number.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\dto\\reserve-number.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\entities\\document-number-audit.entity.ts", "messages": [ { "ruleId": "@typescript-eslint/no-redundant-type-constituents", "severity": 2, "message": "'unknown' overrides all other types in this union type.", "line": 28, "column": 42, "nodeType": "TSUnknownKeyword", "messageId": "overrides", "endLine": 28, "endColumn": 49 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import {\r\n Entity,\r\n PrimaryGeneratedColumn,\r\n Column,\r\n CreateDateColumn,\r\n Index,\r\n} from 'typeorm';\r\n\r\n@Entity('document_number_audit')\r\n@Index(['createdAt'])\r\n@Index(['userId'])\r\n@Index(['documentId'])\r\n@Index(['status'])\r\n@Index(['operation'])\r\n@Index(['documentNumber'])\r\n@Index(['reservationToken'])\r\nexport class DocumentNumberAudit {\r\n @PrimaryGeneratedColumn()\r\n id!: number;\r\n\r\n @Column({ name: 'document_id', nullable: true })\r\n documentId?: number;\r\n\r\n @Column({ name: 'document_number', length: 100 })\r\n documentNumber!: string;\r\n\r\n @Column({ name: 'counter_key', type: 'json' })\r\n counterKey!: Record | unknown;\r\n\r\n @Column({ name: 'template_used', length: 200 })\r\n templateUsed!: string;\r\n\r\n @Column({\r\n name: 'operation',\r\n type: 'enum',\r\n enum: [\r\n 'RESERVE',\r\n 'CONFIRM',\r\n 'CANCEL',\r\n 'MANUAL_OVERRIDE',\r\n 'VOID',\r\n 'VOID_REPLACE',\r\n 'GENERATE',\r\n ],\r\n default: 'CONFIRM',\r\n })\r\n operation!: string;\r\n\r\n @Column({\r\n name: 'status',\r\n type: 'enum',\r\n enum: ['RESERVED', 'CONFIRMED', 'CANCELLED', 'VOID', 'MANUAL'],\r\n nullable: true,\r\n })\r\n status?: string;\r\n\r\n @Column({ name: 'reservation_token', length: 36, nullable: true })\r\n reservationToken?: string;\r\n\r\n @Column({ name: 'idempotency_key', length: 36, nullable: true })\r\n idempotencyKey?: string;\r\n\r\n @Column({ name: 'originator_organization_id', nullable: true })\r\n originatorOrganizationId?: number;\r\n\r\n @Column({ name: 'recipient_organization_id', nullable: true })\r\n recipientOrganizationId?: number;\r\n\r\n @Column({ name: 'old_value', type: 'text', nullable: true })\r\n oldValue?: string;\r\n\r\n @Column({ name: 'new_value', type: 'text', nullable: true })\r\n newValue?: string;\r\n\r\n @Column({ name: 'metadata', type: 'json', nullable: true })\r\n metadata?: Record;\r\n\r\n @Column({ name: 'user_id', nullable: true })\r\n userId?: number;\r\n\r\n @Column({ name: 'ip_address', length: 45, nullable: true })\r\n ipAddress?: string;\r\n\r\n @Column({ name: 'user_agent', type: 'text', nullable: true })\r\n userAgent?: string;\r\n\r\n @Column({ name: 'is_success', default: true })\r\n isSuccess!: boolean;\r\n\r\n @Column({ name: 'retry_count', default: 0 })\r\n retryCount!: number;\r\n\r\n @Column({ name: 'lock_wait_ms', nullable: true })\r\n lockWaitMs?: number;\r\n\r\n @Column({ name: 'total_duration_ms', nullable: true })\r\n totalDurationMs?: number;\r\n\r\n @Column({\r\n name: 'fallback_used',\r\n type: 'enum',\r\n enum: ['NONE', 'DB_LOCK', 'RETRY'],\r\n default: 'NONE',\r\n })\r\n fallbackUsed?: string;\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date;\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\entities\\document-number-counter.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\entities\\document-number-error.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\entities\\document-number-format.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\entities\\document-number-reservation.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\interfaces\\document-numbering.interface.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\audit.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\counter.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\document-numbering-lock.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\document-numbering.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\format.service.ts", "messages": [ { "ruleId": null, "nodeType": null, "fatal": true, "severity": 2, "message": "Parsing error: Declaration or statement expected.", "line": 56, "column": 4 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 1, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Injectable } from '@nestjs/common';\r\nimport { InjectRepository } from '@nestjs/typeorm';\r\nimport { Repository, IsNull } from 'typeorm';\r\nimport { DocumentNumberFormat } from '../entities/document-number-format.entity';\r\nimport { Project } from '../../project/entities/project.entity';\r\nimport { CorrespondenceType } from '../../correspondence/entities/correspondence-type.entity';\r\nimport { Organization } from '../../organization/entities/organization.entity';\r\nimport { Discipline } from '../../master/entities/discipline.entity';\r\n\r\nexport interface FormatOptions {\r\n projectId: number;\r\n correspondenceTypeId: number;\r\n subTypeId?: number;\r\n rfaTypeId?: number;\r\n disciplineId?: number;\r\n sequence: number;\r\n resetScope: string;\r\n year?: number;\r\n originatorOrganizationId: number;\r\n recipientOrganizationId?: number;\r\n}\r\n\r\nexport interface DecodedTokens {\r\n [key: string]: string;\r\n}\r\n\r\n@Injectable()\r\nexport class FormatService {\r\n constructor(\r\n @InjectRepository(DocumentNumberFormat)\r\n private formatRepo: Repository,\r\n @InjectRepository(Project)\r\n private projectRepo: Repository,\r\n @InjectRepository(CorrespondenceType)\r\n private typeRepo: Repository,\r\n @InjectRepository(Organization)\r\n private orgRepo: Repository,\r\n @InjectRepository(Discipline)\r\n private disciplineRepo: Repository\r\n ) {}\r\n\r\n async format(\r\n options: FormatOptions\r\n ): Promise<{ previewNumber: string; isDefault: boolean }> {\r\n const { template, isDefault } = await this.resolveFormatAndScope(options);\r\n const currentYear = options.year || new Date().getFullYear();\r\n const tokens = await this.resolveTokens(options, currentYear);\r\n\r\n const previewNumber = this.replaceTokens(\r\n template,\r\n tokens,\r\n options.sequence\r\n );\r\n// console.log(\r\n `[FormatService] Generated: \"${previewNumber}\" | Template: \"${template}\" | isDefault: ${isDefault}`\r\n );\r\n return { previewNumber, isDefault };\r\n }\r\n\r\n // --- Helpers ---\r\n\r\n private async resolveFormatAndScope(options: FormatOptions): Promise<{\r\n template: string;\r\n resetSequenceYearly: boolean;\r\n isDefault: boolean;\r\n }> {\r\n // 1. Specific Format\r\n const specificFormat = await this.formatRepo.findOne({\r\n where: {\r\n projectId: options.projectId,\r\n correspondenceTypeId: options.correspondenceTypeId,\r\n },\r\n });\r\n if (specificFormat)\r\n return {\r\n template: specificFormat.formatTemplate,\r\n resetSequenceYearly: specificFormat.resetSequenceYearly,\r\n isDefault: false,\r\n };\r\n\r\n // 2. Default Format\r\n const defaultFormat = await this.formatRepo.findOne({\r\n where: { projectId: options.projectId, correspondenceTypeId: IsNull() },\r\n });\r\n if (defaultFormat)\r\n return {\r\n template: defaultFormat.formatTemplate,\r\n resetSequenceYearly: defaultFormat.resetSequenceYearly,\r\n isDefault: true,\r\n };\r\n\r\n // 3. Fallback\r\n return {\r\n template: '{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}',\r\n resetSequenceYearly: true,\r\n isDefault: true,\r\n };\r\n }\r\n\r\n private async resolveTokens(\r\n options: FormatOptions,\r\n year: number\r\n ): Promise {\r\n const [project, type, recipientCode, disciplineCode, orgCode] =\r\n await Promise.all([\r\n this.projectRepo.findOne({\r\n where: { id: options.projectId },\r\n select: ['projectCode'],\r\n }),\r\n this.typeRepo.findOne({\r\n where: { id: options.correspondenceTypeId },\r\n select: ['typeCode'],\r\n }),\r\n this.resolveRecipientCode(options.recipientOrganizationId),\r\n this.resolveDisciplineCode(options.disciplineId),\r\n this.resolveOrgCode(options.originatorOrganizationId),\r\n ]);\r\n\r\n return {\r\n '{PROJECT}': project?.projectCode || 'PROJ',\r\n '{TYPE}': type?.typeCode || 'DOC',\r\n '{ORG}': orgCode,\r\n '{RECIPIENT}': recipientCode,\r\n '{DISCIPLINE}': disciplineCode,\r\n '{YEAR}': year.toString(),\r\n '{YEAR:BE}': (year + 543).toString(),\r\n '{REV}': '0',\r\n };\r\n }\r\n\r\n private replaceTokens(\r\n template: string,\r\n tokens: DecodedTokens,\r\n sequence: number\r\n ): string {\r\n let result = template;\r\n for (const [key, value] of Object.entries(tokens)) {\r\n result = result.replace(\r\n new RegExp(key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'),\r\n value\r\n );\r\n }\r\n const seqMatch = result.match(/{SEQ:(\\d+)}/);\r\n if (seqMatch) {\r\n const padding = Number(seqMatch[1]);\r\n result = result.replace(\r\n seqMatch[0],\r\n sequence.toString().padStart(padding, '0')\r\n );\r\n }\r\n return result;\r\n }\r\n\r\n private async resolveRecipientCode(recipientId?: number): Promise {\r\n if (!recipientId) return 'GEN';\r\n const org = await this.orgRepo.findOne({\r\n where: { id: recipientId },\r\n select: ['organizationCode'],\r\n });\r\n return org ? org.organizationCode : 'GEN';\r\n }\r\n\r\n private async resolveOrgCode(orgId?: number): Promise {\r\n if (!orgId) return 'GEN';\r\n const org = await this.orgRepo.findOne({\r\n where: { id: orgId },\r\n select: ['organizationCode'],\r\n });\r\n return org ? org.organizationCode : 'GEN';\r\n }\r\n\r\n private async resolveDisciplineCode(disciplineId?: number): Promise {\r\n if (!disciplineId) return 'GEN';\r\n const discipline = await this.disciplineRepo.findOne({\r\n where: { id: disciplineId },\r\n select: ['disciplineCode'],\r\n });\r\n return discipline ? discipline.disciplineCode : 'GEN';\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\manual-override.service.spec.ts", "messages": [ { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 56, "column": 12, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 56, "endColumn": 45 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 57, "column": 12, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 57, "endColumn": 28 } ], "suppressedMessages": [], "errorCount": 2, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { ManualOverrideService } from './manual-override.service';\r\nimport { CounterService } from './counter.service';\r\nimport { AuditService } from './audit.service';\r\nimport { ManualOverrideDto } from '../dto/manual-override.dto';\r\n\r\ndescribe('ManualOverrideService', () => {\r\n let service: ManualOverrideService;\r\n let counterService: CounterService;\r\n let auditService: AuditService;\r\n\r\n const mockCounterService = {\r\n forceUpdateCounter: jest.fn(),\r\n };\r\n\r\n const mockAuditService = {\r\n log: jest.fn(),\r\n };\r\n\r\n beforeEach(async () => {\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n ManualOverrideService,\r\n { provide: CounterService, useValue: mockCounterService },\r\n { provide: AuditService, useValue: mockAuditService },\r\n ],\r\n }).compile();\r\n\r\n service = module.get(ManualOverrideService);\r\n counterService = module.get(CounterService);\r\n auditService = module.get(AuditService);\r\n });\r\n\r\n afterEach(() => {\r\n jest.clearAllMocks();\r\n });\r\n\r\n it('should apply override and log audit', async () => {\r\n const dto: ManualOverrideDto = {\r\n projectId: 1,\r\n originatorOrganizationId: 2,\r\n recipientOrganizationId: 3,\r\n correspondenceTypeId: 4,\r\n subTypeId: 5,\r\n rfaTypeId: 6,\r\n disciplineId: 7,\r\n resetScope: 'YEAR_2024',\r\n newLastNumber: 999,\r\n reason: 'System sync',\r\n reference: 'TICKET-123',\r\n };\r\n const userId = 101;\r\n\r\n await service.applyOverride(dto, userId);\r\n\r\n expect(counterService.forceUpdateCounter).toHaveBeenCalledWith(dto, 999);\r\n expect(auditService.log).toHaveBeenCalledWith(\r\n expect.objectContaining({\r\n documentNumber: 'OVERRIDE-TO-999',\r\n operation: 'MANUAL_OVERRIDE',\r\n status: 'MANUAL',\r\n userId: userId,\r\n metadata: { reason: 'System sync', reference: 'TICKET-123' },\r\n })\r\n );\r\n });\r\n});\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\manual-override.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\metrics.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\reservation.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\template.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\asbuilt-drawing.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\asbuilt-drawing.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\contract-drawing.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\contract-drawing.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\drawing-master-data.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\drawing-master-data.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\drawing.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\create-asbuilt-drawing-revision.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\create-asbuilt-drawing.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\create-contract-drawing.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\create-shop-drawing-revision.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\create-shop-drawing.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\search-asbuilt-drawing.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\search-contract-drawing.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\search-shop-drawing.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\update-contract-drawing.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\asbuilt-drawing-revision.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\asbuilt-drawing.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\contract-drawing-category.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\contract-drawing-sub-category.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\contract-drawing-subcat-cat-map.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\contract-drawing-volume.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\contract-drawing.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\shop-drawing-main-category.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\shop-drawing-revision.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\shop-drawing-sub-category.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\shop-drawing.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\shop-drawing.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\shop-drawing.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\dto\\create-json-schema.dto.ts", "messages": [ { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 51, "column": 37, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 51, "endColumn": 40, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [966, 969], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [966, 969], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 55, "column": 29, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 55, "endColumn": 32, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [1035, 1038], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [1035, 1038], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 65, "column": 36, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 65, "endColumn": 39, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [1264, 1267], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [1264, 1267], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] } ], "suppressedMessages": [], "errorCount": 3, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/modules/json-schema/dto/create-json-schema.dto.ts\r\nimport {\r\n IsString,\r\n IsNotEmpty,\r\n IsInt,\r\n IsOptional,\r\n IsBoolean,\r\n IsObject,\r\n IsArray,\r\n ValidateNested,\r\n} from 'class-validator';\r\nimport { Type } from 'class-transformer';\r\n\r\nexport class VirtualColumnConfigDto {\r\n @IsString()\r\n @IsNotEmpty()\r\n json_path!: string;\r\n\r\n @IsString()\r\n @IsNotEmpty()\r\n column_name!: string;\r\n\r\n @IsString()\r\n @IsNotEmpty()\r\n data_type!: 'INT' | 'VARCHAR' | 'BOOLEAN' | 'DATE' | 'DECIMAL' | 'DATETIME';\r\n\r\n @IsString()\r\n @IsOptional()\r\n index_type?: 'INDEX' | 'UNIQUE' | 'FULLTEXT';\r\n\r\n @IsBoolean()\r\n @IsOptional()\r\n is_required?: boolean;\r\n}\r\n\r\nexport class CreateJsonSchemaDto {\r\n @IsString()\r\n @IsNotEmpty()\r\n schemaCode!: string;\r\n\r\n @IsString() // ✅ เพิ่ม Validation\r\n @IsNotEmpty()\r\n tableName!: string;\r\n\r\n @IsInt()\r\n @IsOptional()\r\n version?: number;\r\n\r\n @IsObject()\r\n @IsNotEmpty()\r\n schemaDefinition!: Record;\r\n\r\n @IsObject()\r\n @IsOptional()\r\n uiSchema?: Record;\r\n\r\n @IsArray()\r\n @IsOptional()\r\n @ValidateNested({ each: true })\r\n @Type(() => VirtualColumnConfigDto)\r\n virtualColumns?: VirtualColumnConfigDto[];\r\n\r\n @IsObject()\r\n @IsOptional()\r\n migrationScript?: Record;\r\n\r\n @IsBoolean()\r\n @IsOptional()\r\n isActive?: boolean;\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\dto\\migrate-data.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\dto\\search-json-schema.dto.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unsafe-return", "severity": 2, "message": "Unsafe return of a value of type `any`.", "line": 14, "column": 5, "nodeType": "ReturnStatement", "messageId": "unsafeReturn", "endLine": 14, "endColumn": 18 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { IsString, IsOptional, IsBoolean, IsInt } from 'class-validator';\r\nimport { Type, Transform } from 'class-transformer';\r\n\r\nexport class SearchJsonSchemaDto {\r\n @IsString()\r\n @IsOptional()\r\n search?: string; // ค้นหาจาก schemaCode\r\n\r\n @IsBoolean()\r\n @IsOptional()\r\n @Transform(({ value }) => {\r\n if (value === 'true') return true;\r\n if (value === 'false') return false;\r\n return value;\r\n })\r\n isActive?: boolean;\r\n\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n page: number = 1;\r\n\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n limit: number = 20;\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\dto\\update-json-schema.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\entities\\json-schema.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\interfaces\\ui-schema.interface.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\interfaces\\validation-result.interface.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\json-schema.controller.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\json-schema.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\json-schema.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\json-schema.service.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 265, "column": 11, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 267, "endColumn": 6 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/modules/json-schema/json-schema.service.ts\r\n// บันทึกการแก้ไข: Fix TS2345 (undefined check)\r\n\r\nimport {\r\n BadRequestException,\r\n Injectable,\r\n Logger,\r\n NotFoundException,\r\n OnModuleInit,\r\n} from '@nestjs/common';\r\nimport { InjectRepository } from '@nestjs/typeorm';\r\nimport Ajv, { ValidateFunction } from 'ajv';\r\nimport addFormats from 'ajv-formats';\r\nimport { Repository } from 'typeorm';\r\n\r\nimport { CreateJsonSchemaDto } from './dto/create-json-schema.dto';\r\nimport { SearchJsonSchemaDto } from './dto/search-json-schema.dto';\r\nimport { UpdateJsonSchemaDto } from './dto/update-json-schema.dto';\r\nimport { JsonSchema } from './entities/json-schema.entity';\r\n\r\n// Services ย่อยที่แยกตามหน้าที่ (Single Responsibility)\r\nimport {\r\n JsonSecurityService,\r\n SecurityContext,\r\n} from './services/json-security.service';\r\nimport { UiSchemaService } from './services/ui-schema.service';\r\nimport { UiSchema } from './interfaces/ui-schema.interface';\r\nimport { VirtualColumnService } from './services/virtual-column.service';\r\n\r\nimport {\r\n ValidationErrorDetail,\r\n ValidationOptions,\r\n ValidationResult,\r\n} from './interfaces/validation-result.interface';\r\n\r\n@Injectable()\r\nexport class JsonSchemaService implements OnModuleInit {\r\n private ajv: Ajv;\r\n private validators = new Map(); // Cache สำหรับเก็บ Validator ที่ Compile แล้ว\r\n private readonly logger = new Logger(JsonSchemaService.name);\r\n\r\n // ค่า Default สำหรับการตรวจสอบข้อมูล\r\n private readonly defaultOptions: ValidationOptions = {\r\n removeAdditional: true, // ลบฟิลด์เกิน\r\n coerceTypes: true, // แปลงชนิดข้อมูลอัตโนมัติ (เช่น \"123\" -> 123)\r\n useDefaults: true, // ใส่ค่า Default ถ้าไม่มีข้อมูล\r\n };\r\n\r\n constructor(\r\n @InjectRepository(JsonSchema)\r\n private readonly jsonSchemaRepository: Repository,\r\n private readonly virtualColumnService: VirtualColumnService,\r\n private readonly uiSchemaService: UiSchemaService,\r\n private readonly jsonSecurityService: JsonSecurityService\r\n ) {\r\n // กำหนดค่าเริ่มต้นให้กับ AJV Validation Engine\r\n this.ajv = new Ajv({\r\n allErrors: true, // แสดง Error ทั้งหมด ไม่หยุดแค่จุดแรก\r\n strict: false, // ไม่เคร่งครัดเกินไป (ยอมรับ Keyword แปลกๆ เช่น ui:widget)\r\n coerceTypes: true,\r\n useDefaults: true,\r\n removeAdditional: true,\r\n });\r\n addFormats(this.ajv); // เพิ่ม Format มาตรฐาน (email, date, uri ฯลฯ)\r\n this.registerCustomValidators(); // ลงทะเบียน Validator เฉพาะของโปรเจกต์\r\n }\r\n\r\n async onModuleInit() {\r\n // สามารถโหลด Schema ที่ Active ทั้งหมดมา Cache ไว้ล่วงหน้าได้ที่นี่ เพื่อความเร็วในการตอบสนองครั้งแรก\r\n }\r\n\r\n /**\r\n * ลงทะเบียน Custom Validators เฉพาะสำหรับ LCBP3\r\n */\r\n private registerCustomValidators() {\r\n // 1. ตรวจสอบรูปแบบเลขที่เอกสาร (เช่น TEAM-RFA-STR-0001)\r\n this.ajv.addFormat('document-number', {\r\n type: 'string',\r\n validate: (value: string) => {\r\n // Regex อย่างง่าย: กลุ่มตัวอักษรขีดคั่นด้วย -\r\n return /^[A-Z0-9]{2,10}-[A-Z]{2,5}(-[A-Z0-9]{2,5})?-\\d{4}-\\d{3,5}$/.test(\r\n value\r\n );\r\n },\r\n });\r\n\r\n // 2. Keyword สำหรับระบุ Role ที่จำเป็น (ใช้ร่วมกับ Security Service)\r\n this.ajv.addKeyword({\r\n keyword: 'requiredRole',\r\n type: 'string',\r\n metaSchema: { type: 'string' },\r\n validate: (_schema: string, _data: unknown) => true, // ผ่านเสมอในขั้น AJV (Security Service จะจัดการเอง)\r\n });\r\n }\r\n\r\n /**\r\n * สร้าง Schema ใหม่ พร้อมจัดการ Version, UI Schema และ Virtual Columns\r\n */\r\n async create(createDto: CreateJsonSchemaDto): Promise {\r\n // 1. ตรวจสอบความถูกต้องของ JSON Schema Definition (AJV Syntax)\r\n try {\r\n this.ajv.compile(createDto.schemaDefinition);\r\n } catch (error: unknown) {\r\n throw new BadRequestException(\r\n `Invalid JSON Schema format: ${error instanceof Error ? error.message : String(error)}`\r\n );\r\n }\r\n\r\n // 2. จัดการ UI Schema\r\n if (createDto.uiSchema) {\r\n // ถ้าส่งมา ให้ตรวจสอบความถูกต้องเทียบกับ Data Schema\r\n this.uiSchemaService.validateUiSchema(\r\n createDto.uiSchema as unknown as UiSchema,\r\n createDto.schemaDefinition\r\n );\r\n } else {\r\n // ถ้าไม่ส่งมา ให้สร้าง UI Schema พื้นฐานให้อัตโนมัติ\r\n createDto.uiSchema = this.uiSchemaService.generateDefaultUiSchema(\r\n createDto.schemaDefinition\r\n );\r\n }\r\n\r\n // 3. จัดการ Versioning อัตโนมัติ (Auto-increment)\r\n const latestSchema = await this.jsonSchemaRepository.findOne({\r\n where: { schemaCode: createDto.schemaCode },\r\n order: { version: 'DESC' },\r\n });\r\n\r\n let newVersion = 1;\r\n if (latestSchema) {\r\n // ถ้าผู้ใช้ไม่ระบุ Version หรือระบุมาน้อยกว่าล่าสุด ให้ +1\r\n if (!createDto.version || createDto.version <= latestSchema.version) {\r\n newVersion = latestSchema.version + 1;\r\n } else {\r\n newVersion = createDto.version;\r\n }\r\n } else if (createDto.version) {\r\n newVersion = createDto.version;\r\n }\r\n\r\n // 4. บันทึกลงฐานข้อมูล\r\n const newSchema = this.jsonSchemaRepository.create({\r\n ...createDto,\r\n version: newVersion,\r\n });\r\n\r\n const savedSchema = await this.jsonSchemaRepository.save(newSchema);\r\n\r\n // ล้าง Cache เพื่อให้โหลดตัวใหม่ในครั้งถัดไป\r\n this.validators.delete(savedSchema.schemaCode);\r\n\r\n this.logger.log(\r\n `Schema '${savedSchema.schemaCode}' created (v${savedSchema.version})`\r\n );\r\n\r\n // 5. สร้าง/อัปเดต Virtual Columns บน Database จริง (Performance Optimization)\r\n // Fix TS2345: Add empty array fallback\r\n if (savedSchema.virtualColumns && savedSchema.virtualColumns.length > 0) {\r\n await this.virtualColumnService.setupVirtualColumns(\r\n savedSchema.tableName,\r\n savedSchema.virtualColumns || []\r\n );\r\n }\r\n\r\n return savedSchema;\r\n }\r\n\r\n /**\r\n * ค้นหา Schema ทั้งหมด (Pagination & Filter)\r\n */\r\n async findAll(searchDto: SearchJsonSchemaDto) {\r\n const { search, isActive, page = 1, limit = 20 } = searchDto;\r\n const skip = (page - 1) * limit;\r\n\r\n const query = this.jsonSchemaRepository.createQueryBuilder('schema');\r\n\r\n if (search) {\r\n query.andWhere('schema.schemaCode LIKE :search', {\r\n search: `%${search}%`,\r\n });\r\n }\r\n\r\n if (isActive !== undefined) {\r\n query.andWhere('schema.isActive = :isActive', { isActive });\r\n }\r\n\r\n // เรียงตาม Code ก่อน แล้วตามด้วย Version ล่าสุด\r\n query.orderBy('schema.schemaCode', 'ASC');\r\n query.addOrderBy('schema.version', 'DESC');\r\n\r\n const [items, total] = await query.skip(skip).take(limit).getManyAndCount();\r\n\r\n return {\r\n data: items,\r\n meta: {\r\n total,\r\n page,\r\n limit,\r\n totalPages: Math.ceil(total / limit),\r\n },\r\n };\r\n }\r\n\r\n /**\r\n * ดึงข้อมูล Schema ตาม ID\r\n */\r\n async findOne(id: number): Promise {\r\n const schema = await this.jsonSchemaRepository.findOne({ where: { id } });\r\n if (!schema) {\r\n throw new NotFoundException(`JsonSchema with ID ${id} not found`);\r\n }\r\n return schema;\r\n }\r\n\r\n /**\r\n * ดึงข้อมูล Schema ตาม Code และ Version (สำหรับ Migration)\r\n */\r\n async findOneByCodeAndVersion(\r\n code: string,\r\n version: number\r\n ): Promise {\r\n const schema = await this.jsonSchemaRepository.findOne({\r\n where: { schemaCode: code, version },\r\n });\r\n\r\n if (!schema) {\r\n throw new NotFoundException(\r\n `JsonSchema '${code}' version ${version} not found`\r\n );\r\n }\r\n return schema;\r\n }\r\n\r\n /**\r\n * ดึง Schema เวอร์ชันล่าสุดที่ Active (สำหรับใช้งานทั่วไป)\r\n */\r\n async findLatestByCode(code: string): Promise {\r\n const schema = await this.jsonSchemaRepository.findOne({\r\n where: { schemaCode: code, isActive: true },\r\n order: { version: 'DESC' },\r\n });\r\n\r\n if (!schema) {\r\n throw new NotFoundException(\r\n `Active JsonSchema with code '${code}' not found`\r\n );\r\n }\r\n return schema;\r\n }\r\n\r\n /**\r\n * [CORE FUNCTION] ตรวจสอบข้อมูล (Validate), ทำความสะอาด (Sanitize) และเข้ารหัส (Encrypt)\r\n * ใช้สำหรับ \"ขาเข้า\" (Write) ก่อนบันทึกลง Database\r\n */\r\n async validateData(\r\n schemaCode: string,\r\n data: Record,\r\n _options: ValidationOptions = {}\r\n ): Promise {\r\n // 1. ดึงและ Compile Validator\r\n const validate = await this.getValidator(schemaCode);\r\n const schema = await this.findLatestByCode(schemaCode); // ดึง Full Schema เพื่อใช้ Config อื่นๆ\r\n\r\n // 2. สำเนาข้อมูลเพื่อป้องกัน Side Effect และเตรียมสำหรับ AJV Mutation (Sanitization)\r\n const dataToValidate: Record = JSON.parse(\r\n JSON.stringify(data)\r\n );\r\n\r\n // 3. เริ่มการตรวจสอบ (AJV จะทำการ Coerce Type และ Remove Additional Properties ให้ด้วย)\r\n const valid = validate(dataToValidate);\r\n\r\n // 4. จัดการกรณีข้อมูลไม่ถูกต้อง\r\n if (!valid) {\r\n const errors: ValidationErrorDetail[] = (validate.errors || []).map(\r\n (err) => ({\r\n field: err.instancePath || 'root',\r\n message: err.message || 'Validation error',\r\n value: err.params,\r\n })\r\n );\r\n\r\n return {\r\n isValid: false,\r\n errors,\r\n sanitizedData: null,\r\n };\r\n }\r\n\r\n // 5. เข้ารหัสข้อมูล (Encryption) สำหรับ Field ที่มีความลับ (x-encrypt: true)\r\n const secureData = this.jsonSecurityService.encryptFields(\r\n dataToValidate,\r\n schema.schemaDefinition\r\n );\r\n\r\n return {\r\n isValid: true,\r\n errors: [],\r\n sanitizedData: secureData, // ข้อมูลนี้สะอาดและปลอดภัย พร้อมบันทึก\r\n };\r\n }\r\n\r\n /**\r\n * [CORE FUNCTION] อ่านข้อมูล, ถอดรหัส (Decrypt) และกรองตามสิทธิ์ (Filter)\r\n * ใช้สำหรับ \"ขาออก\" (Read) ก่อนส่งให้ Frontend\r\n */\r\n async processReadData(\r\n schemaCode: string,\r\n data: Record,\r\n userContext: SecurityContext\r\n ): Promise> {\r\n if (!data) return data;\r\n\r\n // ดึง Schema เพื่อดู Config การถอดรหัสและการมองเห็น\r\n const schema = await this.findLatestByCode(schemaCode);\r\n\r\n return this.jsonSecurityService.decryptAndFilterFields(\r\n data,\r\n schema.schemaDefinition,\r\n userContext\r\n );\r\n }\r\n\r\n /**\r\n * Helper: ดึงและ Cache AJV Validator Function เพื่อประสิทธิภาพ\r\n */\r\n private async getValidator(schemaCode: string): Promise {\r\n let validate = this.validators.get(schemaCode);\r\n\r\n if (!validate) {\r\n const schema = await this.findLatestByCode(schemaCode);\r\n try {\r\n validate = this.ajv.compile(schema.schemaDefinition);\r\n this.validators.set(schemaCode, validate);\r\n } catch (error: unknown) {\r\n throw new BadRequestException(\r\n `Invalid Schema Definition for '${schemaCode}': ${error instanceof Error ? error.message : String(error)}`\r\n );\r\n }\r\n }\r\n return validate;\r\n }\r\n\r\n /**\r\n * Wrapper เก่าสำหรับ Backward Compatibility (ถ้ามีโค้ดเก่าเรียกใช้)\r\n */\r\n async validate(\r\n schemaCode: string,\r\n data: Record\r\n ): Promise {\r\n const result = await this.validateData(schemaCode, data);\r\n if (!result.isValid) {\r\n const errorMsg = result.errors\r\n .map((e) => `${e.field}: ${e.message}`)\r\n .join(', ');\r\n throw new BadRequestException(`JSON Validation Failed: ${errorMsg}`);\r\n }\r\n return true;\r\n }\r\n\r\n /**\r\n * อัปเดตข้อมูล Schema และจัดการผลกระทบ (Virtual Columns / UI Schema)\r\n */\r\n async update(\r\n id: number,\r\n updateDto: UpdateJsonSchemaDto\r\n ): Promise {\r\n const schema = await this.findOne(id);\r\n\r\n // ตรวจสอบ JSON Schema\r\n if (updateDto.schemaDefinition) {\r\n try {\r\n this.ajv.compile(updateDto.schemaDefinition);\r\n } catch (error: unknown) {\r\n throw new BadRequestException(\r\n `Invalid JSON Schema format: ${error instanceof Error ? error.message : String(error)}`\r\n );\r\n }\r\n this.validators.delete(schema.schemaCode); // เคลียร์ Cache เก่า\r\n }\r\n\r\n // ตรวจสอบ UI Schema\r\n if (updateDto.uiSchema) {\r\n this.uiSchemaService.validateUiSchema(\r\n updateDto.uiSchema as unknown as UiSchema,\r\n updateDto.schemaDefinition || schema.schemaDefinition\r\n );\r\n }\r\n\r\n const updatedSchema = this.jsonSchemaRepository.merge(schema, updateDto);\r\n const savedSchema = await this.jsonSchemaRepository.save(updatedSchema);\r\n\r\n // อัปเดต Virtual Columns ใน Database ถ้ามีการเปลี่ยนแปลง Config\r\n // Fix TS2345: Add empty array fallback\r\n if (updateDto.virtualColumns && updatedSchema.virtualColumns) {\r\n await this.virtualColumnService.setupVirtualColumns(\r\n savedSchema.tableName,\r\n savedSchema.virtualColumns || []\r\n );\r\n }\r\n\r\n return savedSchema;\r\n }\r\n\r\n /**\r\n * ลบ Schema (Hard Delete)\r\n */\r\n async remove(id: number): Promise {\r\n const schema = await this.findOne(id);\r\n this.validators.delete(schema.schemaCode);\r\n await this.jsonSchemaRepository.remove(schema);\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\services\\json-security.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\services\\schema-migration.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\services\\ui-schema.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\services\\virtual-column.service.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 101, "column": 11, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 104, "endColumn": 7 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access [0] on an `any` value.", "line": 106, "column": 16, "nodeType": "Literal", "messageId": "unsafeMemberExpression", "endLine": 106, "endColumn": 17 } ], "suppressedMessages": [], "errorCount": 2, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/modules/json-schema/services/virtual-column.service.ts\r\nimport { Injectable, Logger } from '@nestjs/common';\r\nimport { DataSource, QueryRunner } from 'typeorm';\r\nimport { VirtualColumnConfig } from '../entities/json-schema.entity';\r\n\r\n@Injectable()\r\nexport class VirtualColumnService {\r\n private readonly logger = new Logger(VirtualColumnService.name);\r\n\r\n constructor(private readonly dataSource: DataSource) {}\r\n\r\n /**\r\n * สร้าง/อัปเดต Virtual Columns และ Index บน Database จริง\r\n */\r\n async setupVirtualColumns(tableName: string, configs: VirtualColumnConfig[]) {\r\n if (!configs || configs.length === 0) return;\r\n\r\n // ใช้ QueryRunner เพื่อให้จัดการ Transaction หรือ Connection ได้ละเอียด\r\n const queryRunner = this.dataSource.createQueryRunner();\r\n await queryRunner.connect();\r\n\r\n try {\r\n this.logger.log(\r\n `Start setting up virtual columns for table '${tableName}'...`\r\n );\r\n\r\n // 1. ตรวจสอบว่าตารางมีอยู่จริงไหม\r\n const tableExists = await queryRunner.hasTable(tableName);\r\n if (!tableExists) {\r\n this.logger.warn(\r\n `Table '${tableName}' not found. Skipping virtual columns.`\r\n );\r\n return;\r\n }\r\n\r\n for (const config of configs) {\r\n await this.ensureVirtualColumn(queryRunner, tableName, config);\r\n\r\n if (config.index_type) {\r\n await this.ensureIndex(queryRunner, tableName, config);\r\n }\r\n }\r\n\r\n this.logger.log(\r\n `Finished setting up virtual columns for '${tableName}'.`\r\n );\r\n } catch (err: unknown) {\r\n this.logger.error(\r\n `Failed to setup virtual columns: ${err instanceof Error ? err.message : String(err)}`,\r\n err instanceof Error ? err.stack : undefined\r\n );\r\n throw err;\r\n } finally {\r\n await queryRunner.release();\r\n }\r\n }\r\n\r\n /**\r\n * สร้าง Column ถ้ายังไม่มี\r\n */\r\n private async ensureVirtualColumn(\r\n queryRunner: QueryRunner,\r\n tableName: string,\r\n config: VirtualColumnConfig\r\n ) {\r\n const hasColumn = await queryRunner.hasColumn(\r\n tableName,\r\n config.column_name\r\n );\r\n\r\n if (!hasColumn) {\r\n const sql = this.generateAddColumnSql(tableName, config);\r\n this.logger.log(`Executing: ${sql}`);\r\n await queryRunner.query(sql);\r\n } else {\r\n // TODO: (Advance) ถ้ามี Column แล้ว แต่ Definition เปลี่ยน อาจต้อง ALTER MODIFY\r\n this.logger.debug(\r\n `Column '${config.column_name}' already exists in '${tableName}'.`\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * สร้าง Index ถ้ายังไม่มี\r\n */\r\n private async ensureIndex(\r\n queryRunner: QueryRunner,\r\n tableName: string,\r\n config: VirtualColumnConfig\r\n ) {\r\n const indexName = `idx_${tableName}_${config.column_name}`;\r\n\r\n // ตรวจสอบว่า Index มีอยู่จริงไหม (Query จาก information_schema เพื่อความชัวร์)\r\n const checkIndexSql = `\r\n SELECT COUNT(1) as count\r\n FROM information_schema.STATISTICS\r\n WHERE table_schema = DATABASE()\r\n AND table_name = ?\r\n AND index_name = ?\r\n `;\r\n const result = await queryRunner.query(checkIndexSql, [\r\n tableName,\r\n indexName,\r\n ]);\r\n\r\n if (result[0].count == 0) {\r\n const sql = `CREATE ${config.index_type === 'UNIQUE' ? 'UNIQUE' : ''} INDEX ${indexName} ON ${tableName} (${config.column_name})`;\r\n this.logger.log(`Creating Index: ${sql}`);\r\n await queryRunner.query(sql);\r\n }\r\n }\r\n\r\n /**\r\n * Generate SQL สำหรับ MariaDB 10.11 Virtual Column\r\n * Syntax: ADD COLUMN name type GENERATED ALWAYS AS (expr) VIRTUAL\r\n */\r\n private generateAddColumnSql(\r\n tableName: string,\r\n config: VirtualColumnConfig\r\n ): string {\r\n const dbType = this.mapDataTypeToSql(config.data_type);\r\n // JSON_UNQUOTE(JSON_EXTRACT(details, '$.path'))\r\n // ใช้ 'details' เป็นชื่อ column JSON หลัก (ต้องตรงกับ Database Schema ที่ออกแบบไว้)\r\n const expression = `JSON_UNQUOTE(JSON_EXTRACT(details, '${config.json_path}'))`;\r\n\r\n // Handle Type Casting inside expression if needed,\r\n // but usually MariaDB handles string return from JSON_EXTRACT.\r\n // For INT/DATE, virtual column type definition enforces it.\r\n\r\n return `ALTER TABLE ${tableName} ADD COLUMN ${config.column_name} ${dbType} GENERATED ALWAYS AS (${expression}) VIRTUAL`;\r\n }\r\n\r\n private mapDataTypeToSql(type: string): string {\r\n switch (type) {\r\n case 'INT':\r\n return 'INT';\r\n case 'VARCHAR':\r\n return 'VARCHAR(255)';\r\n case 'BOOLEAN':\r\n return 'TINYINT(1)';\r\n case 'DATE':\r\n return 'DATE';\r\n case 'DATETIME':\r\n return 'DATETIME';\r\n case 'DECIMAL':\r\n return 'DECIMAL(10,2)';\r\n default:\r\n return 'VARCHAR(255)';\r\n }\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\create-discipline.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\create-sub-type.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\create-tag.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\save-number-format.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\search-tag.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\update-tag.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\entities\\discipline.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\entities\\tag.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\master.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\master.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\master.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\dto\\commit-batch.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\dto\\create-migration-error.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\dto\\enqueue-migration.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\dto\\import-correspondence.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\dto\\migration-queue-query.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\entities\\import-transaction.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\entities\\migration-error.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\entities\\migration-review-queue.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\migration.controller.spec.ts", "messages": [ { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 53, "column": 12, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 53, "endColumn": 40 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { MigrationController } from './migration.controller';\r\nimport { MigrationService } from './migration.service';\r\nimport { ImportCorrespondenceDto } from './dto/import-correspondence.dto';\r\n\r\ndescribe('MigrationController', () => {\r\n let controller: MigrationController;\r\n let service: MigrationService;\r\n\r\n beforeEach(async () => {\r\n const module: TestingModule = await Test.createTestingModule({\r\n controllers: [MigrationController],\r\n providers: [\r\n {\r\n provide: MigrationService,\r\n useValue: {\r\n importCorrespondence: jest\r\n .fn()\r\n .mockResolvedValue({ message: 'Success' }),\r\n },\r\n },\r\n ],\r\n }).compile();\r\n\r\n controller = module.get(MigrationController);\r\n service = module.get(MigrationService);\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(controller).toBeDefined();\r\n });\r\n\r\n it('should call importCorrespondence on service', async () => {\r\n const dto: ImportCorrespondenceDto = {\r\n document_number: 'DOC-001',\r\n subject: 'Legacy Record',\r\n category: 'Correspondence',\r\n source_file_path: '/staging_ai/test.pdf',\r\n migrated_by: 'SYSTEM_IMPORT',\r\n batch_id: 'batch1',\r\n project_id: 1,\r\n };\r\n\r\n const idempotencyKey = 'key123';\r\n const user = { userId: 5 };\r\n\r\n const result = await controller.importCorrespondence(\r\n dto,\r\n idempotencyKey,\r\n user\r\n );\r\n expect(result).toEqual({ message: 'Success' });\r\n expect(service.importCorrespondence).toHaveBeenCalledWith(\r\n dto,\r\n idempotencyKey,\r\n 5\r\n );\r\n });\r\n});\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\migration.controller.ts", "messages": [ { "ruleId": "@typescript-eslint/require-await", "severity": 2, "message": "Async method 'getStagingFile' has no 'await' expression.", "line": 166, "column": 3, "nodeType": "FunctionExpression", "messageId": "missingAwait", "endLine": 166, "endColumn": 23, "suggestions": [ { "messageId": "removeAsync", "fix": { "range": [5026, 5032], "text": "" }, "desc": "Remove 'async'." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import {\r\n Controller,\r\n Post,\r\n Body,\r\n Headers,\r\n UseGuards,\r\n Get,\r\n Param,\r\n Query,\r\n Res,\r\n ParseIntPipe,\r\n} from '@nestjs/common';\r\nimport { MigrationService } from './migration.service';\r\nimport { ImportCorrespondenceDto } from './dto/import-correspondence.dto';\r\nimport { EnqueueMigrationDto } from './dto/enqueue-migration.dto';\r\nimport { CommitBatchDto } from './dto/commit-batch.dto';\r\nimport { CreateMigrationErrorDto } from './dto/create-migration-error.dto';\r\nimport { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';\r\nimport { CurrentUser } from '../../common/decorators/current-user.decorator';\r\nimport { User } from '../user/entities/user.entity';\r\nimport {\r\n ApiTags,\r\n ApiOperation,\r\n ApiBearerAuth,\r\n ApiHeader,\r\n ApiQuery,\r\n ApiParam,\r\n} from '@nestjs/swagger';\r\nimport { MigrationQueueQueryDto } from './dto/migration-queue-query.dto';\r\nimport type { Response } from 'express';\r\n\r\n@ApiTags('Migration')\r\n@ApiBearerAuth()\r\n@Controller('migration')\r\nexport class MigrationController {\r\n constructor(private readonly migrationService: MigrationService) {}\r\n\r\n @Post('import')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({\r\n summary: 'Import generic legacy correspondence record via n8n integration',\r\n })\r\n @ApiHeader({\r\n name: 'Idempotency-Key',\r\n description:\r\n 'Unique key per document and batch to prevent duplicate inserts',\r\n required: true,\r\n })\r\n async importCorrespondence(\r\n @Body() dto: ImportCorrespondenceDto,\r\n @Headers('idempotency-key') idempotencyKey: string,\r\n @CurrentUser() user: User\r\n ) {\r\n const userId = user?.user_id || 5;\r\n return this.migrationService.importCorrespondence(\r\n dto,\r\n idempotencyKey,\r\n userId\r\n );\r\n }\r\n\r\n @Post('commit_batch')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({\r\n summary: 'Batch approve and import migration review queue items',\r\n })\r\n @ApiHeader({\r\n name: 'Idempotency-Key',\r\n description:\r\n 'Unique key for the entire batch to prevent duplicate execution',\r\n required: true,\r\n })\r\n async commitBatch(\r\n @Body() dto: CommitBatchDto,\r\n @Headers('idempotency-key') idempotencyKey: string,\r\n @CurrentUser() user: User\r\n ) {\r\n const userId = user?.user_id || 5;\r\n return this.migrationService.commitBatch(dto, idempotencyKey, userId);\r\n }\r\n\r\n @Post('queue')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({\r\n summary: 'Enqueue a record into the staging migration review queue',\r\n })\r\n async enqueueRecord(@Body() dto: EnqueueMigrationDto) {\r\n return this.migrationService.enqueueRecord(dto);\r\n }\r\n\r\n @Get('queue')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Get migration review queue' })\r\n async getReviewQueue(@Query() query: MigrationQueueQueryDto) {\r\n return this.migrationService.getReviewQueue(query);\r\n }\r\n\r\n @Get('queue/:id')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Get a specific queue item by ID' })\r\n @ApiParam({ name: 'id', type: Number })\r\n async getQueueItemById(@Param('id', ParseIntPipe) id: number) {\r\n return this.migrationService.getQueueItemById(id);\r\n }\r\n\r\n @Post('errors')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Log a migration error from n8n workflow' })\r\n async createError(@Body() dto: CreateMigrationErrorDto) {\r\n return this.migrationService.createError(dto);\r\n }\r\n\r\n @Get('errors')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Get migration errors' })\r\n @ApiQuery({ name: 'page', required: false, type: Number })\r\n @ApiQuery({ name: 'limit', required: false, type: Number })\r\n async getErrors(\r\n @Query('page') page?: number,\r\n @Query('limit') limit?: number\r\n ) {\r\n return this.migrationService.getErrors(page, limit);\r\n }\r\n\r\n @Post('queue/:id/approve')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Approve and import a queued migration item' })\r\n @ApiParam({ name: 'id', type: Number })\r\n @ApiHeader({\r\n name: 'Idempotency-Key',\r\n description:\r\n 'Unique key per document and batch to prevent duplicate inserts',\r\n required: true,\r\n })\r\n async approveQueueItem(\r\n @Param('id', ParseIntPipe) id: number,\r\n @Body() dto: ImportCorrespondenceDto,\r\n @Headers('idempotency-key') idempotencyKey: string,\r\n @CurrentUser() user: User\r\n ) {\r\n const userId = user?.user_id || 5;\r\n return this.migrationService.approveQueueItem(\r\n id,\r\n dto,\r\n idempotencyKey,\r\n userId\r\n );\r\n }\r\n\r\n @Post('queue/:id/reject')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Reject a queued migration item' })\r\n @ApiParam({ name: 'id', type: Number })\r\n async rejectQueueItem(\r\n @Param('id', ParseIntPipe) id: number,\r\n @CurrentUser() user: User\r\n ) {\r\n const userId = user?.user_id || 5;\r\n return this.migrationService.rejectQueueItem(id, userId);\r\n }\r\n\r\n @Get('staging-file')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Stream a file from staging' })\r\n @ApiQuery({ name: 'path', required: true, type: String })\r\n async getStagingFile(@Query('path') filePath: string, @Res() res: Response) {\r\n const stream = this.migrationService.getStagingFileStream(filePath);\r\n res.set({\r\n 'Content-Type': 'application/pdf',\r\n 'Content-Disposition': 'inline; filename=\"document.pdf\"',\r\n });\r\n stream.pipe(res);\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\migration.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\migration.service.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\migration.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\controllers\\health.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\dto\\set-maintenance.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\logger\\winston.config.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\monitoring.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\monitoring.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\monitoring.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\services\\metrics.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\dto\\create-notification.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\dto\\search-notification.dto.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unsafe-return", "severity": 2, "message": "Unsafe return of a value of type `any`.", "line": 20, "column": 5, "nodeType": "ReturnStatement", "messageId": "unsafeReturn", "endLine": 20, "endColumn": 18 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { IsInt, IsOptional, IsBoolean } from 'class-validator';\r\nimport { Type, Transform } from 'class-transformer';\r\n\r\nexport class SearchNotificationDto {\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n page: number = 1;\r\n\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n limit: number = 20;\r\n\r\n @IsOptional()\r\n @IsBoolean()\r\n @Transform(({ value }) => {\r\n if (value === 'true') return true;\r\n if (value === 'false') return false;\r\n return value;\r\n })\r\n isRead?: boolean; // กรอง: อ่านแล้ว/ยังไม่อ่าน\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\entities\\notification.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification-cleanup.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification.gateway.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification.processor.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\dto\\create-organization.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\dto\\search-organization.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\dto\\update-organization.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\entities\\organization-role.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\entities\\organization.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\organization.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\organization.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\organization.service.ts", "messages": [ { "ruleId": "prettier/prettier", "severity": 2, "message": "Insert `····`", "line": 76, "column": 1, "nodeType": null, "messageId": "insert", "endLine": 76, "endColumn": 1, "fix": { "range": [2499, 2499], "text": " " } } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 1, "fixableWarningCount": 0, "source": "import {\r\n Injectable,\r\n NotFoundException,\r\n ConflictException,\r\n} from '@nestjs/common';\r\nimport { InjectRepository } from '@nestjs/typeorm';\r\nimport { Repository } from 'typeorm';\r\nimport { Organization } from './entities/organization.entity';\r\nimport { CreateOrganizationDto } from './dto/create-organization.dto.js';\r\nimport { UpdateOrganizationDto } from './dto/update-organization.dto.js';\r\n\r\n@Injectable()\r\nexport class OrganizationService {\r\n constructor(\r\n @InjectRepository(Organization)\r\n private readonly orgRepo: Repository\r\n ) {}\r\n\r\n async create(dto: CreateOrganizationDto) {\r\n const existing = await this.orgRepo.findOne({\r\n where: { organizationCode: dto.organizationCode },\r\n });\r\n if (existing) {\r\n throw new ConflictException(\r\n `Organization Code \"${dto.organizationCode}\" already exists`\r\n );\r\n }\r\n const org = this.orgRepo.create(dto);\r\n return this.orgRepo.save(org);\r\n }\r\n\r\n async findAll(params?: {\r\n search?: string;\r\n roleId?: number;\r\n projectId?: number;\r\n page?: number;\r\n limit?: number;\r\n }) {\r\n const { search, roleId, projectId, page = 1, limit = 100 } = params || {};\r\n const skip = (page - 1) * limit;\r\n\r\n // Start with a basic query builder to handle dynamic conditions easily\r\n const queryBuilder = this.orgRepo.createQueryBuilder('org');\r\n\r\n if (search) {\r\n queryBuilder.andWhere(\r\n '(org.organizationCode LIKE :search OR org.organizationName LIKE :search)',\r\n { search: `%${search}%` }\r\n );\r\n }\r\n\r\n // [Refactor] Support filtering by roleId (e.g., getting all CONTRACTORS)\r\n if (roleId) {\r\n // Assuming there is a relation or a way to filter by role.\r\n // If Organization has a roleId column directly:\r\n queryBuilder.andWhere('org.roleId = :roleId', { roleId });\r\n }\r\n\r\n // [New] Support filtering by projectId (e.g. organizations in a project)\r\n // Assuming a Many-to-Many or One-to-Many relation exists via ProjectOrganization\r\n if (projectId) {\r\n // Use raw join to avoid circular dependency with ProjectOrganization entity\r\n queryBuilder.innerJoin(\r\n 'project_organizations',\r\n 'po',\r\n 'po.organization_id = org.id AND po.project_id = :projectId',\r\n { projectId }\r\n );\r\n }\r\n\r\n queryBuilder.orderBy('org.organizationCode', 'ASC').skip(skip).take(limit);\r\n\r\n const [data, total] = await queryBuilder.getManyAndCount();\r\n\r\n // Debug logging\r\n// console.log(`[OrganizationService] Found ${total} organizations`);\r\n\r\n return {\r\n data,\r\n meta: {\r\n total,\r\n page,\r\n limit,\r\n totalPages: Math.ceil(total / limit),\r\n },\r\n };\r\n }\r\n\r\n async findOne(id: number) {\r\n const org = await this.orgRepo.findOne({ where: { id } });\r\n if (!org) throw new NotFoundException(`Organization ID ${id} not found`);\r\n return org;\r\n }\r\n\r\n async findOneByUuid(uuid: string) {\r\n const org = await this.orgRepo.findOne({ where: { uuid } });\r\n if (!org)\r\n throw new NotFoundException(`Organization UUID ${uuid} not found`);\r\n return org;\r\n }\r\n\r\n async update(uuid: string, dto: UpdateOrganizationDto) {\r\n const org = await this.findOneByUuid(uuid);\r\n Object.assign(org, dto);\r\n return this.orgRepo.save(org);\r\n }\r\n\r\n async remove(uuid: string) {\r\n const org = await this.findOneByUuid(uuid);\r\n return this.orgRepo.remove(org);\r\n }\r\n\r\n async findAllActive() {\r\n return this.orgRepo.find({\r\n where: { isActive: true },\r\n order: { organizationCode: 'ASC' },\r\n });\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\dto\\create-project.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\dto\\search-project.dto.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unsafe-return", "severity": 2, "message": "Unsafe return of a value of type `any`.", "line": 14, "column": 5, "nodeType": "ReturnStatement", "messageId": "unsafeReturn", "endLine": 14, "endColumn": 18 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { IsString, IsOptional, IsInt, IsBoolean } from 'class-validator';\r\nimport { Type, Transform } from 'class-transformer';\r\n\r\nexport class SearchProjectDto {\r\n @IsString()\r\n @IsOptional()\r\n search?: string; // ค้นหาจาก Project Code หรือ Name\r\n\r\n @IsOptional()\r\n @IsBoolean()\r\n @Transform(({ value }) => {\r\n if (value === 'true') return true;\r\n if (value === 'false') return false;\r\n return value;\r\n })\r\n isActive?: boolean; // กรองตามสถานะ Active\r\n\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n page: number = 1;\r\n\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n limit: number = 20;\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\dto\\update-project.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\entities\\project-organization.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\entities\\project.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\project.controller.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\project.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\project.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\project.service.spec.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unsafe-call", "severity": 2, "message": "Unsafe call of an `any` typed value.", "line": 64, "column": 7, "nodeType": "MemberExpression", "messageId": "unsafeCall", "endLine": 66, "endColumn": 43 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .getManyAndCount on an `any` value.", "line": 66, "column": 10, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 66, "endColumn": 25 } ], "suppressedMessages": [], "errorCount": 2, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { ProjectService } from './project.service';\r\nimport { Project } from './entities/project.entity';\r\nimport { OrganizationService } from '../organization/organization.service';\r\n\r\ndescribe('ProjectService', () => {\r\n let service: ProjectService;\r\n let mockProjectRepository: Record;\r\n let mockOrganizationService: Record;\r\n\r\n beforeEach(async () => {\r\n mockProjectRepository = {\r\n find: jest.fn(),\r\n findOne: jest.fn(),\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n softDelete: jest.fn(),\r\n createQueryBuilder: jest.fn(() => ({\r\n leftJoinAndSelect: jest.fn().mockReturnThis(),\r\n where: jest.fn().mockReturnThis(),\r\n andWhere: jest.fn().mockReturnThis(),\r\n orderBy: jest.fn().mockReturnThis(),\r\n skip: jest.fn().mockReturnThis(),\r\n take: jest.fn().mockReturnThis(),\r\n getManyAndCount: jest.fn().mockResolvedValue([[], 0]),\r\n })),\r\n };\r\n\r\n mockOrganizationService = {\r\n findAllActive: jest.fn(),\r\n };\r\n\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n ProjectService,\r\n {\r\n provide: getRepositoryToken(Project),\r\n useValue: mockProjectRepository,\r\n },\r\n {\r\n provide: OrganizationService,\r\n useValue: mockOrganizationService,\r\n },\r\n ],\r\n }).compile();\r\n\r\n service = module.get(ProjectService);\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('findAll', () => {\r\n it('should return paginated projects', async () => {\r\n const mockProjects = [\r\n {\r\n project_id: 1,\r\n project_code: 'PROJ-001',\r\n project_name: 'Test Project',\r\n },\r\n ];\r\n mockProjectRepository\r\n .createQueryBuilder()\r\n .getManyAndCount.mockResolvedValue([mockProjects, 1]);\r\n\r\n const result = await service.findAll({ page: 1, limit: 10 });\r\n\r\n expect(result.data).toBeDefined();\r\n expect(result.meta).toBeDefined();\r\n });\r\n });\r\n\r\n describe('findAllOrganizations', () => {\r\n it('should return all organizations', async () => {\r\n const mockOrgs = [{ organization_id: 1, name: 'Test Org' }];\r\n mockOrganizationService.findAllActive.mockResolvedValue(mockOrgs);\r\n\r\n const result = await service.findAllOrganizations();\r\n\r\n expect(mockOrganizationService.findAllActive).toHaveBeenCalled();\r\n expect(result).toEqual(mockOrgs);\r\n });\r\n });\r\n});\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\project.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\create-rfa-revision.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\create-rfa-workflow.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\create-rfa.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\search-rfa.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\submit-rfa.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\update-rfa.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-approve-code.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-item.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-revision.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-status-code.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-type.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-workflow-template-step.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-workflow-template.entity.ts", "messages": [ { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 26, "column": 35, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 26, "endColumn": 38, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [638, 641], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [638, 641], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import {\r\n Entity,\r\n PrimaryGeneratedColumn,\r\n Column,\r\n CreateDateColumn,\r\n UpdateDateColumn,\r\n OneToMany,\r\n} from 'typeorm';\r\nimport { RfaWorkflowTemplateStep } from './rfa-workflow-template-step.entity';\r\n\r\n@Entity('rfa_workflow_templates')\r\nexport class RfaWorkflowTemplate {\r\n @PrimaryGeneratedColumn()\r\n id!: number;\r\n\r\n @Column({ name: 'template_name', length: 100 })\r\n templateName!: string;\r\n\r\n @Column({ type: 'text', nullable: true })\r\n description?: string;\r\n\r\n @Column({ name: 'is_active', default: true })\r\n isActive!: boolean;\r\n\r\n @Column({ type: 'json', nullable: true })\r\n workflowConfig?: Record; // Configuration เพิ่มเติม\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date;\r\n\r\n @UpdateDateColumn({ name: 'updated_at' })\r\n updatedAt!: Date;\r\n\r\n // Relations\r\n @OneToMany(() => RfaWorkflowTemplateStep, (step) => step.template, {\r\n cascade: true,\r\n })\r\n steps!: RfaWorkflowTemplateStep[];\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-workflow.entity.ts", "messages": [ { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 55, "column": 33, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 55, "endColumn": 36, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [1420, 1423], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [1420, 1423], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/modules/rfa/entities/rfa-workflow.entity.ts\r\nimport {\r\n Column,\r\n CreateDateColumn,\r\n Entity,\r\n JoinColumn,\r\n ManyToOne,\r\n PrimaryGeneratedColumn,\r\n UpdateDateColumn,\r\n} from 'typeorm';\r\nimport { Organization } from '../../organization/entities/organization.entity';\r\nimport { User } from '../../user/entities/user.entity';\r\nimport { RfaRevision } from './rfa-revision.entity';\r\nimport { RfaActionType } from './rfa-workflow-template-step.entity'; // ✅ Import Enum\r\n\r\n@Entity('rfa_workflows')\r\nexport class RfaWorkflow {\r\n @PrimaryGeneratedColumn()\r\n id!: number;\r\n\r\n @Column({ name: 'rfa_revision_id' })\r\n rfaRevisionId!: number;\r\n\r\n @Column({ name: 'step_number' })\r\n stepNumber!: number;\r\n\r\n @Column({ name: 'organization_id' })\r\n organizationId!: number;\r\n\r\n @Column({ name: 'assigned_to', nullable: true })\r\n assignedTo?: number;\r\n\r\n @Column({\r\n name: 'action_type',\r\n type: 'enum',\r\n enum: RfaActionType, // ✅ Use Shared Enum\r\n nullable: true,\r\n })\r\n actionType?: RfaActionType;\r\n\r\n @Column({\r\n type: 'enum',\r\n enum: ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'REJECTED'],\r\n nullable: true,\r\n })\r\n status?: string;\r\n\r\n @Column({ type: 'text', nullable: true })\r\n comments?: string;\r\n\r\n @Column({ name: 'completed_at', type: 'datetime', nullable: true })\r\n completedAt?: Date;\r\n\r\n @Column({ type: 'json', nullable: true })\r\n stateContext?: Record;\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date;\r\n\r\n @UpdateDateColumn({ name: 'updated_at' })\r\n updatedAt!: Date;\r\n\r\n // Relations\r\n @ManyToOne(() => RfaRevision, (rev) => rev.workflows, { onDelete: 'CASCADE' })\r\n @JoinColumn({ name: 'rfa_revision_id' })\r\n rfaRevision!: RfaRevision;\r\n\r\n @ManyToOne(() => Organization)\r\n @JoinColumn({ name: 'organization_id' })\r\n organization!: Organization;\r\n\r\n @ManyToOne(() => User)\r\n @JoinColumn({ name: 'assigned_to' })\r\n assignee?: User;\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\rfa-workflow.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\rfa.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\rfa.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\rfa.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\search\\dto\\search-query.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\search\\search.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\search\\search.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\search\\search.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\dto\\create-transmittal.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\dto\\search-transmittal.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\dto\\update-transmittal.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\entities\\transmittal-item.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\entities\\transmittal.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\transmittal.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\transmittal.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\transmittal.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\assign-role.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\bulk-assignment.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\create-user.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\search-user.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\update-preference.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\update-user.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\entities\\permission.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\entities\\role.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\entities\\user-assignment.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\entities\\user-preference.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\entities\\user.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user-assignment.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user-preference.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user.controller.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user.service.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dsl\\parser.service.spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dsl\\parser.service.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 24, "column": 13, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 24, "endColumn": 41 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 185, "column": 13, "nodeType": "VariableDeclarator", "messageId": "anyAssignment", "endLine": 185, "endColumn": 41 } ], "suppressedMessages": [], "errorCount": 2, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Injectable, Logger, BadRequestException } from '@nestjs/common';\r\nimport { InjectRepository } from '@nestjs/typeorm';\r\nimport { Repository } from 'typeorm';\r\nimport { WorkflowDslSchema, WorkflowDsl } from './workflow-dsl.schema';\r\nimport { WorkflowDefinition } from '../entities/workflow-definition.entity';\r\n\r\n@Injectable()\r\nexport class WorkflowDslParser {\r\n private readonly logger = new Logger(WorkflowDslParser.name);\r\n\r\n constructor(\r\n @InjectRepository(WorkflowDefinition)\r\n private workflowDefRepo: Repository\r\n ) {}\r\n\r\n /**\r\n * Parse และ Validate Workflow DSL from JSON string\r\n * @param dslJson JSON string ของ Workflow DSL\r\n * @returns WorkflowDefinition entity พร้อมบันทึกลง database\r\n */\r\n async parse(dslJson: string): Promise {\r\n try {\r\n // Step 1: Parse JSON\r\n const rawDsl = JSON.parse(dslJson);\r\n\r\n // Step 2: Validate with Zod schema\r\n const dsl = WorkflowDslSchema.parse(rawDsl);\r\n\r\n // Step 3: Validate state machine integrity\r\n this.validateStateMachine(dsl);\r\n\r\n // Step 4: Build WorkflowDefinition entity\r\n const definition = this.buildDefinition(dsl);\r\n\r\n // Step 5: Save to database\r\n return await this.workflowDefRepo.save(definition);\r\n } catch (error: unknown) {\r\n if (error instanceof SyntaxError) {\r\n throw new BadRequestException(`Invalid JSON: ${error.message}`);\r\n }\r\n const err = error as {\r\n name?: string;\r\n errors?: unknown;\r\n message?: string;\r\n };\r\n if (err.name === 'ZodError') {\r\n throw new BadRequestException(\r\n `Invalid workflow DSL: ${JSON.stringify(err.errors)}`\r\n );\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Validate state machine integrity\r\n * ตรวจสอบว่า state machine ถูกต้องตามหลักการ:\r\n * - All states in transitions must exist in states array\r\n * - Initial state must exist\r\n * - All final states must exist\r\n * - No dead-end states (states with no outgoing transitions except final states)\r\n */\r\n private validateStateMachine(dsl: WorkflowDsl): void {\r\n const stateSet = new Set(dsl.states);\r\n const finalStateSet = new Set(dsl.finalStates);\r\n\r\n // 1. Validate initial state\r\n if (!stateSet.has(dsl.initialState)) {\r\n throw new BadRequestException(\r\n `Initial state \"${dsl.initialState}\" not found in states array`\r\n );\r\n }\r\n\r\n // 2. Validate final states\r\n dsl.finalStates.forEach((state) => {\r\n if (!stateSet.has(state)) {\r\n throw new BadRequestException(\r\n `Final state \"${state}\" not found in states array`\r\n );\r\n }\r\n });\r\n\r\n // 3. Validate transitions\r\n const statesWithOutgoing = new Set();\r\n\r\n dsl.transitions.forEach((transition, index) => {\r\n // Check 'from' state\r\n if (!stateSet.has(transition.from)) {\r\n throw new BadRequestException(\r\n `Transition ${index}: 'from' state \"${transition.from}\" not found in states array`\r\n );\r\n }\r\n\r\n // Check 'to' state\r\n if (!stateSet.has(transition.to)) {\r\n throw new BadRequestException(\r\n `Transition ${index}: 'to' state \"${transition.to}\" not found in states array`\r\n );\r\n }\r\n\r\n // Track states with outgoing transitions\r\n statesWithOutgoing.add(transition.from);\r\n });\r\n\r\n // 4. Check for dead-end states (except final states)\r\n const nonFinalStates = dsl.states.filter(\r\n (state) => !finalStateSet.has(state)\r\n );\r\n\r\n nonFinalStates.forEach((state) => {\r\n if (!statesWithOutgoing.has(state) && state !== dsl.initialState) {\r\n this.logger.warn(\r\n `Warning: State \"${state}\" has no outgoing transitions (potential dead-end)`\r\n );\r\n }\r\n });\r\n\r\n // 5. Check for duplicate transitions\r\n const transitionKeys = new Set();\r\n dsl.transitions.forEach((transition) => {\r\n const key = `${transition.from}-${transition.trigger}-${transition.to}`;\r\n if (transitionKeys.has(key)) {\r\n throw new BadRequestException(\r\n `Duplicate transition: ${transition.from} --[${transition.trigger}]--> ${transition.to}`\r\n );\r\n }\r\n transitionKeys.add(key);\r\n });\r\n\r\n this.logger.log(\r\n `Workflow \"${dsl.name}\" v${dsl.version} validated successfully`\r\n );\r\n }\r\n\r\n /**\r\n * Build WorkflowDefinition entity from validated DSL\r\n */\r\n private buildDefinition(dsl: WorkflowDsl): WorkflowDefinition {\r\n const definition = new WorkflowDefinition();\r\n definition.workflow_code = dsl.name;\r\n // Map Semver (1.0.0) to version int (1)\r\n const majorVersion = Number(dsl.version.split('.')[0], 10);\r\n definition.version = isNaN(majorVersion) ? 1 : majorVersion;\r\n definition.description = dsl.description;\r\n definition.dsl = dsl;\r\n definition.compiled = dsl;\r\n definition.is_active = true;\r\n\r\n return definition;\r\n }\r\n\r\n /**\r\n * Get parsed DSL from WorkflowDefinition\r\n */\r\n async getParsedDsl(definitionId: string): Promise {\r\n const definition = await this.workflowDefRepo.findOne({\r\n where: { id: definitionId },\r\n });\r\n\r\n if (!definition) {\r\n throw new BadRequestException(\r\n `Workflow definition ${definitionId} not found`\r\n );\r\n }\r\n\r\n try {\r\n const dsl = definition.dsl;\r\n return WorkflowDslSchema.parse(dsl);\r\n } catch (error: unknown) {\r\n this.logger.error(\r\n `Failed to parse stored DSL for definition ${definitionId}`,\r\n error\r\n );\r\n throw new BadRequestException(\r\n `Invalid stored DSL: ${error instanceof Error ? error.message : String(error)}`\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Validate DSL without saving (dry-run)\r\n */\r\n validateOnly(dslJson: string): { valid: boolean; errors?: string[] } {\r\n try {\r\n const rawDsl = JSON.parse(dslJson);\r\n const dsl = WorkflowDslSchema.parse(rawDsl);\r\n this.validateStateMachine(dsl);\r\n return { valid: true };\r\n } catch (error: unknown) {\r\n return {\r\n valid: false,\r\n errors: [\r\n error instanceof Error ? error.message : 'Unknown validation error',\r\n ],\r\n };\r\n }\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dsl\\workflow-dsl.schema.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dto\\create-workflow-definition.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dto\\evaluate-workflow.dto.ts", "messages": [ { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 24, "column": 28, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 24, "endColumn": 31, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [777, 780], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [777, 780], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/modules/workflow-engine/dto/evaluate-workflow.dto.ts\r\nimport { IsString, IsNotEmpty, IsObject, IsOptional } from 'class-validator';\r\nimport { ApiProperty } from '@nestjs/swagger';\r\n\r\nexport class EvaluateWorkflowDto {\r\n @ApiProperty({ example: 'RFA', description: 'รหัส Workflow' })\r\n @IsString()\r\n @IsNotEmpty()\r\n workflow_code!: string; // เพิ่ม !\r\n\r\n @ApiProperty({ example: 'DRAFT', description: 'สถานะปัจจุบัน' })\r\n @IsString()\r\n @IsNotEmpty()\r\n current_state!: string; // เพิ่ม !\r\n\r\n @ApiProperty({ example: 'SUBMIT', description: 'Action ที่ต้องการทำ' })\r\n @IsString()\r\n @IsNotEmpty()\r\n action!: string; // เพิ่ม !\r\n\r\n @ApiProperty({ description: 'Context', example: { userId: 1 } })\r\n @IsObject()\r\n @IsOptional()\r\n context?: Record;\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dto\\get-available-actions.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dto\\update-workflow-definition.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dto\\workflow-transition.dto.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\entities\\workflow-definition.entity.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\entities\\workflow-history.entity.ts", "messages": [ { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 57, "column": 29, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 57, "endColumn": 32, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [1615, 1618], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [1615, 1618], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/modules/workflow-engine/entities/workflow-history.entity.ts\r\n\r\nimport {\r\n Column,\r\n CreateDateColumn,\r\n Entity,\r\n Index,\r\n JoinColumn,\r\n ManyToOne,\r\n PrimaryGeneratedColumn,\r\n} from 'typeorm';\r\nimport { WorkflowInstance } from './workflow-instance.entity';\r\n\r\n/**\r\n * เก็บประวัติการเปลี่ยนสถานะ (Audit Trail)\r\n * สำคัญมากสำหรับการตรวจสอบย้อนหลัง (Who did What, When)\r\n */\r\n@Entity('workflow_histories')\r\n@Index(['instanceId']) // ค้นหาประวัติของ Instance นี้\r\n@Index(['actionByUserId']) // ค้นหาว่า User คนนี้ทำอะไรไปบ้าง\r\nexport class WorkflowHistory {\r\n @PrimaryGeneratedColumn('uuid')\r\n id!: string;\r\n\r\n @ManyToOne(() => WorkflowInstance, { onDelete: 'CASCADE' })\r\n @JoinColumn({ name: 'instance_id' })\r\n instance!: WorkflowInstance;\r\n\r\n @Column({ name: 'instance_id' })\r\n instanceId!: string;\r\n\r\n @Column({ name: 'from_state', length: 50, comment: 'สถานะต้นทาง' })\r\n fromState!: string;\r\n\r\n @Column({ name: 'to_state', length: 50, comment: 'สถานะปลายทาง' })\r\n toState!: string;\r\n\r\n @Column({ length: 50, comment: 'Action ที่ User กด (เช่น APPROVE, REJECT)' })\r\n action!: string;\r\n\r\n @Column({\r\n name: 'action_by_user_id',\r\n nullable: true,\r\n comment: 'User ID ผู้ดำเนินการ (Nullable กรณี System Auto)',\r\n })\r\n actionByUserId?: number;\r\n\r\n @Column({ type: 'text', nullable: true, comment: 'ความเห็นประกอบการอนุมัติ' })\r\n comment?: string;\r\n\r\n // Snapshot ข้อมูล ณ เวลาที่เปลี่ยนสถานะ เพื่อเป็นหลักฐานหาก Context เปลี่ยนในอนาคต\r\n @Column({\r\n type: 'json',\r\n nullable: true,\r\n comment: 'Snapshot of Context or Metadata',\r\n })\r\n metadata?: Record;\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date;\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\entities\\workflow-instance.entity.ts", "messages": [ { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 73, "column": 28, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 73, "endColumn": 31, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [2108, 2111], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [2108, 2111], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/modules/workflow-engine/entities/workflow-instance.entity.ts\r\n\r\nimport {\r\n Column,\r\n CreateDateColumn,\r\n Entity,\r\n Index,\r\n JoinColumn,\r\n ManyToOne,\r\n PrimaryGeneratedColumn,\r\n UpdateDateColumn,\r\n} from 'typeorm';\r\nimport { WorkflowDefinition } from './workflow-definition.entity';\r\n\r\nexport enum WorkflowStatus {\r\n ACTIVE = 'ACTIVE', // กำลังดำเนินการ\r\n COMPLETED = 'COMPLETED', // จบกระบวนการ (ถึง Terminal State)\r\n CANCELLED = 'CANCELLED', // ถูกยกเลิกกลางคัน\r\n TERMINATED = 'TERMINATED', // ถูกบังคับจบโดยระบบ หรือ Error\r\n}\r\n\r\n/**\r\n * เก็บสถานะการเดินเรื่องของเอกสารแต่ละใบ (Runtime State)\r\n */\r\n@Entity('workflow_instances')\r\n@Index(['entityType', 'entityId']) // เพื่อค้นหาว่าเอกสารนี้ (เช่น RFA-001) อยู่ขั้นตอนไหน\r\n@Index(['currentState']) // เพื่อ Dashboard: \"มีงานค้างที่ขั้นตอนไหนบ้าง\"\r\nexport class WorkflowInstance {\r\n @PrimaryGeneratedColumn('uuid')\r\n id!: string;\r\n\r\n // ผูกกับ Definition เพื่อรู้ว่าใช้กฎชุดไหน (Version ไหน)\r\n @ManyToOne(() => WorkflowDefinition)\r\n @JoinColumn({ name: 'definition_id' })\r\n definition!: WorkflowDefinition;\r\n\r\n @Column({ name: 'definition_id' })\r\n definitionId!: string;\r\n\r\n // Polymorphic Relation: เชื่อมกับเอกสารได้หลายประเภท (RFA, CORR, etc.) โดยไม่ต้อง Foreign Key จริง\r\n @Column({\r\n name: 'entity_type',\r\n length: 50,\r\n comment: 'ประเภทเอกสาร เช่น rfa, correspondence',\r\n })\r\n entityType!: string;\r\n\r\n @Column({\r\n name: 'entity_id',\r\n length: 50,\r\n comment: 'ID ของเอกสาร (String/UUID)',\r\n })\r\n entityId!: string;\r\n\r\n @Column({\r\n name: 'current_state',\r\n length: 50,\r\n comment: 'ชื่อ State ปัจจุบัน เช่น DRAFT, IN_REVIEW',\r\n })\r\n currentState!: string;\r\n\r\n @Column({\r\n type: 'enum',\r\n enum: WorkflowStatus,\r\n default: WorkflowStatus.ACTIVE,\r\n comment: 'สถานะภาพรวมของ Instance',\r\n })\r\n status!: WorkflowStatus;\r\n\r\n // Context: เก็บตัวแปรที่จำเป็นสำหรับการตัดสินใจใน Workflow\r\n // เช่น { \"amount\": 500000, \"requester_role\": \"ENGINEER\", \"approver_ids\": [1, 2] }\r\n @Column({ type: 'json', nullable: true, comment: 'Runtime Context Data' })\r\n context?: Record;\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date;\r\n\r\n @UpdateDateColumn({ name: 'updated_at' })\r\n updatedAt!: Date;\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\interfaces\\workflow.interface.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-dsl.service.ts", "messages": [ { "ruleId": "@typescript-eslint/no-implied-eval", "severity": 2, "message": "Implied eval. Do not use the Function constructor to create functions.", "line": 258, "column": 20, "nodeType": "NewExpression", "messageId": "noFunctionConstructor", "endLine": 258, "endColumn": 68 }, { "ruleId": "@typescript-eslint/no-unsafe-call", "severity": 2, "message": "Unsafe call of a `Function` typed value.", "line": 259, "column": 16, "nodeType": "Identifier", "messageId": "unsafeCall", "endLine": 259, "endColumn": 20 } ], "suppressedMessages": [], "errorCount": 2, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/modules/workflow-engine/workflow-dsl.service.ts\r\n\r\nimport { BadRequestException, Injectable, Logger } from '@nestjs/common';\r\n\r\n// ==========================================\r\n// 1. Interfaces for RAW DSL (Input from User)\r\n// ==========================================\r\nexport interface RawWorkflowDSL {\r\n workflow: string;\r\n version?: number;\r\n description?: string;\r\n states: RawState[];\r\n}\r\n\r\nexport interface RawState {\r\n name: string;\r\n initial?: boolean;\r\n terminal?: boolean;\r\n on?: Record;\r\n}\r\n\r\nexport interface RawTransition {\r\n to: string;\r\n require?: {\r\n role?: string | string[];\r\n user?: string;\r\n };\r\n condition?: string; // JavaScript Expression string\r\n events?: RawEvent[];\r\n}\r\n\r\nexport interface RawEvent {\r\n type: 'notify' | 'webhook' | 'assign' | 'auto_action';\r\n target?: string;\r\n template?: string;\r\n payload?: Record;\r\n}\r\n\r\n// ==========================================\r\n// 2. Interfaces for COMPILED Schema (Optimized for Runtime)\r\n// ==========================================\r\nexport interface CompiledWorkflow {\r\n workflow: string;\r\n version: number;\r\n initialState: string; // Optimize: เก็บชื่อ Initial State ไว้เลย ไม่ต้อง loop หา\r\n states: Record;\r\n}\r\n\r\nexport interface CompiledState {\r\n terminal: boolean;\r\n transitions: Record;\r\n}\r\n\r\nexport interface CompiledTransition {\r\n to: string;\r\n requirements: {\r\n roles: string[];\r\n userId?: string;\r\n };\r\n condition?: string;\r\n events: RawEvent[];\r\n}\r\n\r\n@Injectable()\r\nexport class WorkflowDslService {\r\n private readonly logger = new Logger(WorkflowDslService.name);\r\n\r\n /**\r\n * [Compile Time]\r\n * แปลง Raw DSL เป็น Compiled Structure พร้อม Validation\r\n */\r\n compile(dsl: RawWorkflowDSL): CompiledWorkflow {\r\n this.validateSchemaStructure(dsl);\r\n\r\n const compiled: CompiledWorkflow = {\r\n workflow: dsl.workflow,\r\n version: dsl.version || 1,\r\n initialState: '',\r\n states: {},\r\n };\r\n\r\n const definedStates = new Set(dsl.states.map((s) => s.name));\r\n let initialFound = false;\r\n\r\n // 1. Process States\r\n for (const rawState of dsl.states) {\r\n if (rawState.initial) {\r\n if (initialFound) {\r\n throw new BadRequestException(\r\n `DSL Error: Multiple initial states found (at \"${rawState.name}\").`\r\n );\r\n }\r\n compiled.initialState = rawState.name;\r\n initialFound = true;\r\n }\r\n\r\n const compiledState: CompiledState = {\r\n terminal: !!rawState.terminal,\r\n transitions: {},\r\n };\r\n\r\n // 2. Process Transitions\r\n if (rawState.on) {\r\n for (const [action, rule] of Object.entries(rawState.on)) {\r\n // Validation: Target state must exist\r\n if (!definedStates.has(rule.to)) {\r\n throw new BadRequestException(\r\n `DSL Error: State \"${rawState.name}\" transitions via \"${action}\" to unknown state \"${rule.to}\".`\r\n );\r\n }\r\n\r\n compiledState.transitions[action] = {\r\n to: rule.to,\r\n requirements: {\r\n roles: rule.require?.role\r\n ? Array.isArray(rule.require.role)\r\n ? rule.require.role\r\n : [rule.require.role]\r\n : [],\r\n userId: rule.require?.user,\r\n },\r\n condition: rule.condition,\r\n events: rule.events || [],\r\n };\r\n }\r\n } else if (!rawState.terminal) {\r\n this.logger.warn(\r\n `State \"${rawState.name}\" is not terminal but has no transitions.`\r\n );\r\n }\r\n\r\n compiled.states[rawState.name] = compiledState;\r\n }\r\n\r\n if (!initialFound) {\r\n throw new BadRequestException('DSL Error: No initial state defined.');\r\n }\r\n\r\n return compiled;\r\n }\r\n\r\n /**\r\n * [Runtime]\r\n * ประมวลผล Action และคืนค่า State ถัดไป\r\n */\r\n evaluate(\r\n compiled: CompiledWorkflow,\r\n currentState: string,\r\n action: string,\r\n context: Record = {}\r\n ): { nextState: string; events: RawEvent[] } {\r\n const stateConfig = compiled.states[currentState];\r\n\r\n // 1. Validate State Existence\r\n if (!stateConfig) {\r\n throw new BadRequestException(\r\n `Runtime Error: Current state \"${currentState}\" is invalid.`\r\n );\r\n }\r\n\r\n // 2. Check if terminal\r\n if (stateConfig.terminal) {\r\n throw new BadRequestException(\r\n `Runtime Error: Cannot transition from terminal state \"${currentState}\".`\r\n );\r\n }\r\n\r\n // 3. Find Transition\r\n const transition = stateConfig.transitions[action];\r\n if (!transition) {\r\n const allowed = Object.keys(stateConfig.transitions).join(', ');\r\n throw new BadRequestException(\r\n `Invalid Action: \"${action}\" is not allowed from \"${currentState}\". Allowed: [${allowed}]`\r\n );\r\n }\r\n\r\n // 4. Validate Requirements (RBAC)\r\n this.checkRequirements(transition.requirements, context);\r\n\r\n // 5. Evaluate Condition (Dynamic Logic)\r\n if (transition.condition) {\r\n const isMet = this.evaluateCondition(transition.condition, context);\r\n if (!isMet) {\r\n throw new BadRequestException(\r\n 'Condition Failed: The criteria for this transition are not met.'\r\n );\r\n }\r\n }\r\n\r\n return {\r\n nextState: transition.to,\r\n events: transition.events,\r\n };\r\n }\r\n\r\n // --------------------------------------------------------\r\n // Private Helpers\r\n // --------------------------------------------------------\r\n\r\n private validateSchemaStructure(dsl: unknown) {\r\n if (!dsl || typeof dsl !== 'object') {\r\n throw new BadRequestException('DSL must be a JSON object.');\r\n }\r\n const d = dsl as Record;\r\n if (!d.workflow || !d.states || !Array.isArray(d.states)) {\r\n throw new BadRequestException(\r\n 'DSL Error: Missing required fields (workflow, states).'\r\n );\r\n }\r\n }\r\n\r\n private checkRequirements(\r\n req: CompiledTransition['requirements'],\r\n context: Record\r\n ) {\r\n // [FIX] Early return if no requirements defined\r\n if (!req) {\r\n return;\r\n }\r\n\r\n const userRoles: string[] = (context.roles as string[]) || [];\r\n const userId: string | number = context.userId as string | number;\r\n\r\n // Check Roles (OR logic inside array) - with null-safety\r\n const requiredRoles = req.roles || [];\r\n if (requiredRoles.length > 0) {\r\n const hasRole = requiredRoles.some((r) => userRoles.includes(r));\r\n if (!hasRole) {\r\n throw new BadRequestException(\r\n `Access Denied: Required roles [${requiredRoles.join(', ')}]`\r\n );\r\n }\r\n }\r\n\r\n // Check Specific User\r\n if (req.userId && String(req.userId) !== String(userId)) {\r\n throw new BadRequestException('Access Denied: User mismatch.');\r\n }\r\n }\r\n\r\n /**\r\n * Evaluate simple JS expression securely\r\n * NOTE: In production, use a safe parser like 'json-logic-js' or vm2\r\n * For this phase, we use a simple Function constructor with restricted scope.\r\n */\r\n private evaluateCondition(\r\n expression: string,\r\n context: Record\r\n ): boolean {\r\n try {\r\n // Simple guard against malicious code (basic)\r\n if (expression.includes('process') || expression.includes('require')) {\r\n throw new Error('Unsafe expression detected');\r\n }\r\n\r\n // Create a function that returns the expression result\r\n // \"context\" is available inside the expression\r\n const func = new Function('context', `return ${expression};`);\r\n return !!func(context);\r\n } catch (error: unknown) {\r\n this.logger.error(\r\n `Condition Error: \"${expression}\" -> ${error instanceof Error ? error.message : String(error)}`\r\n );\r\n return false; // Fail safe\r\n }\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-engine.controller.ts", "messages": [ { "ruleId": "@typescript-eslint/require-await", "severity": 2, "message": "Async method 'getAvailableActions' has no 'await' expression.", "line": 118, "column": 3, "nodeType": "FunctionExpression", "messageId": "missingAwait", "endLine": 118, "endColumn": 28, "suggestions": [ { "messageId": "removeAsync", "fix": { "range": [4233, 4239], "text": "" }, "desc": "Remove 'async'." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/modules/workflow-engine/workflow-engine.controller.ts\r\n\r\nimport {\r\n Body,\r\n Controller,\r\n Get,\r\n Param,\r\n Patch,\r\n Post,\r\n Request,\r\n UseGuards,\r\n} from '@nestjs/common';\r\nimport {\r\n ApiBearerAuth,\r\n ApiOperation,\r\n ApiParam,\r\n ApiResponse,\r\n ApiTags,\r\n} from '@nestjs/swagger';\r\n\r\n// Services\r\nimport { WorkflowEngineService } from './workflow-engine.service';\r\n\r\n// DTOs\r\nimport { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dto';\r\nimport { EvaluateWorkflowDto } from './dto/evaluate-workflow.dto';\r\nimport { UpdateWorkflowDefinitionDto } from './dto/update-workflow-definition.dto';\r\nimport { WorkflowTransitionDto } from './dto/workflow-transition.dto';\r\n\r\n// Guards & Decorators (อ้างอิงตามโครงสร้าง src/common ในแผนงาน)\r\nimport { RequirePermission } from '../../common/decorators/require-permission.decorator';\r\nimport { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';\r\nimport { RbacGuard } from '../../common/guards/rbac.guard';\r\nimport type { RequestWithUser } from '../../common/interfaces/request-with-user.interface';\r\n\r\n@ApiTags('Workflow Engine')\r\n@ApiBearerAuth() // ระบุว่าต้องใช้ Token ใน Swagger\r\n@Controller('workflow-engine')\r\n@UseGuards(JwtAuthGuard, RbacGuard) // บังคับ Login และตรวจสอบสิทธิ์ทุก Request\r\nexport class WorkflowEngineController {\r\n constructor(private readonly workflowService: WorkflowEngineService) {}\r\n\r\n // =================================================================\r\n // Definition Management (Admin / Developer)\r\n // =================================================================\r\n\r\n @Post('definitions')\r\n @ApiOperation({ summary: 'สร้าง Workflow Definition ใหม่ (Auto Versioning)' })\r\n @ApiResponse({ status: 201, description: 'Created successfully' })\r\n // ใช้ Permission 'system.manage_all' (Admin) หรือสร้าง permission ใหม่ 'workflow.manage' ในอนาคต\r\n @RequirePermission('system.manage_all')\r\n async createDefinition(@Body() dto: CreateWorkflowDefinitionDto) {\r\n return this.workflowService.createDefinition(dto);\r\n }\r\n\r\n @Get('definitions')\r\n @ApiOperation({ summary: 'ดึง Workflow Definition ทั้งหมด' })\r\n @RequirePermission('system.manage_all')\r\n async getDefinitions() {\r\n return this.workflowService.getDefinitions();\r\n }\r\n\r\n @Get('definitions/:id')\r\n @ApiOperation({ summary: 'ดึง Workflow Definition ด้วย ID' })\r\n @RequirePermission('system.manage_all')\r\n async getDefinitionById(@Param('id') id: string) {\r\n return this.workflowService.getDefinitionById(id);\r\n }\r\n\r\n @Patch('definitions/:id')\r\n @ApiOperation({ summary: 'แก้ไข Workflow Definition (Re-compile DSL)' })\r\n @RequirePermission('system.manage_all')\r\n async updateDefinition(\r\n @Param('id') id: string,\r\n @Body() dto: UpdateWorkflowDefinitionDto\r\n ) {\r\n return this.workflowService.update(id, dto);\r\n }\r\n\r\n @Post('evaluate')\r\n @ApiOperation({ summary: 'ทดสอบ Logic Workflow (Dry Run) ไม่บันทึกข้อมูล' })\r\n @RequirePermission('system.manage_all')\r\n async evaluate(@Body() dto: EvaluateWorkflowDto) {\r\n return this.workflowService.evaluate(dto);\r\n }\r\n\r\n // =================================================================\r\n // Runtime Engine (User Actions)\r\n // =================================================================\r\n\r\n @Post('instances/:id/transition')\r\n @ApiOperation({ summary: 'สั่งเปลี่ยนสถานะเอกสาร (User Action)' })\r\n @ApiParam({ name: 'id', description: 'Workflow Instance ID (UUID)' })\r\n // Permission จะถูกตรวจสอบ Dynamic ภายใน Service ตาม State ของ Workflow แต่ขั้นต้นต้องมีสิทธิ์ทำงาน Workflow\r\n @RequirePermission('workflow.action_review')\r\n async processTransition(\r\n @Param('id') instanceId: string,\r\n @Body() dto: WorkflowTransitionDto,\r\n @Request() req: RequestWithUser\r\n ) {\r\n // ดึง User ID จาก Token (req.user มาจาก JwtStrategy)\r\n const userId = req.user?.user_id;\r\n\r\n return this.workflowService.processTransition(\r\n instanceId,\r\n dto.action,\r\n userId,\r\n dto.comment,\r\n dto.payload\r\n );\r\n }\r\n\r\n @Get('instances/:id/actions')\r\n @ApiOperation({\r\n summary: 'ดึงรายการปุ่ม Action ที่สามารถกดได้ ณ สถานะปัจจุบัน',\r\n })\r\n @RequirePermission('document.view') // ผู้ที่มีสิทธิ์ดูเอกสาร ควรดู Action ได้\r\n async getAvailableActions(@Param('id') _instanceId: string) {\r\n // Note: Logic การดึง Action ตาม Instance ID จะถูก Implement ใน Task ถัดไป\r\n return { message: 'Pending implementation in Service layer' };\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-engine.module.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-engine.service.spec.ts", "messages": [ { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 121, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 121, "endColumn": 32 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 122, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 122, "endColumn": 28 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 145, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 145, "endColumn": 33 }, { "ruleId": "@typescript-eslint/unbound-method", "severity": 2, "message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.", "line": 188, "column": 14, "nodeType": "MemberExpression", "messageId": "unboundWithoutThisAnnotation", "endLine": 188, "endColumn": 41 } ], "suppressedMessages": [], "errorCount": 4, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { WorkflowEngineService } from './workflow-engine.service';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { DataSource, Repository } from 'typeorm';\r\nimport { WorkflowDefinition } from './entities/workflow-definition.entity';\r\nimport {\r\n WorkflowInstance,\r\n WorkflowStatus,\r\n} from './entities/workflow-instance.entity';\r\nimport { WorkflowHistory } from './entities/workflow-history.entity';\r\nimport { WorkflowDslService } from './workflow-dsl.service';\r\nimport { WorkflowEventService } from './workflow-event.service';\r\nimport { NotFoundException } from '@nestjs/common';\r\nimport { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dto';\r\n\r\ndescribe('WorkflowEngineService', () => {\r\n let service: WorkflowEngineService;\r\n let defRepo: Repository;\r\n let instanceRepo: Repository;\r\n let dslService: WorkflowDslService;\r\n let eventService: WorkflowEventService;\r\n\r\n // Mock Objects\r\n const mockQueryRunner = {\r\n connect: jest.fn(),\r\n startTransaction: jest.fn(),\r\n commitTransaction: jest.fn(),\r\n rollbackTransaction: jest.fn(),\r\n release: jest.fn(),\r\n manager: {\r\n findOne: jest.fn(),\r\n save: jest.fn(),\r\n },\r\n };\r\n\r\n const mockDataSource = {\r\n createQueryRunner: jest.fn().mockReturnValue(mockQueryRunner),\r\n };\r\n\r\n const mockDslService = {\r\n compile: jest.fn(),\r\n evaluate: jest.fn(),\r\n };\r\n\r\n const mockEventService = {\r\n dispatchEvents: jest.fn(),\r\n };\r\n\r\n const mockCompiledWorkflow = {\r\n initialState: 'START',\r\n states: {\r\n START: { transitions: { SUBMIT: 'PENDING' } },\r\n PENDING: { transitions: { APPROVE: 'APPROVED', REJECT: 'REJECTED' } },\r\n APPROVED: { terminal: true },\r\n REJECTED: { terminal: true },\r\n },\r\n };\r\n\r\n beforeEach(async () => {\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n WorkflowEngineService,\r\n {\r\n provide: getRepositoryToken(WorkflowDefinition),\r\n useValue: {\r\n findOne: jest.fn(),\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: getRepositoryToken(WorkflowInstance),\r\n useValue: {\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n findOne: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: getRepositoryToken(WorkflowHistory),\r\n useValue: {\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n },\r\n },\r\n { provide: WorkflowDslService, useValue: mockDslService },\r\n { provide: WorkflowEventService, useValue: mockEventService },\r\n { provide: DataSource, useValue: mockDataSource },\r\n ],\r\n }).compile();\r\n\r\n service = module.get(WorkflowEngineService);\r\n defRepo = module.get(getRepositoryToken(WorkflowDefinition));\r\n instanceRepo = module.get(getRepositoryToken(WorkflowInstance));\r\n dslService = module.get(WorkflowDslService);\r\n eventService = module.get(WorkflowEventService);\r\n\r\n jest.clearAllMocks();\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('createDefinition', () => {\r\n it('should create a new definition version', async () => {\r\n const dto = {\r\n workflow_code: 'WF01',\r\n dsl: {},\r\n } as CreateWorkflowDefinitionDto;\r\n mockDslService.compile.mockReturnValue(mockCompiledWorkflow);\r\n (defRepo.findOne as jest.Mock).mockResolvedValue({ version: 1 });\r\n (defRepo.create as jest.Mock).mockReturnValue({ version: 2 });\r\n (defRepo.save as jest.Mock).mockResolvedValue({\r\n version: 2,\r\n workflow_code: 'WF01',\r\n });\r\n\r\n const result = await service.createDefinition(dto);\r\n\r\n expect(dslService.compile).toHaveBeenCalledWith(dto.dsl);\r\n expect(defRepo.create).toHaveBeenCalledWith(\r\n expect.objectContaining({ version: 2 })\r\n );\r\n expect(result).toEqual(expect.objectContaining({ version: 2 }));\r\n });\r\n });\r\n\r\n describe('createInstance', () => {\r\n it('should create a new instance with initial state', async () => {\r\n const mockDef = {\r\n id: 'def-1',\r\n compiled: mockCompiledWorkflow,\r\n };\r\n\r\n (defRepo.findOne as jest.Mock).mockResolvedValue(mockDef);\r\n (instanceRepo.create as jest.Mock).mockReturnValue({\r\n id: 'inst-1',\r\n currentState: 'START',\r\n });\r\n (instanceRepo.save as jest.Mock).mockResolvedValue({ id: 'inst-1' });\r\n\r\n const result = await service.createInstance('WF01', 'DOC', '101');\r\n\r\n expect(instanceRepo.create).toHaveBeenCalledWith(\r\n expect.objectContaining({\r\n currentState: 'START',\r\n entityId: '101',\r\n })\r\n );\r\n expect(result).toBeDefined();\r\n });\r\n\r\n it('should throw NotFoundException if definition not found', async () => {\r\n (defRepo.findOne as jest.Mock).mockResolvedValue(null);\r\n await expect(\r\n service.createInstance('WF01', 'DOC', '101')\r\n ).rejects.toThrow(NotFoundException);\r\n });\r\n });\r\n\r\n describe('processTransition', () => {\r\n it('should process transition successfully and commit transaction', async () => {\r\n const instanceId = 'inst-1';\r\n const mockInstance = {\r\n id: instanceId,\r\n currentState: 'PENDING',\r\n status: WorkflowStatus.ACTIVE,\r\n definition: { compiled: mockCompiledWorkflow },\r\n context: { some: 'data' },\r\n };\r\n\r\n // Mock Pessimistic Lock Find\r\n mockQueryRunner.manager.findOne.mockResolvedValue(mockInstance);\r\n\r\n // Mock DSL Evaluation\r\n mockDslService.evaluate.mockReturnValue({\r\n nextState: 'APPROVED',\r\n events: [{ type: 'NOTIFY' }],\r\n });\r\n\r\n const result = await service.processTransition(instanceId, 'APPROVE', 1);\r\n\r\n expect(mockQueryRunner.startTransaction).toHaveBeenCalled();\r\n expect(mockDslService.evaluate).toHaveBeenCalled();\r\n expect(mockQueryRunner.manager.save).toHaveBeenCalledTimes(2); // Instance + History\r\n expect(mockQueryRunner.commitTransaction).toHaveBeenCalled();\r\n expect(eventService.dispatchEvents).toHaveBeenCalled(); // Should dispatch events\r\n expect(result.nextState).toBe('APPROVED');\r\n expect(result.isCompleted).toBe(true);\r\n });\r\n\r\n it('should rollback transaction on error', async () => {\r\n mockQueryRunner.manager.findOne.mockRejectedValue(new Error('DB Error'));\r\n\r\n await expect(\r\n service.processTransition('inst-1', 'APPROVE', 1)\r\n ).rejects.toThrow('DB Error');\r\n\r\n expect(mockQueryRunner.rollbackTransaction).toHaveBeenCalled();\r\n expect(mockQueryRunner.release).toHaveBeenCalled();\r\n });\r\n });\r\n});\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-engine.service.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-event.service.ts", "messages": [ { "ruleId": "@typescript-eslint/require-await", "severity": 2, "message": "Async method 'processSingleEvent' has no 'await' expression.", "line": 54, "column": 3, "nodeType": "FunctionExpression", "messageId": "missingAwait", "endLine": 54, "endColumn": 35, "suggestions": [ { "messageId": "removeAsync", "fix": { "range": [1738, 1744], "text": "" }, "desc": "Remove 'async'." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "// File: src/modules/workflow-engine/workflow-event.service.ts\r\n\r\nimport { Injectable, Logger } from '@nestjs/common';\r\nimport { RawEvent } from './workflow-dsl.service';\r\n\r\n// Interface สำหรับ External Services ที่จะมารับ Event ต่อ\r\n// (ในอนาคตควรใช้ NestJS Event Emitter เพื่อ Decouple อย่างสมบูรณ์)\r\nexport interface WorkflowEventHandler {\r\n handleNotification(\r\n target: string,\r\n template: string,\r\n payload: Record\r\n ): Promise;\r\n handleWebhook(url: string, payload: Record): Promise;\r\n handleAutoAction(instanceId: string, action: string): Promise;\r\n}\r\n\r\n@Injectable()\r\nexport class WorkflowEventService {\r\n private readonly logger = new Logger(WorkflowEventService.name);\r\n\r\n // สามารถ Inject NotificationService หรือ HttpService เข้ามาได้ตรงนี้\r\n // constructor(private readonly notificationService: NotificationService) {}\r\n\r\n /**\r\n * ประมวลผลรายการ Events ที่เกิดจากการเปลี่ยนสถานะ\r\n */\r\n dispatchEvents(\r\n instanceId: string,\r\n events: RawEvent[],\r\n context: Record\r\n ) {\r\n if (!events || events.length === 0) return;\r\n\r\n this.logger.log(\r\n `Dispatching ${events.length} events for Instance ${instanceId}`\r\n );\r\n\r\n // ทำแบบ Async ไม่รอผล (Fire-and-forget) เพื่อไม่ให้กระทบ Response Time ของ User\r\n void Promise.allSettled(\r\n events.map((event) => this.processSingleEvent(instanceId, event, context))\r\n ).then((results) => {\r\n // Log errors if any\r\n results.forEach((res, idx) => {\r\n if (res.status === 'rejected') {\r\n this.logger.error(\r\n `Failed to process event [${idx}]: ${String(res.reason)}`\r\n );\r\n }\r\n });\r\n });\r\n }\r\n\r\n private async processSingleEvent(\r\n instanceId: string,\r\n event: RawEvent,\r\n context: Record\r\n ) {\r\n try {\r\n switch (event.type) {\r\n case 'notify':\r\n this.handleNotify(event, context);\r\n break;\r\n case 'webhook':\r\n this.handleWebhook(event, context);\r\n break;\r\n case 'auto_action':\r\n // Logic สำหรับ Auto Transition (เช่น ถ้าผ่านเงื่อนไข ให้ไปต่อเลย)\r\n this.logger.log(`Auto Action triggered for ${instanceId}`);\r\n break;\r\n default:\r\n this.logger.warn(`Unknown event type: ${event.type}`);\r\n }\r\n } catch (error) {\r\n this.logger.error(\r\n `Error processing event ${event.type}: ${String(error)}`\r\n );\r\n throw error;\r\n }\r\n }\r\n\r\n // --- Handlers ---\r\n\r\n private handleNotify(event: RawEvent, _context: Record) {\r\n // Mockup: ในของจริงจะเรียก NotificationService.send()\r\n // const recipients = this.resolveRecipients(event.target, context);\r\n this.logger.log(\r\n `[EVENT] Notify target: \"${event.target}\" | Template: \"${event.template}\"`\r\n );\r\n }\r\n\r\n private handleWebhook(event: RawEvent, _context: Record) {\r\n // Mockup: เรียก HttpService.post()\r\n this.logger.log(\r\n `[EVENT] Webhook to: \"${event.target}\" | Payload: ${JSON.stringify(event.payload)}`\r\n );\r\n }\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\redlock.d.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\src\\scripts\\migrate-storage-v2.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `DataSourceOptions`.", "line": 10, "column": 37, "nodeType": "TSAsExpression", "messageId": "unsafeArgument", "endLine": 10, "endColumn": 50 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 10, "column": 47, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 10, "endColumn": 50, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [446, 449], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [446, 449], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 14, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 14, "endColumn": 16, "suggestions": [ { "fix": { "range": [502, 553], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 21, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 21, "endColumn": 16, "suggestions": [ { "fix": { "range": [766, 832], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 33, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 33, "endColumn": 16, "suggestions": [ { "fix": { "range": [1170, 1233], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 36, "column": 7, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 36, "endColumn": 19, "suggestions": [ { "fix": { "range": [1288, 1384], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "warn" }, "desc": "Remove the console.warn()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 50, "column": 9, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 50, "endColumn": 21, "suggestions": [ { "fix": { "range": [1652, 1723], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "warn" }, "desc": "Remove the console.warn()." } ] }, { "ruleId": "no-useless-escape", "severity": 2, "message": "Unnecessary escape character: \\/.", "line": 57, "column": 20, "nodeType": "Literal", "messageId": "unnecessaryEscape", "endLine": 57, "endColumn": 21, "suggestions": [ { "messageId": "removeEscape", "fix": { "range": [1913, 1914], "text": "" }, "desc": "Remove the `\\`. This maintains the current functionality." }, { "messageId": "escapeBackslash", "fix": { "range": [1913, 1913], "text": "\\" }, "desc": "Replace the `\\` with `\\\\` to include the actual backslash character." } ] }, { "ruleId": "no-useless-escape", "severity": 2, "message": "Unnecessary escape character: \\/.", "line": 57, "column": 78, "nodeType": "Literal", "messageId": "unnecessaryEscape", "endLine": 57, "endColumn": 79, "suggestions": [ { "messageId": "removeEscape", "fix": { "range": [1971, 1972], "text": "" }, "desc": "Remove the `\\`. This maintains the current functionality." }, { "messageId": "escapeBackslash", "fix": { "range": [1971, 1971], "text": "\\" }, "desc": "Replace the `\\` with `\\\\` to include the actual backslash character." } ] }, { "ruleId": "no-useless-escape", "severity": 2, "message": "Unnecessary escape character: \\/.", "line": 57, "column": 89, "nodeType": "Literal", "messageId": "unnecessaryEscape", "endLine": 57, "endColumn": 90, "suggestions": [ { "messageId": "removeEscape", "fix": { "range": [1982, 1983], "text": "" }, "desc": "Remove the `\\`. This maintains the current functionality." }, { "messageId": "escapeBackslash", "fix": { "range": [1982, 1982], "text": "\\" }, "desc": "Replace the `\\` with `\\\\` to include the actual backslash character." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 68, "column": 9, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 68, "endColumn": 21, "suggestions": [ { "fix": { "range": [2296, 2353], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "warn" }, "desc": "Remove the console.warn()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 98, "column": 37, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 98, "endColumn": 48 }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 100, "column": 9, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 100, "endColumn": 22, "suggestions": [ { "fix": { "range": [3366, 3474], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "error" }, "desc": "Remove the console.error()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 108, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 108, "endColumn": 16, "suggestions": [ { "fix": { "range": [3521, 3557], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 109, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 109, "endColumn": 16, "suggestions": [ { "fix": { "range": [3563, 3599], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 110, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 110, "endColumn": 16, "suggestions": [ { "fix": { "range": [3605, 3645], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 111, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 111, "endColumn": 16, "suggestions": [ { "fix": { "range": [3651, 3688], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 113, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 113, "endColumn": 18, "suggestions": [ { "fix": { "range": [3715, 3757], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "error" }, "desc": "Remove the console.error()." } ] }, { "ruleId": "@typescript-eslint/no-floating-promises", "severity": 2, "message": "Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.", "line": 119, "column": 1, "nodeType": "ExpressionStatement", "messageId": "floatingVoid", "endLine": 119, "endColumn": 18, "suggestions": [ { "messageId": "floatingFixVoid", "fix": { "range": [3817, 3817], "text": "void " }, "desc": "Add void operator to ignore." }, { "messageId": "floatingFixAwait", "fix": { "range": [3817, 3817], "text": "await " }, "desc": "Add await operator." } ] } ], "suppressedMessages": [], "errorCount": 19, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { DataSource } from 'typeorm';\r\nimport { databaseConfig } from '../config/database.config';\r\nimport { Attachment } from '../common/file-storage/entities/attachment.entity';\r\nimport * as fs from 'fs-extra';\r\nimport * as path from 'path';\r\n\r\nasync function migrateStorage() {\r\n // Config override for script execution if needed\r\n const config = { ...databaseConfig, entities: [Attachment] };\r\n const dataSource = new DataSource(config as any);\r\n await dataSource.initialize();\r\n\r\n try {\r\n console.log('🚀 Starting Storage Migration v2...');\r\n const attachmentRepo = dataSource.getRepository(Attachment);\r\n\r\n // Find all permanent attachments\r\n const attachments = await attachmentRepo.find({\r\n where: { isTemporary: false },\r\n });\r\n console.log(`Found ${attachments.length} permanent attachments.`);\r\n\r\n let movedCount = 0;\r\n let errorCount = 0;\r\n let skippedCount = 0;\r\n\r\n // Define base permanent directory\r\n // Note: Adjust path based on execution context (e.g., from backend root)\r\n const permanentBaseDir =\r\n process.env.UPLOAD_PERMANENT_DIR ||\r\n path.join(process.cwd(), 'uploads', 'permanent');\r\n\r\n console.log(`Target Permanent Directory: ${permanentBaseDir}`);\r\n\r\n if (!fs.existsSync(permanentBaseDir)) {\r\n console.warn(\r\n `Base directory not found: ${permanentBaseDir}. Creating it...`\r\n );\r\n await fs.ensureDir(permanentBaseDir);\r\n }\r\n\r\n for (const att of attachments) {\r\n if (!att.filePath) {\r\n skippedCount++;\r\n continue;\r\n }\r\n\r\n const currentPath = att.filePath;\r\n if (!fs.existsSync(currentPath)) {\r\n console.warn(`File not found on disk: ${currentPath} (ID: ${att.id})`);\r\n errorCount++;\r\n continue;\r\n }\r\n\r\n // Check if already in new structure (contains /General/YYYY/MM or similar)\r\n const newStructureRegex =\r\n /permanent[\\/\\\\](ContractDrawing|ShopDrawing|AsBuiltDrawing|General)[\\/\\\\]\\d{4}[\\/\\\\]\\d{2}/;\r\n if (newStructureRegex.test(currentPath)) {\r\n skippedCount++;\r\n continue;\r\n }\r\n\r\n // Determine target date\r\n const refDate = att.referenceDate\r\n ? new Date(att.referenceDate)\r\n : new Date(att.createdAt);\r\n if (isNaN(refDate.getTime())) {\r\n console.warn(`Invalid date for ID ${att.id}, skipping.`);\r\n errorCount++;\r\n continue;\r\n }\r\n\r\n const year = refDate.getFullYear().toString();\r\n const month = (refDate.getMonth() + 1).toString().padStart(2, '0');\r\n\r\n // Determine Doc Type (Default 'General' as we don't know easily without joins)\r\n const docType = 'General';\r\n\r\n const newDir = path.join(permanentBaseDir, docType, year, month);\r\n const newPath = path.join(newDir, att.storedFilename);\r\n\r\n if (path.resolve(currentPath) === path.resolve(newPath)) {\r\n skippedCount++;\r\n continue;\r\n }\r\n\r\n try {\r\n await fs.ensureDir(newDir);\r\n await fs.move(currentPath, newPath, { overwrite: true });\r\n\r\n // Update DB\r\n att.filePath = newPath;\r\n if (!att.referenceDate) {\r\n att.referenceDate = refDate;\r\n }\r\n await attachmentRepo.save(att);\r\n movedCount++;\r\n if (movedCount % 100 === 0) console.log(`Moved ${movedCount} files...`);\r\n } catch (err: unknown) {\r\n console.error(\r\n `Failed to move file ID ${att.id}:`,\r\n (err as Error).message\r\n );\r\n errorCount++;\r\n }\r\n }\r\n\r\n console.log(`Migration completed.`);\r\n console.log(`Moved: ${movedCount}`);\r\n console.log(`Skipped: ${skippedCount}`);\r\n console.log(`Errors: ${errorCount}`);\r\n } catch (error) {\r\n console.error('Migration failed:', error);\r\n } finally {\r\n await dataSource.destroy();\r\n }\r\n}\r\n\r\nmigrateStorage();\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\test\\app.e2e-spec.ts", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\test\\phase3-workflow.e2e-spec.ts", "messages": [ { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 57, "column": 7, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 57, "endColumn": 19, "suggestions": [ { "fix": { "range": [1934, 2037], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "warn" }, "desc": "Remove the console.warn()." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `App`.", "line": 70, "column": 36, "nodeType": "CallExpression", "messageId": "unsafeArgument", "endLine": 70, "endColumn": 55 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 83, "column": 5, "nodeType": "AssignmentExpression", "messageId": "anyAssignment", "endLine": 83, "endColumn": 40 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .id on an `any` value.", "line": 83, "column": 38, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 83, "endColumn": 40 }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 84, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 84, "endColumn": 16, "suggestions": [ { "fix": { "range": [2707, 2767], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `App`.", "line": 88, "column": 36, "nodeType": "CallExpression", "messageId": "unsafeArgument", "endLine": 88, "endColumn": 55 }, { "ruleId": "@typescript-eslint/no-unsafe-assignment", "severity": 2, "message": "Unsafe assignment of an `any` value.", "line": 98, "column": 5, "nodeType": "AssignmentExpression", "messageId": "anyAssignment", "endLine": 98, "endColumn": 50 }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .instanceId on an `any` value.", "line": 98, "column": 40, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 98, "endColumn": 50 }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 99, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 99, "endColumn": 16, "suggestions": [ { "fix": { "range": [3290, 3347], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 100, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 100, "endColumn": 16, "suggestions": [ { "fix": { "range": [3353, 3411], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-member-access", "severity": 2, "message": "Unsafe member access .currentState on an `any` value.", "line": 100, "column": 49, "nodeType": "Identifier", "messageId": "unsafeMemberExpression", "endLine": 100, "endColumn": 61 }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 106, "column": 7, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 106, "endColumn": 19, "suggestions": [ { "fix": { "range": [3592, 3657], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "warn" }, "desc": "Remove the console.warn()." } ] }, { "ruleId": "@typescript-eslint/no-unsafe-argument", "severity": 2, "message": "Unsafe argument of type `any` assigned to a parameter of type `App`.", "line": 110, "column": 36, "nodeType": "CallExpression", "messageId": "unsafeArgument", "endLine": 110, "endColumn": 55 }, { "ruleId": "no-console", "severity": 2, "message": "Unexpected console statement.", "line": 122, "column": 5, "nodeType": "MemberExpression", "messageId": "unexpected", "endLine": 122, "endColumn": 16, "suggestions": [ { "fix": { "range": [4216, 4261], "text": "" }, "messageId": "removeConsole", "data": { "propertyName": "log" }, "desc": "Remove the console.log()." } ] } ], "suppressedMessages": [], "errorCount": 14, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { INestApplication } from '@nestjs/common';\r\nimport request from 'supertest';\r\nimport { AppModule } from '../src/app.module';\r\nimport { JwtService } from '@nestjs/jwt';\r\nimport { DataSource } from 'typeorm';\r\nimport { WorkflowDefinition } from '../src/modules/workflow-engine/entities/workflow-definition.entity';\r\n\r\n/**\r\n * Phase 3 Workflow (E2E) - Unified Workflow Engine\r\n *\r\n * Tests the correspondence workflow using the Unified Workflow Engine\r\n * instead of the deprecated RoutingTemplate system.\r\n */\r\ndescribe('Phase 3 Workflow (E2E)', () => {\r\n let app: INestApplication;\r\n let jwtService: JwtService;\r\n let dataSource: DataSource;\r\n let correspondenceId: number;\r\n let workflowInstanceId: string;\r\n\r\n // Test Users (must exist in seed data)\r\n const editorUser = { user_id: 3, username: 'editor01', organization_id: 41 };\r\n const adminUser = { user_id: 2, username: 'admin', organization_id: 1 };\r\n\r\n let editorToken: string;\r\n let _adminToken: string;\r\n\r\n beforeAll(async () => {\r\n const moduleFixture: TestingModule = await Test.createTestingModule({\r\n imports: [AppModule],\r\n }).compile();\r\n\r\n app = moduleFixture.createNestApplication();\r\n await app.init();\r\n\r\n jwtService = moduleFixture.get(JwtService);\r\n dataSource = moduleFixture.get(DataSource);\r\n\r\n // Generate Tokens\r\n editorToken = jwtService.sign({\r\n username: editorUser.username,\r\n sub: editorUser.user_id,\r\n });\r\n adminToken = jwtService.sign({\r\n username: adminUser.username,\r\n sub: adminUser.user_id,\r\n });\r\n\r\n // Ensure workflow definition exists (should be seeded)\r\n const defRepo = dataSource.getRepository(WorkflowDefinition);\r\n const existing = await defRepo.findOne({\r\n where: { workflow_code: 'CORRESPONDENCE_FLOW_V1', is_active: true },\r\n });\r\n\r\n if (!existing) {\r\n console.warn(\r\n 'WorkflowDefinition CORRESPONDENCE_FLOW_V1 not found. Tests may fail.'\r\n );\r\n }\r\n });\r\n\r\n afterAll(async () => {\r\n if (app) {\r\n await app.close();\r\n }\r\n });\r\n\r\n it('/correspondences (POST) - Create Document', async () => {\r\n const response = await request(app.getHttpServer())\r\n .post('/correspondences')\r\n .set('Authorization', `Bearer ${editorToken}`)\r\n .send({\r\n projectId: 1,\r\n typeId: 1,\r\n title: 'E2E Workflow Test Document',\r\n details: { question: 'Testing Unified Workflow' },\r\n })\r\n .expect(201);\r\n\r\n expect(response.body).toHaveProperty('id');\r\n expect(response.body).toHaveProperty('correspondenceNumber');\r\n correspondenceId = response.body.id;\r\n console.log('Created Correspondence ID:', correspondenceId);\r\n });\r\n\r\n it('/correspondences/:id/submit (POST) - Submit to Workflow', async () => {\r\n const response = await request(app.getHttpServer())\r\n .post(`/correspondences/${correspondenceId}/submit`)\r\n .set('Authorization', `Bearer ${editorToken}`)\r\n .send({\r\n note: 'Submitting for E2E test',\r\n })\r\n .expect(201);\r\n\r\n expect(response.body).toHaveProperty('instanceId');\r\n expect(response.body).toHaveProperty('currentState');\r\n workflowInstanceId = response.body.instanceId;\r\n console.log('Workflow Instance ID:', workflowInstanceId);\r\n console.log('Current State:', response.body.currentState);\r\n });\r\n\r\n it('/correspondences/:id/workflow/action (POST) - Process Action', async () => {\r\n // Skip if submit failed to get instanceId\r\n if (!workflowInstanceId) {\r\n console.warn('Skipping action test - no instanceId from submit');\r\n return;\r\n }\r\n\r\n const response = await request(app.getHttpServer())\r\n .post(`/correspondences/${correspondenceId}/workflow/action`)\r\n .set('Authorization', `Bearer ${editorToken}`) // Use editor - has workflow.action_review permission\r\n .send({\r\n instanceId: workflowInstanceId,\r\n action: 'APPROVE',\r\n comment: 'E2E Approved via Unified Workflow Engine',\r\n })\r\n .expect(201);\r\n\r\n expect(response.body).toHaveProperty('success', true);\r\n expect(response.body).toHaveProperty('nextState');\r\n console.log('Action Result:', response.body);\r\n });\r\n});\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\backend\\test\\simple.e2e-spec.ts", "messages": [ { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'request' is defined but never used. Allowed unused vars must match /^_/u.", "line": 1, "column": 8, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 1, "endColumn": 15, "suggestions": [ { "messageId": "removeUnusedImportDeclaration", "data": { "varName": "request" }, "fix": { "range": [0, 34], "text": "" }, "desc": "Remove unused import declaration." } ] } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "import request from 'supertest';\r\nimport { AppModule } from '../src/app.module';\r\nimport { _RoutingTemplate } from '../src/modules/correspondence/entities/routing-template.entity';\r\n\r\nimport { Test, TestingModule } from '@nestjs/testing';\r\n\r\ndescribe('Simple Test', () => {\r\n it('should pass', async () => {\r\n const moduleFixture: TestingModule = await Test.createTestingModule({\r\n imports: [AppModule],\r\n }).compile();\r\n expect(moduleFixture).toBeDefined();\r\n });\r\n});\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\eslint.config.mjs", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\fix-console.cjs", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\fix-lint-automated.cjs", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\fix-parseInt.cjs", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\access-control\\organizations\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\access-control\\roles\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\access-control\\users\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\audit-logs\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\contracts\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\contract\\categories\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\contract\\sub-categories\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\contract\\volumes\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\shop\\main-categories\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\shop\\sub-categories\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\numbering\\[id]\\edit\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\numbering\\new\\page.tsx", "messages": [ { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'_error' is defined but never used.", "line": 28, "column": 14, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 28, "endColumn": 20 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "\"use client\";\r\n\r\nimport { TemplateEditor } from \"@/components/numbering/template-editor\";\r\nimport { numberingApi, NumberingTemplate } from \"@/lib/api/numbering\";\r\nimport { useRouter } from \"next/navigation\";\r\nimport { useCorrespondenceTypes, useContracts, useDisciplines } from \"@/hooks/use-master-data\";\r\nimport { useProjects } from \"@/hooks/use-projects\";\r\nimport { toast } from 'sonner';\r\n\r\nexport default function NewTemplatePage() {\r\n const router = useRouter();\r\n\r\n // Master Data\r\n const { data: correspondenceTypes = [] } = useCorrespondenceTypes();\r\n const { data: projects = [] } = useProjects();\r\n const projectId = 1; // Default or sync with selection\r\n const { data: contracts = [] } = useContracts(projectId);\r\n const contractId = contracts[0]?.id;\r\n const { data: disciplines = [] } = useDisciplines(contractId);\r\n\r\n const selectedProjectName =\r\n projects.find((p: { id: number; projectName: string }) => p.id === projectId)?.projectName || 'LCBP3';\r\n\r\n const handleSave = async (data: Partial) => {\r\n try {\r\n await numberingApi.saveTemplate(data);\r\n router.push(\"/admin/numbering\");\r\n } catch (_error) {\r\n toast.error('Failed to create template');\r\n }\r\n };\r\n\r\n const handleCancel = () => {\r\n router.push('/admin/doc-control/numbering');\r\n };\r\n\r\n return (\r\n
\r\n

New Numbering Template

\r\n \r\n
\r\n );\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\numbering\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\projects\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\correspondence-types\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\disciplines\\page.tsx", "messages": [ { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 17, "column": 28, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 17, "endColumn": 31, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [742, 745], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [742, 745], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 46, "column": 45, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 46, "endColumn": 48, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [1497, 1500], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [1497, 1500], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] }, { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'id' is defined but never used. Allowed unused args must match /^_/u.", "line": 70, "column": 20, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 70, "endColumn": 22 }, { "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, "message": "Unexpected any. Specify a different type.", "line": 84, "column": 36, "nodeType": "TSAnyKeyword", "messageId": "unexpectedAny", "endLine": 84, "endColumn": 39, "suggestions": [ { "messageId": "suggestUnknown", "fix": { "range": [3233, 3236], "text": "unknown" }, "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }, { "messageId": "suggestNever", "fix": { "range": [3233, 3236], "text": "never" }, "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." } ] } ], "suppressedMessages": [], "errorCount": 4, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "'use client';\n\nimport { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';\nimport { masterDataService } from '@/lib/services/master-data.service';\nimport { useContracts } from '@/hooks/use-master-data';\nimport { ColumnDef } from '@tanstack/react-table';\nimport { useState } from 'react';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\n\nexport default function DisciplinesPage() {\n const [selectedContractId, setSelectedContractId] = useState(null);\n\n const { data: contractsData = [] } = useContracts();\n // Ensure we consistently use an array\n const contracts = Array.isArray(contractsData) ? contractsData : [];\n\n const columns: ColumnDef[] = [\n {\n accessorKey: 'disciplineCode',\n header: 'Code',\n cell: ({ row }) => {row.getValue('disciplineCode')},\n },\n {\n accessorKey: 'codeNameTh',\n header: 'Name (TH)',\n },\n {\n accessorKey: 'codeNameEn',\n header: 'Name (EN)',\n },\n {\n accessorKey: 'isActive',\n header: 'Status',\n cell: ({ row }) => (\n \n {row.getValue('isActive') ? 'Active' : 'Inactive'}\n \n ),\n },\n ];\n\n const contractOptions = contracts.map((c: any) => ({\n label: `${c.contractName} (${c.contractCode})`,\n value: String(c.id),\n }));\n\n return (\n
\n {\n const items = await masterDataService.getDisciplines(selectedContractId ? selectedContractId : undefined);\n // ADR-019: Map contractId INT → contract UUID for edit mode select matching\n return (items as Record[]).map((item) => {\n const rec = item as { contract?: { id?: number; uuid?: string }; contractId?: number };\n return {\n ...item,\n contractId: rec.contract?.id || rec.contract?.uuid || String(rec.contractId),\n };\n });\n }}\n createFn={(data) => masterDataService.createDiscipline(data as unknown as Parameters[0])}\n updateFn={(id, _data) => Promise.reject('Not implemented yet')}\n deleteFn={(id) => masterDataService.deleteDiscipline(id)}\n columns={columns}\n filters={\n
\n setSelectedContractId(val === 'all' ? null : val)}\n >\n \n \n \n \n All Contracts\n {contracts.map((c: any) => (\n \n {c.contractName} ({c.contractCode})\n \n ))}\n \n \n
\n }\n fields={[\n {\n name: 'contractId',\n label: 'Contract',\n type: 'select',\n required: true,\n options: contractOptions,\n },\n {\n name: 'disciplineCode',\n label: 'Code',\n type: 'text',\n required: true,\n },\n {\n name: 'codeNameTh',\n label: 'Name (TH)',\n type: 'text',\n required: true,\n },\n { name: 'codeNameEn', label: 'Name (EN)', type: 'text' },\n { name: 'isActive', label: 'Active', type: 'checkbox' },\n ]}\n />\n
\n );\n}\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\drawing-categories\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\rfa-types\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\tags\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\workflows\\[id]\\edit\\page.tsx", "messages": [ { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'_error' is defined but never used.", "line": 72, "column": 14, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 72, "endColumn": 20 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "'use client';\r\n\r\nimport { useState, useEffect } from 'react';\r\nimport { useRouter, useParams } from 'next/navigation';\r\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\r\nimport { DSLEditor } from '@/components/workflows/dsl-editor';\r\nimport { VisualWorkflowBuilder } from '@/components/workflows/visual-builder';\r\nimport { Button } from '@/components/ui/button';\r\nimport { Input } from '@/components/ui/input';\r\nimport { Label } from '@/components/ui/label';\r\nimport { Textarea } from '@/components/ui/textarea';\r\nimport { Card } from '@/components/ui/card';\r\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\r\nimport { useWorkflowDefinition, useCreateWorkflowDefinition, useUpdateWorkflowDefinition } from '@/hooks/use-workflows';\r\nimport { Workflow } from '@/types/workflow';\r\nimport { CreateWorkflowDefinitionDto } from '@/types/dto/workflow-engine/workflow-engine.dto';\r\nimport { toast } from 'sonner';\r\nimport { Save, ArrowLeft, Loader2 } from 'lucide-react';\r\nimport Link from 'next/link';\r\n\r\nexport default function WorkflowEditPage() {\r\n const params = useParams();\r\n const router = useRouter();\r\n const id = params?.id === 'new' ? null : params?.id as string;\r\n\r\n const [workflowData, setWorkflowData] = useState>({\r\n workflowName: '',\r\n description: '',\r\n workflowType: 'CORRESPONDENCE',\r\n dslDefinition: 'name: New Workflow\\nversion: 1.0\\nsteps: []',\r\n isActive: true,\r\n });\r\n\r\n const { data: fetchedWorkflow, isLoading: loadingWorkflow } = useWorkflowDefinition(id as string);\r\n const createMutation = useCreateWorkflowDefinition();\r\n const updateMutation = useUpdateWorkflowDefinition();\r\n\r\n useEffect(() => {\r\n if (fetchedWorkflow) {\r\n setWorkflowData(fetchedWorkflow);\r\n }\r\n }, [fetchedWorkflow]);\r\n\r\n const loading = (!!id && loadingWorkflow) || createMutation.isPending || updateMutation.isPending;\r\n const saving = createMutation.isPending || updateMutation.isPending;\r\n\r\n const handleSave = async () => {\r\n if (!workflowData.workflowName) {\r\n toast.error('Workflow name is required');\r\n return;\r\n }\r\n\r\n try {\r\n const dto: CreateWorkflowDefinitionDto = {\r\n workflow_code: workflowData.workflowType || 'CORRESPONDENCE',\r\n dsl: {\r\n workflowName: workflowData.workflowName,\r\n description: workflowData.description,\r\n dslDefinition: workflowData.dslDefinition,\r\n },\r\n is_active: workflowData.isActive,\r\n };\r\n\r\n if (id) {\r\n await updateMutation.mutateAsync({ id, data: dto });\r\n toast.success('Workflow updated successfully');\r\n } else {\r\n await createMutation.mutateAsync(dto);\r\n toast.success('Workflow created successfully');\r\n router.push('/admin/doc-control/workflows');\r\n }\r\n } catch (_error) {\r\n toast.error('Failed to save workflow');\r\n }\r\n };\r\n\r\n if (loading) {\r\n return (\r\n
\r\n \r\n
\r\n );\r\n }\r\n\r\n return (\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n

{id ? 'Edit Workflow' : 'New Workflow'}

\r\n

\r\n {id ? `Version ${workflowData.version}` : 'Create a new workflow definition'}\r\n

\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n
\r\n
\r\n\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n \r\n setWorkflowData({\r\n ...workflowData,\r\n workflowName: e.target.value,\r\n })\r\n }\r\n placeholder=\"e.g. Standard RFA Workflow\"\r\n />\r\n
\r\n\r\n
\r\n \r\n \r\n setWorkflowData({\r\n ...workflowData,\r\n description: e.target.value,\r\n })\r\n }\r\n placeholder=\"Describe the purpose of this workflow\"\r\n />\r\n
\r\n\r\n
\r\n \r\n \r\n setWorkflowData({ ...workflowData, workflowType: value })\r\n }\r\n >\r\n \r\n \r\n \r\n \r\n Correspondence\r\n RFA\r\n Drawing\r\n \r\n \r\n
\r\n
\r\n
\r\n
\r\n\r\n
\r\n \r\n \r\n DSL Editor\r\n Visual Builder\r\n \r\n\r\n \r\n setWorkflowData({ ...workflowData, dslDefinition: value })}\r\n />\r\n \r\n\r\n \r\n setWorkflowData({ ...workflowData, dslDefinition: newDsl })}\r\n onSave={() => toast.info('Visual state saving not implemented in this demo')}\r\n />\r\n \r\n \r\n
\r\n
\r\n
\r\n );\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\workflows\\new\\page.tsx", "messages": [ { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'_error' is defined but never used.", "line": 34, "column": 14, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 34, "endColumn": 20 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "'use client';\r\n\r\nimport { useState } from 'react';\r\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\r\nimport { DSLEditor } from '@/components/workflows/dsl-editor';\r\nimport { VisualWorkflowBuilder } from '@/components/workflows/visual-builder';\r\nimport { Button } from '@/components/ui/button';\r\nimport { Input } from '@/components/ui/input';\r\nimport { Label } from '@/components/ui/label';\r\nimport { Textarea } from '@/components/ui/textarea';\r\nimport { Card } from '@/components/ui/card';\r\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\r\nimport { workflowApi } from '@/lib/api/workflows';\r\nimport { WorkflowType } from '@/types/workflow';\r\nimport { useRouter } from 'next/navigation';\r\nimport { Loader2 } from 'lucide-react';\r\nimport { toast } from 'sonner';\r\n\r\nexport default function NewWorkflowPage() {\r\n const router = useRouter();\r\n const [saving, setSaving] = useState(false);\r\n const [workflowData, setWorkflowData] = useState({\r\n workflowName: '',\r\n description: '',\r\n workflowType: 'CORRESPONDENCE' as WorkflowType,\r\n dslDefinition: 'name: New Workflow\\nversion: 1.0\\nsteps: []',\r\n });\r\n\r\n const handleSave = async () => {\r\n setSaving(true);\r\n try {\r\n await workflowApi.createWorkflow(workflowData);\r\n router.push('/admin/doc-control/workflows');\r\n } catch (_error) {\r\n toast.error('Failed to create workflow');\r\n } finally {\r\n setSaving(false);\r\n }\r\n };\r\n\r\n return (\r\n
\r\n
\r\n

New Workflow

\r\n
\r\n \r\n \r\n
\r\n
\r\n\r\n \r\n
\r\n
\r\n \r\n \r\n setWorkflowData({\r\n ...workflowData,\r\n workflowName: e.target.value,\r\n })\r\n }\r\n placeholder=\"e.g., Special RFA Approval\"\r\n />\r\n
\r\n\r\n
\r\n \r\n \r\n setWorkflowData({\r\n ...workflowData,\r\n description: e.target.value,\r\n })\r\n }\r\n placeholder=\"Describe the purpose of this workflow\"\r\n />\r\n
\r\n\r\n
\r\n \r\n setWorkflowData({ ...workflowData, workflowType: value as WorkflowType })}\r\n >\r\n \r\n \r\n \r\n \r\n Correspondence\r\n RFA\r\n Drawing\r\n \r\n \r\n
\r\n
\r\n
\r\n\r\n \r\n \r\n DSL Editor\r\n Visual Builder\r\n \r\n\r\n \r\n setWorkflowData({ ...workflowData, dslDefinition: value })}\r\n />\r\n \r\n\r\n \r\n \r\n \r\n \r\n
\r\n );\r\n}\r\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\workflows\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\migration\\errors\\page.tsx", "messages": [], "suppressedMessages": [], "errorCount": 0, "fatalErrorCount": 0, "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\migration\\page.tsx", "messages": [ { "ruleId": "react-hooks/exhaustive-deps", "severity": 1, "message": "React Hook useEffect has a missing dependency: 'fetchData'. Either include it or remove the dependency array.", "line": 35, "column": 6, "nodeType": "ArrayExpression", "endLine": 35, "endColumn": 20, "suggestions": [ { "desc": "Update the dependencies array to be: [fetchData, statusFilter]", "fix": { "range": [1363, 1377], "text": "[fetchData, statusFilter]" } } ] }, { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'error' is defined but never used.", "line": 104, "column": 14, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 104, "endColumn": 19 } ], "suppressedMessages": [], "errorCount": 1, "fatalErrorCount": 0, "warningCount": 1, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { migrationService } from \"@/lib/services/migration.service\";\nimport { MigrationReviewQueueItem, MigrationReviewStatus } from \"@/types/migration\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { toast } from \"sonner\";\nimport { Button } from \"@/components/ui/button\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { format } from \"date-fns\";\nimport { EyeIcon, FileXIcon, CheckSquareIcon } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { getApiErrorMessage } from \"@/types/api-error\";\n\nexport default function MigrationReviewQueuePage() {\n const [items, setItems] = useState([]);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [statusFilter, setStatusFilter] = useState(\"PENDING\");\n const [selectedIds, setSelectedIds] = useState([]);\n const [errorMessage, setErrorMessage] = useState(null);\n\n useEffect(() => {\n fetchData();\n }, [statusFilter]);\n\n const fetchData = async () => {\n try {\n setLoading(true);\n setErrorMessage(null);\n const res = await migrationService.getReviewQueue({\n status: statusFilter === \"ALL\" ? undefined : (statusFilter as MigrationReviewStatus),\n limit: 50,\n });\n setItems(Array.isArray(res.items) ? res.items : []);\n setSelectedIds([]); // reset selection on fetch\n } catch (error: unknown) {\n setItems([]);\n setErrorMessage(getApiErrorMessage(error, \"Failed to load queue\"));\n } finally {\n setLoading(false);\n }\n };\n\n const handleToggleSelectAll = () => {\n if (selectedIds.length === items.length) {\n setSelectedIds([]);\n } else {\n setSelectedIds(items.map((i) => i.id));\n }\n };\n\n const handleToggleSelect = (id: number) => {\n setSelectedIds((prev) =>\n prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]\n );\n };\n\n const handleBatchApprove = async () => {\n if (selectedIds.length === 0) return;\n try {\n setSubmitting(true);\n\n const batchItems = items\n .filter((i) => selectedIds.includes(i.id))\n .map((item) => ({\n queueId: item.id,\n dto: {\n document_number: item.documentNumber,\n subject: item.title || item.originalTitle || 'Untitled',\n category: item.aiSuggestedCategory || 'Correspondence',\n project_id: item.projectId || 1,\n migrated_by: 'SYSTEM_IMPORT',\n temp_attachment_id: item.tempAttachmentId,\n ai_confidence: item.aiConfidence,\n ai_issues: item.aiIssues,\n issued_date: item.issuedDate,\n received_date: item.receivedDate,\n sender_id: item.senderOrganizationId,\n receiver_id: item.receiverOrganizationId,\n details: {\n tags: item.extractedTags\n }\n }\n }));\n\n const batchId = `BATCH_UI_${Date.now()}`;\n await migrationService.commitBatch(\n { items: batchItems, batchId },\n batchId\n );\n\n fetchData();\n } catch (error) {\n toast.error(\"Batch commit failed.\");\n } finally {\n setSubmitting(false);\n }\n };\n\n return (\n
\n
\n
\n

Migration Review Queue

\n

\n Review and correct documents that AI flagged as low confidence.\n

\n
\n
\n {selectedIds.length > 0 && (\n \n \n {submitting ? \"Processing...\" : `Batch Approve (${selectedIds.length})`}\n \n )}\n \n \n \n \n
\n
\n\n \n \n Queue Items - {statusFilter}\n \n \n {errorMessage && (\n
\n {errorMessage}\n
\n )}\n {loading ? (\n
Loading queue...
\n ) : items.length === 0 ? (\n
No items in the queue.
\n ) : (\n
\n \n \n \n \n 0 && selectedIds.length === items.length}\n onCheckedChange={handleToggleSelectAll}\n aria-label=\"Select all\"\n />\n \n Document No.\n Suggested Category\n Confidence\n Status\n Created At\n Action\n \n \n \n {items.map((item) => (\n \n \n handleToggleSelect(item.id)}\n aria-label={`Select item ${item.id}`}\n />\n \n {item.documentNumber}\n {item.aiSuggestedCategory || \"Unknown\"}\n \n 0.8\n ? \"default\"\n : item.aiConfidence > 0.5\n ? \"secondary\"\n : \"destructive\"\n }\n >\n {item.aiConfidence ? (item.aiConfidence * 100).toFixed(1) + \"%\" : \"N/A\"}\n \n \n \n \n {item.status}\n \n \n {format(new Date(item.createdAt), \"dd MMM yyyy, HH:mm\")}\n \n \n \n \n \n \n ))}\n \n
\n
\n )}\n
\n
\n
\n );\n}\n", "usedDeprecatedRules": [] }, { "filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\migration\\review\\[id]\\page.tsx", "messages": [ { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'FormDescription' is defined but never used. Allowed unused vars must match /^_/u.", "line": 14, "column": 3, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 14, "endColumn": 18, "suggestions": [ { "messageId": "removeUnusedVar", "data": { "varName": "FormDescription" }, "fix": { "range": [450, 469], "text": "" }, "desc": "Remove unused variable \"FormDescription\"." } ] }, { "ruleId": "react-hooks/exhaustive-deps", "severity": 1, "message": "React Hook useEffect has a missing dependency: 'fetchItem'. Either include it or remove the dependency array.", "line": 67, "column": 6, "nodeType": "ArrayExpression", "endLine": 67, "endColumn": 10, "suggestions": [ { "desc": "Update the dependencies array to be: [fetchItem, id]", "fix": { "range": [2120, 2124], "text": "[fetchItem, id]" } } ] }, { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'error' is defined but never used.", "line": 89, "column": 14, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 89, "endColumn": 19 }, { "ruleId": "@typescript-eslint/no-unused-vars", "severity": 2, "message": "'error' is defined but never used.", "line": 144, "column": 14, "nodeType": "Identifier", "messageId": "unusedVar", "endLine": 144, "endColumn": 19 } ], "suppressedMessages": [], "errorCount": 3, "fatalErrorCount": 0, "warningCount": 1, "fixableErrorCount": 0, "fixableWarningCount": 0, "source": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useParams, useRouter } from \"next/navigation\";\nimport { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport * as z from \"zod\";\nimport { migrationService } from \"@/lib/services/migration.service\";\nimport { MigrationReviewQueueItem } from \"@/types/migration\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Form,\n FormControl,\n FormDescription,\n FormField,\n FormItem,\n FormLabel,\n FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { ArrowLeftIcon, CheckCircleIcon, XCircleIcon } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { toast } from \"sonner\";\nimport { Card, CardContent } from \"@/components/ui/card\";\n\nconst reviewFormSchema = z.object({\n document_number: z.string().min(1, \"Document number is required\"),\n subject: z.string().min(1, \"Subject is required\"),\n category: z.string().min(1, \"Category is required\"),\n document_date: z.string().optional(),\n issued_date: z.string().optional(),\n received_date: z.string().optional(),\n sender_id: z.string().optional(),\n discipline_id: z.string().optional(),\n});\n\ntype ReviewFormValues = z.infer;\n\nexport default function MigrationReviewPage() {\n const params = useParams();\n const router = useRouter();\n const id = Number(params.id);\n\n const [item, setItem] = useState(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n\n const form = useForm({\n resolver: zodResolver(reviewFormSchema),\n defaultValues: {\n document_number: \"\",\n subject: \"\",\n category: \"\",\n document_date: \"\",\n issued_date: \"\",\n received_date: \"\",\n sender_id: \"\",\n discipline_id: \"\",\n },\n });\n\n useEffect(() => {\n if (!id) return;\n fetchItem(id);\n }, [id]);\n\n const fetchItem = async (itemId: number) => {\n try {\n setLoading(true);\n const res = await migrationService.getQueueItem(itemId);\n setItem(res);\n\n if (res) {\n // Pre-fill form from database item and aiIssues payload\n const issues = res.aiIssues || {};\n form.reset({\n document_number: res.documentNumber || \"\",\n subject: res.title || res.originalTitle || \"\",\n category: res.aiSuggestedCategory || \"\",\n document_date: issues.document_date || \"\",\n issued_date: issues.issued_date || \"\",\n received_date: issues.received_date || \"\",\n sender_id: issues.sender_id ? String(issues.sender_id) : \"\",\n discipline_id: issues.discipline_id ? String(issues.discipline_id) : \"\",\n });\n }\n } catch (error) {\n toast.error(\"Failed to load queue item\");\n } finally {\n setLoading(false);\n }\n };\n\n const onSubmit = async (values: ReviewFormValues) => {\n if (!item) return;\n\n try {\n setSubmitting(true);\n const issues = item.aiIssues || {};\n\n const payload = {\n document_number: values.document_number,\n subject: values.subject,\n category: values.category,\n source_file_path: issues.source_file_path || \"\",\n migrated_by: \"SYSTEM_IMPORT\",\n batch_id: \"MANUAL_REVIEW_BATCH\",\n project_id: 1, // Assumption or pulled from store\n document_date: values.document_date,\n issued_date: values.issued_date,\n received_date: values.received_date,\n sender_id: values.sender_id ? Number(values.sender_id) : undefined,\n discipline_id: values.discipline_id ? Number(values.discipline_id) : undefined,\n details: {\n tags: issues.tags || [],\n ai_confidence: item.aiConfidence,\n }\n };\n\n // Mock idempotency key based on timestamp to ensure uniqueness per approval retry\n const idempotencyKey = `review-${item.id}-${Date.now()}`;\n await migrationService.approveQueueItem(item.id, payload, idempotencyKey);\n\n toast.success(\"Document approved and imported successfully\");\n router.push(\"/admin/migration\");\n } catch (error: unknown) {\n const err = error as { response?: { data?: { message?: string } } };\n toast.error(err?.response?.data?.message || \"Failed to approve and import\");\n } finally {\n setSubmitting(false);\n }\n };\n\n const onReject = async () => {\n if (!item || !confirm(\"Are you sure you want to REJECT this document? It will not be imported.\")) return;\n\n try {\n setSubmitting(true);\n await migrationService.rejectQueueItem(item.id);\n toast.success(\"Document rejected\");\n router.push(\"/admin/migration\");\n } catch (error: unknown) {\n toast.error(\"Failed to reject document\");\n } finally {\n setSubmitting(false);\n }\n };\n\n if (loading) {\n return
Loading document data...
;\n }\n\n if (!item) {\n return
Document not found
;\n }\n\n const pdfUrl = item.aiIssues?.source_file_path\n ? migrationService.getStagingFileUrl(item.aiIssues.source_file_path)\n : null;\n\n return (\n
\n
\n
\n \n \n \n
\n

Review Document: {item.documentNumber}

\n

\n Status: {item.status}\n {' | '} Confidence: {item.aiConfidence ? (item.aiConfidence * 100).toFixed(1) + \"%\" : \"N/A\"}\n

\n
\n
\n
\n\n
\n {/* Left Side: PDF Viewer */}\n \n\n {/* Right Side: Form */}\n \n
\n

\n Extracted Information\n

\n {item.reviewReason && (\n

\n Reason: {item.reviewReason}\n

\n )}\n
\n \n
\n \n (\n \n Document Number\n \n \n \n \n \n )}\n />\n (\n \n Subject\n \n