690406:2310 Done Task BE-ERR-01
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
PermissionException,
|
||||
ValidationException,
|
||||
} from '../../common/exceptions';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
|
||||
@@ -53,15 +53,18 @@ export class CirculationService {
|
||||
user.user_id
|
||||
);
|
||||
if (!canManageAll) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to create documents on behalf of other organizations.'
|
||||
throw new PermissionException(
|
||||
'circulation',
|
||||
'create on behalf of other organization'
|
||||
);
|
||||
}
|
||||
userOrgId = resolvedOriginatorId;
|
||||
}
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException('User must belong to an organization');
|
||||
throw new ValidationException(
|
||||
'User must belong to an organization to create a circulation'
|
||||
);
|
||||
}
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
@@ -195,11 +198,12 @@ export class CirculationService {
|
||||
relations: ['circulation'],
|
||||
});
|
||||
|
||||
if (!routing) throw new NotFoundException('Routing task not found');
|
||||
if (!routing)
|
||||
throw new NotFoundException('Routing task', String(routingId));
|
||||
|
||||
// Check Permission: คนทำต้องเป็นเจ้าของ Task
|
||||
if (routing.assignedTo !== user.user_id) {
|
||||
throw new ForbiddenException('You are not assigned to this task');
|
||||
throw new PermissionException('circulation routing task', 'process');
|
||||
}
|
||||
|
||||
// Update Routing
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { ForbiddenException } from '@nestjs/common';
|
||||
import { PermissionException } from '../../common/exceptions';
|
||||
import { CorrespondenceService } from './correspondence.service';
|
||||
import { Correspondence } from './entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||
@@ -260,7 +260,7 @@ describe('CorrespondenceService', () => {
|
||||
|
||||
await expect(
|
||||
service.update(2, { subject: 'Should Fail' }, mockUser)
|
||||
).rejects.toThrow(ForbiddenException);
|
||||
).rejects.toThrow(PermissionException);
|
||||
});
|
||||
|
||||
it('should NOT regenerate number if critical fields unchanged', async () => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// File: src/modules/correspondence/correspondence.service.ts
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
BusinessException,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
ForbiddenException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
PermissionException,
|
||||
SystemException,
|
||||
ValidationException,
|
||||
} from '../../common/exceptions';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
|
||||
@@ -125,7 +125,7 @@ export class CorrespondenceService {
|
||||
}
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'User must belong to an organization to create documents'
|
||||
);
|
||||
}
|
||||
@@ -139,14 +139,17 @@ export class CorrespondenceService {
|
||||
// Check if it's internal communication
|
||||
if (createDto.isInternal) {
|
||||
// Internal communications should use Circulation instead
|
||||
throw new BadRequestException(
|
||||
'Internal communications should use Circulation Sheet instead of Correspondence'
|
||||
throw new BusinessException(
|
||||
'INVALID_DOCUMENT_TYPE',
|
||||
'Internal communications should use Circulation Sheet instead of Correspondence',
|
||||
'การสื่อสารภายในควรใช้ Circulation Sheet แทน Correspondence',
|
||||
['ใช้ Circulation Sheet สำหรับการสื่อสารภายในองค์กร']
|
||||
);
|
||||
}
|
||||
|
||||
// Validate recipients
|
||||
if (!createDto.recipients || createDto.recipients.length === 0) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'At least one recipient (TO or CC) is required'
|
||||
);
|
||||
}
|
||||
@@ -155,7 +158,7 @@ export class CorrespondenceService {
|
||||
const ccRecipients = createDto.recipients.filter((r) => r.type === 'CC');
|
||||
|
||||
if (toRecipients.length === 0 && ccRecipients.length === 0) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'At least one TO or CC recipient is required'
|
||||
);
|
||||
}
|
||||
@@ -167,8 +170,11 @@ export class CorrespondenceService {
|
||||
);
|
||||
|
||||
if (recipientOrgId === originatorOrgId) {
|
||||
throw new BadRequestException(
|
||||
'Cannot send correspondence to your own organization. Use Circulation Sheet for internal communication.'
|
||||
throw new BusinessException(
|
||||
'CORRESPONDENCE_TO_SELF',
|
||||
'Cannot send correspondence to your own organization',
|
||||
'ไม่สามารถส่งเอกสารถึงองค์กรของตัวเองได้ ใช้ Circulation Sheet แทน',
|
||||
['ใช้ Circulation Sheet สำหรับการสื่อสารภายใน']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -199,15 +205,14 @@ export class CorrespondenceService {
|
||||
const type = await this.typeRepo.findOne({
|
||||
where: { id: createDto.typeId },
|
||||
});
|
||||
if (!type) throw new NotFoundException('Document Type not found');
|
||||
if (!type)
|
||||
throw new NotFoundException('Document Type', String(createDto.typeId));
|
||||
|
||||
const statusDraft = await this.statusRepo.findOne({
|
||||
where: { statusCode: 'DRAFT' },
|
||||
});
|
||||
if (!statusDraft) {
|
||||
throw new InternalServerErrorException(
|
||||
'Status DRAFT not found in Master Data'
|
||||
);
|
||||
throw new SystemException('Status DRAFT not found in Master Data');
|
||||
}
|
||||
|
||||
let userOrgId = user.primaryOrganizationId;
|
||||
@@ -225,15 +230,16 @@ export class CorrespondenceService {
|
||||
user.user_id
|
||||
);
|
||||
if (!canManageAll) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to create documents on behalf of other organizations.'
|
||||
throw new PermissionException(
|
||||
'correspondence',
|
||||
'create on behalf of other organization'
|
||||
);
|
||||
}
|
||||
userOrgId = resolvedOriginatorId;
|
||||
}
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'User must belong to an organization to create documents'
|
||||
);
|
||||
}
|
||||
@@ -505,7 +511,7 @@ export class CorrespondenceService {
|
||||
});
|
||||
|
||||
if (!correspondence) {
|
||||
throw new NotFoundException(`Correspondence with ID ${id} not found`);
|
||||
throw new NotFoundException('Correspondence', String(id));
|
||||
}
|
||||
return correspondence;
|
||||
}
|
||||
@@ -533,9 +539,7 @@ export class CorrespondenceService {
|
||||
.getOne();
|
||||
|
||||
if (!correspondence) {
|
||||
throw new NotFoundException(
|
||||
`Correspondence with UUID ${publicId} not found`
|
||||
);
|
||||
throw new NotFoundException('Correspondence', publicId);
|
||||
}
|
||||
return correspondence;
|
||||
}
|
||||
@@ -548,11 +552,15 @@ export class CorrespondenceService {
|
||||
});
|
||||
|
||||
if (!source || !target) {
|
||||
throw new NotFoundException('Source or Target correspondence not found');
|
||||
throw new NotFoundException('Source or Target correspondence');
|
||||
}
|
||||
|
||||
if (source.id === target.id) {
|
||||
throw new BadRequestException('Cannot reference self');
|
||||
throw new BusinessException(
|
||||
'SELF_REFERENCE',
|
||||
'Cannot reference self',
|
||||
'ไม่สามารถอ้างอิงเอกสารเดียวกันได้'
|
||||
);
|
||||
}
|
||||
|
||||
const exists = await this.referenceRepo.findOne({
|
||||
@@ -581,7 +589,7 @@ export class CorrespondenceService {
|
||||
});
|
||||
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException('Reference not found');
|
||||
throw new NotFoundException('Reference');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,14 +606,14 @@ export class CorrespondenceService {
|
||||
where: { id },
|
||||
});
|
||||
if (!correspondence) {
|
||||
throw new NotFoundException(`Correspondence ${id} not found`);
|
||||
throw new NotFoundException('Correspondence', String(id));
|
||||
}
|
||||
|
||||
const tag = await this.dataSource.manager.findOne(Tag, {
|
||||
where: { id: tagId },
|
||||
});
|
||||
if (!tag) {
|
||||
throw new NotFoundException(`Tag ${tagId} not found`);
|
||||
throw new NotFoundException('Tag', String(tagId));
|
||||
}
|
||||
|
||||
const exists = await this.tagRepo.findOne({
|
||||
@@ -620,7 +628,7 @@ export class CorrespondenceService {
|
||||
async removeTag(id: number, tagId: number) {
|
||||
const result = await this.tagRepo.delete({ correspondenceId: id, tagId });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException('Tag assignment not found');
|
||||
throw new NotFoundException('Tag assignment');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,9 +657,7 @@ export class CorrespondenceService {
|
||||
});
|
||||
|
||||
if (!revision) {
|
||||
throw new NotFoundException(
|
||||
`Current revision for correspondence ${id} not found`
|
||||
);
|
||||
throw new NotFoundException('Current revision', `correspondence:${id}`);
|
||||
}
|
||||
|
||||
// 2. Check Permission
|
||||
@@ -669,9 +675,7 @@ export class CorrespondenceService {
|
||||
permissions.includes('system.manage_all');
|
||||
|
||||
if (!canEditSubmittedOrLater) {
|
||||
throw new ForbiddenException(
|
||||
'Only Org Admin or Superadmin can edit non-draft correspondences'
|
||||
);
|
||||
throw new PermissionException('correspondence', 'edit non-draft');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -699,7 +703,7 @@ export class CorrespondenceService {
|
||||
// 3. Check if number regeneration is needed (only for DRAFT status)
|
||||
const oldCorr = revision.correspondence;
|
||||
if (!oldCorr) {
|
||||
throw new InternalServerErrorException(
|
||||
throw new SystemException(
|
||||
'Correspondence relation not loaded for revision'
|
||||
);
|
||||
}
|
||||
@@ -734,7 +738,7 @@ export class CorrespondenceService {
|
||||
const type = await this.typeRepo.findOne({ where: { id: typeId } });
|
||||
|
||||
if (!type) {
|
||||
throw new NotFoundException('Document Type not found');
|
||||
throw new NotFoundException('Document Type', String(typeId));
|
||||
}
|
||||
|
||||
// Get recipient org code for number generation
|
||||
@@ -898,7 +902,8 @@ export class CorrespondenceService {
|
||||
const type = await this.typeRepo.findOne({
|
||||
where: { id: createDto.typeId },
|
||||
});
|
||||
if (!type) throw new NotFoundException('Document Type not found');
|
||||
if (!type)
|
||||
throw new NotFoundException('Document Type', String(createDto.typeId));
|
||||
|
||||
let userOrgId = user.primaryOrganizationId;
|
||||
if (!userOrgId) {
|
||||
@@ -953,9 +958,7 @@ export class CorrespondenceService {
|
||||
permissions.includes('system.manage_all');
|
||||
|
||||
if (!canCancel) {
|
||||
throw new ForbiddenException(
|
||||
'Only administrators can cancel correspondences'
|
||||
);
|
||||
throw new PermissionException('correspondence', 'cancel');
|
||||
}
|
||||
|
||||
// Check if there are any active circulations
|
||||
@@ -981,7 +984,7 @@ export class CorrespondenceService {
|
||||
});
|
||||
|
||||
if (!currentRevision) {
|
||||
throw new NotFoundException('Current revision not found');
|
||||
throw new NotFoundException('Current revision');
|
||||
}
|
||||
|
||||
// Get cancelled status
|
||||
@@ -990,7 +993,7 @@ export class CorrespondenceService {
|
||||
});
|
||||
|
||||
if (!cancelledStatus) {
|
||||
throw new InternalServerErrorException('CANCELLED status not found');
|
||||
throw new SystemException('CANCELLED status not found in Master Data');
|
||||
}
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
@@ -24,7 +24,10 @@ import {
|
||||
} from './dto';
|
||||
import { Project } from '../project/entities/project.entity';
|
||||
import { UserAssignment } from '../user/entities/user-assignment.entity';
|
||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
NotFoundException,
|
||||
PermissionException,
|
||||
} from '../../common/exceptions';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
@@ -58,7 +61,7 @@ export class DashboardService {
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
||||
throw new NotFoundException('Project', String(projectId));
|
||||
}
|
||||
|
||||
// 2. ตรวจสอบสิทธิ (UserAssignment)
|
||||
@@ -82,9 +85,7 @@ export class DashboardService {
|
||||
this.logger.warn(
|
||||
`User ${userId} attempted to access project ${projectId} without assignment`
|
||||
);
|
||||
throw new ForbiddenException(
|
||||
`You do not have access to project ${projectId}`
|
||||
);
|
||||
throw new PermissionException('project', 'view');
|
||||
}
|
||||
|
||||
return project.id;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { BusinessException } from '../../../common/exceptions';
|
||||
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
|
||||
import { Repository, EntityManager, IsNull, Equal } from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
@@ -318,8 +319,11 @@ export class DocumentNumberingService {
|
||||
async setCounterValue(id: number, sequence: number) {
|
||||
await Promise.resolve(id); // satisfy unused
|
||||
await Promise.resolve(sequence);
|
||||
throw new BadRequestException(
|
||||
'Updating counter by single ID is not supported with composite keys. Use manualOverride.'
|
||||
throw new BusinessException(
|
||||
'COUNTER_UPDATE_NOT_SUPPORTED',
|
||||
'Updating counter by single ID is not supported with composite keys',
|
||||
'ไม่รองรับการอัปเดต Counter แบบ Single ID กรุณาใช้ manualOverride',
|
||||
['ใช้ manualOverride แทน']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// File: src/modules/json-schema/json-schema.service.ts
|
||||
// บันทึกการแก้ไข: Fix TS2345 (undefined check)
|
||||
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
BusinessException,
|
||||
NotFoundException,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
ValidationException,
|
||||
} from '../../common/exceptions';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import Ajv, { ValidateFunction } from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
@@ -101,7 +100,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
try {
|
||||
this.ajv.compile(createDto.schemaDefinition);
|
||||
} catch (error: unknown) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
`Invalid JSON Schema format: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
@@ -207,7 +206,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
async findOne(id: number): Promise<JsonSchema> {
|
||||
const schema = await this.jsonSchemaRepository.findOne({ where: { id } });
|
||||
if (!schema) {
|
||||
throw new NotFoundException(`JsonSchema with ID ${id} not found`);
|
||||
throw new NotFoundException('JsonSchema', String(id));
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
@@ -224,9 +223,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
});
|
||||
|
||||
if (!schema) {
|
||||
throw new NotFoundException(
|
||||
`JsonSchema '${code}' version ${version} not found`
|
||||
);
|
||||
throw new NotFoundException('JsonSchema', `${code}@v${version}`);
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
@@ -241,9 +238,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
});
|
||||
|
||||
if (!schema) {
|
||||
throw new NotFoundException(
|
||||
`Active JsonSchema with code '${code}' not found`
|
||||
);
|
||||
throw new NotFoundException('Active JsonSchema', code);
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
@@ -333,8 +328,10 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
validate = this.ajv.compile(schema.schemaDefinition);
|
||||
this.validators.set(schemaCode, validate);
|
||||
} catch (error: unknown) {
|
||||
throw new BadRequestException(
|
||||
`Invalid Schema Definition for '${schemaCode}': ${error instanceof Error ? error.message : String(error)}`
|
||||
throw new BusinessException(
|
||||
'INVALID_SCHEMA_DEFINITION',
|
||||
`Invalid Schema Definition for '${schemaCode}': ${error instanceof Error ? error.message : String(error)}`,
|
||||
'Schema Definition ไม่ถูกต้อง'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -353,7 +350,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
const errorMsg = result.errors
|
||||
.map((e) => `${e.field}: ${e.message}`)
|
||||
.join(', ');
|
||||
throw new BadRequestException(`JSON Validation Failed: ${errorMsg}`);
|
||||
throw new ValidationException(`JSON Validation Failed: ${errorMsg}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -372,7 +369,7 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
try {
|
||||
this.ajv.compile(updateDto.schemaDefinition);
|
||||
} catch (error: unknown) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
`Invalid JSON Schema format: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// File: src/modules/json-schema/services/schema-migration.service.ts
|
||||
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
BusinessException,
|
||||
NotFoundException,
|
||||
} from '../../../common/exceptions';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { JsonSchemaService } from '../json-schema.service';
|
||||
|
||||
@@ -66,9 +70,7 @@ export class SchemaMigrationService {
|
||||
]);
|
||||
|
||||
if (!entities || entities.length === 0) {
|
||||
throw new BadRequestException(
|
||||
`Entity ${entityType} with ID ${entityId} not found.`
|
||||
);
|
||||
throw new NotFoundException(entityType, String(entityId));
|
||||
}
|
||||
|
||||
const entity = entities[0];
|
||||
@@ -125,8 +127,10 @@ export class SchemaMigrationService {
|
||||
);
|
||||
|
||||
if (!validation.isValid) {
|
||||
throw new BadRequestException(
|
||||
`Migration failed: Resulting data does not match target schema v${targetSchema.version}. Errors: ${JSON.stringify(validation.errors)}`
|
||||
throw new BusinessException(
|
||||
'SCHEMA_MIGRATION_VALIDATION_FAILED',
|
||||
`Migration failed: Data does not match target schema v${targetSchema.version}`,
|
||||
'การ Migration ล้มเหลว: ข้อมูลไม่ตรงกับ Schema เป้าหมาย'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// File: src/modules/json-schema/services/ui-schema.service.ts
|
||||
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ValidationException } from '../../../common/exceptions';
|
||||
import {
|
||||
UiSchema,
|
||||
UiSchemaField,
|
||||
@@ -21,8 +22,8 @@ export class UiSchemaService {
|
||||
|
||||
// 1. Validate Structure เบื้องต้น
|
||||
if (!uiSchema.layout || !uiSchema.fields) {
|
||||
throw new BadRequestException(
|
||||
'UI Schema must contain "layout" and "fields" properties.'
|
||||
throw new ValidationException(
|
||||
'UI Schema must contain "layout" and "fields" properties'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +35,7 @@ export class UiSchemaService {
|
||||
group.fields.forEach((fieldKey) => {
|
||||
layoutFields.add(fieldKey);
|
||||
if (!definedFields.has(fieldKey)) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
`Field "${fieldKey}" used in layout "${group.title}" is not defined in "fields".`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
BusinessException,
|
||||
ConflictException,
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
NotFoundException,
|
||||
SystemException,
|
||||
ValidationException,
|
||||
} from '../../common/exceptions';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { ImportCorrespondenceDto } from './dto/import-correspondence.dto';
|
||||
@@ -57,7 +58,7 @@ export class MigrationService {
|
||||
userId: number
|
||||
) {
|
||||
if (!idempotencyKey) {
|
||||
throw new BadRequestException('Idempotency-Key header is required');
|
||||
throw new ValidationException('Idempotency-Key header is required');
|
||||
}
|
||||
|
||||
// 1. Idempotency Check
|
||||
@@ -76,7 +77,10 @@ export class MigrationService {
|
||||
};
|
||||
} else {
|
||||
throw new ConflictException(
|
||||
`Transaction failed previously with status ${existingTransaction.statusCode}`
|
||||
'MIGRATION_DUPLICATE_TRANSACTION',
|
||||
`Transaction failed previously with status ${existingTransaction.statusCode}`,
|
||||
'รายการนี้เคยดำเนินการไปแล้วและล้มเหลว',
|
||||
['ตรวจสอบสถานะ Transaction ก่อนหน้า', 'ลองใช้ Idempotency-Key ใหม่']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -114,8 +118,8 @@ export class MigrationService {
|
||||
}
|
||||
|
||||
if (!typeId) {
|
||||
throw new BadRequestException(
|
||||
`Category "${dto.category}" not found in system.`
|
||||
throw new ValidationException(
|
||||
`Category "${dto.category}" not found in system`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,8 +133,8 @@ export class MigrationService {
|
||||
});
|
||||
}
|
||||
if (!status) {
|
||||
throw new InternalServerErrorException(
|
||||
'CRITICAL: No default correspondence status found (missing CLBOWN/DRAFT)'
|
||||
throw new SystemException(
|
||||
'No default correspondence status found (missing CLBOWN/DRAFT)'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,9 +143,7 @@ export class MigrationService {
|
||||
where: { id: dto.projectId },
|
||||
});
|
||||
if (!project) {
|
||||
throw new BadRequestException(
|
||||
`Project ID ${dto.projectId} not found in database`
|
||||
);
|
||||
throw new NotFoundException('Project', String(dto.projectId));
|
||||
}
|
||||
|
||||
const isRFA = type?.typeCode === 'RFA' || dto.category === 'RFA';
|
||||
@@ -397,9 +399,7 @@ export class MigrationService {
|
||||
});
|
||||
await this.importTransactionRepo.save(failedTransaction).catch(() => {});
|
||||
|
||||
throw new InternalServerErrorException(
|
||||
'Migration import failed: ' + errorMessage
|
||||
);
|
||||
throw new SystemException('Migration import failed: ' + errorMessage);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
@@ -407,7 +407,7 @@ export class MigrationService {
|
||||
|
||||
async enqueueRecord(dto: EnqueueMigrationDto) {
|
||||
if (!dto.documentNumber) {
|
||||
throw new BadRequestException('documentNumber is required');
|
||||
throw new ValidationException('documentNumber is required');
|
||||
}
|
||||
|
||||
// Determine status based on confidence policy in ADR-017
|
||||
@@ -492,7 +492,7 @@ export class MigrationService {
|
||||
async getQueueItemById(id: number) {
|
||||
const item = await this.reviewQueueRepo.findOne({ where: { id } });
|
||||
if (!item) {
|
||||
throw new BadRequestException(`Queue item with ID ${id} not found`);
|
||||
throw new NotFoundException('Queue item', String(id));
|
||||
}
|
||||
return item;
|
||||
}
|
||||
@@ -538,12 +538,14 @@ export class MigrationService {
|
||||
) {
|
||||
const queueItem = await this.reviewQueueRepo.findOne({ where: { id } });
|
||||
if (!queueItem) {
|
||||
throw new BadRequestException(`Queue item ${id} not found`);
|
||||
throw new NotFoundException('Queue item', String(id));
|
||||
}
|
||||
|
||||
if (queueItem.status !== MigrationReviewStatus.PENDING) {
|
||||
throw new BadRequestException(
|
||||
`Queue item ${id} is already ${queueItem.status}`
|
||||
throw new BusinessException(
|
||||
'MIGRATION_ITEM_NOT_PENDING',
|
||||
`Queue item ${id} is already ${queueItem.status}`,
|
||||
'รายการนี้ไม่อยู่ในสถานะ PENDING'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -565,7 +567,7 @@ export class MigrationService {
|
||||
userId: number
|
||||
) {
|
||||
if (!idempotencyKey) {
|
||||
throw new BadRequestException('Idempotency-Key header is required');
|
||||
throw new ValidationException('Idempotency-Key header is required');
|
||||
}
|
||||
|
||||
const results = [];
|
||||
@@ -612,7 +614,7 @@ export class MigrationService {
|
||||
async rejectQueueItem(id: number, userId: number) {
|
||||
const queueItem = await this.reviewQueueRepo.findOne({ where: { id } });
|
||||
if (!queueItem) {
|
||||
throw new BadRequestException('Queue item not found');
|
||||
throw new NotFoundException('Queue item', String(id));
|
||||
}
|
||||
|
||||
queueItem.status = MigrationReviewStatus.REJECTED;
|
||||
@@ -628,12 +630,12 @@ export class MigrationService {
|
||||
|
||||
getStagingFileStream(filePath: string) {
|
||||
if (!filePath) {
|
||||
throw new BadRequestException('File path is required');
|
||||
throw new ValidationException('File path is required');
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
if (!existsSync(resolvedPath)) {
|
||||
throw new BadRequestException('File not found at specified path');
|
||||
throw new NotFoundException('File', filePath);
|
||||
}
|
||||
|
||||
return createReadStream(resolvedPath);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// File: src/modules/rfa/rfa.service.ts
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
BusinessException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
PermissionException,
|
||||
SystemException,
|
||||
ValidationException,
|
||||
WorkflowException,
|
||||
} from '../../common/exceptions';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DataSource, In, Repository } from 'typeorm';
|
||||
|
||||
@@ -122,7 +123,8 @@ export class RfaService {
|
||||
const rfaType = await this.rfaTypeRepo.findOne({
|
||||
where: { id: createDto.rfaTypeId },
|
||||
});
|
||||
if (!rfaType) throw new NotFoundException('RFA Type not found');
|
||||
if (!rfaType)
|
||||
throw new NotFoundException('RFA Type', String(createDto.rfaTypeId));
|
||||
|
||||
const rfaTypeCode = rfaType.typeCode.toUpperCase();
|
||||
const rawShopDrawingRefs = createDto.shopDrawingRevisionIds ?? [];
|
||||
@@ -130,25 +132,25 @@ export class RfaService {
|
||||
|
||||
if (['DDW', 'SDW'].includes(rfaTypeCode)) {
|
||||
if (rawShopDrawingRefs.length === 0) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'Selected RFA Type requires at least one Shop Drawing Revision'
|
||||
);
|
||||
}
|
||||
|
||||
if (rawAsBuiltDrawingRefs.length > 0) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'Selected RFA Type cannot reference As-Built Drawing Revisions'
|
||||
);
|
||||
}
|
||||
} else if (rfaTypeCode === 'ADW') {
|
||||
if (rawAsBuiltDrawingRefs.length === 0) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'Selected RFA Type requires at least one As-Built Drawing Revision'
|
||||
);
|
||||
}
|
||||
|
||||
if (rawShopDrawingRefs.length > 0) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'Selected RFA Type cannot reference Shop Drawing Revisions'
|
||||
);
|
||||
}
|
||||
@@ -156,7 +158,7 @@ export class RfaService {
|
||||
rawShopDrawingRefs.length > 0 ||
|
||||
rawAsBuiltDrawingRefs.length > 0
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'Selected RFA Type does not support drawing revision items'
|
||||
);
|
||||
}
|
||||
@@ -185,7 +187,7 @@ export class RfaService {
|
||||
where: { typeCode: 'RFA', isActive: true },
|
||||
});
|
||||
if (!correspondenceType) {
|
||||
throw new InternalServerErrorException(
|
||||
throw new SystemException(
|
||||
'Correspondence Type RFA not found in Master Data'
|
||||
);
|
||||
}
|
||||
@@ -195,8 +197,11 @@ export class RfaService {
|
||||
: rfaType.contractId;
|
||||
|
||||
if (rfaType.contractId !== internalContractId) {
|
||||
throw new BadRequestException(
|
||||
'Selected RFA Type does not belong to the selected contract'
|
||||
throw new BusinessException(
|
||||
'RFA_TYPE_CONTRACT_MISMATCH',
|
||||
'Selected RFA Type does not belong to the selected contract',
|
||||
'ประเภท RFA ที่เลือกไม่ตรงกับสัญญาที่ระบุ',
|
||||
['เลือกประเภท RFA ที่ตรงกับสัญญา']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,12 +211,18 @@ export class RfaService {
|
||||
});
|
||||
|
||||
if (!discipline) {
|
||||
throw new NotFoundException('Discipline not found');
|
||||
throw new NotFoundException(
|
||||
'Discipline',
|
||||
String(createDto.disciplineId)
|
||||
);
|
||||
}
|
||||
|
||||
if (discipline.contractId !== internalContractId) {
|
||||
throw new BadRequestException(
|
||||
'Selected Discipline does not belong to the selected contract'
|
||||
throw new BusinessException(
|
||||
'DISCIPLINE_CONTRACT_MISMATCH',
|
||||
'Selected Discipline does not belong to the selected contract',
|
||||
'Discipline ที่เลือกไม่ตรงกับสัญญาที่ระบุ',
|
||||
['เลือก Discipline ที่ตรงกับสัญญา']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -226,9 +237,7 @@ export class RfaService {
|
||||
where: { statusCode: 'DFT' },
|
||||
});
|
||||
if (!statusDraft) {
|
||||
throw new InternalServerErrorException(
|
||||
'Status DFT (Draft) not found in Master Data'
|
||||
);
|
||||
throw new SystemException('Status DFT (Draft) not found in Master Data');
|
||||
}
|
||||
|
||||
const resolvedOriginatorId = createDto.originatorId
|
||||
@@ -247,15 +256,18 @@ export class RfaService {
|
||||
user.user_id
|
||||
);
|
||||
if (!canManageAll) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to create documents on behalf of other organizations.'
|
||||
throw new PermissionException(
|
||||
'rfa',
|
||||
'create on behalf of other organization'
|
||||
);
|
||||
}
|
||||
userOrgId = resolvedOriginatorId;
|
||||
}
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException('User must belong to an organization');
|
||||
throw new ValidationException(
|
||||
'User must belong to an organization to create RFA'
|
||||
);
|
||||
}
|
||||
|
||||
// EC-RFA-001: Check for existing active RFA per Shop Drawing Revision
|
||||
@@ -273,9 +285,11 @@ export class RfaService {
|
||||
.getMany();
|
||||
|
||||
if (conflictingItems.length > 0) {
|
||||
throw new BadRequestException(
|
||||
'[EC-RFA-001] One or more selected Shop Drawing Revisions already have an active RFA. ' +
|
||||
'A Shop Drawing Revision can only be referenced by one active RFA at a time.'
|
||||
throw new BusinessException(
|
||||
'EC_RFA_001_ACTIVE_RFA_EXISTS',
|
||||
'[EC-RFA-001] One or more selected Shop Drawing Revisions already have an active RFA.',
|
||||
'Shop Drawing Revision ที่เลือกมี RFA ที่ยังใช้งานอยู่แล้ว',
|
||||
['ตรวจสอบ RFA ที่มีอยู่', 'เลือก Shop Drawing Revision อื่น']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -315,8 +329,8 @@ export class RfaService {
|
||||
}
|
||||
);
|
||||
if (!corrStatusDraft)
|
||||
throw new InternalServerErrorException(
|
||||
'Correspondence Status DRAFT not found'
|
||||
throw new SystemException(
|
||||
'Correspondence Status DRAFT not found in Master Data'
|
||||
);
|
||||
|
||||
// 1. Create Correspondence Record
|
||||
@@ -385,7 +399,7 @@ export class RfaService {
|
||||
});
|
||||
|
||||
if (shopDrawings.length !== shopDrawingRevisionIds.length) {
|
||||
throw new NotFoundException('Some Shop Drawing Revisions not found');
|
||||
throw new NotFoundException('Shop Drawing Revision');
|
||||
}
|
||||
|
||||
rfaItems.push(
|
||||
@@ -405,9 +419,7 @@ export class RfaService {
|
||||
});
|
||||
|
||||
if (asBuiltDrawings.length !== asBuiltDrawingRevisionIds.length) {
|
||||
throw new NotFoundException(
|
||||
'Some As-Built Drawing Revisions not found'
|
||||
);
|
||||
throw new NotFoundException('As-Built Drawing Revision');
|
||||
}
|
||||
|
||||
rfaItems.push(
|
||||
@@ -588,7 +600,7 @@ export class RfaService {
|
||||
select: ['id'],
|
||||
});
|
||||
if (!correspondence) {
|
||||
throw new NotFoundException(`RFA with publicId ${publicId} not found`);
|
||||
throw new NotFoundException('RFA', publicId);
|
||||
}
|
||||
return this.findOne(correspondence.id);
|
||||
}
|
||||
@@ -599,7 +611,7 @@ export class RfaService {
|
||||
select: ['id'],
|
||||
});
|
||||
if (!correspondence) {
|
||||
throw new NotFoundException(`RFA with publicId ${publicId} not found`);
|
||||
throw new NotFoundException('RFA', publicId);
|
||||
}
|
||||
return this.findOne(correspondence.id, true);
|
||||
}
|
||||
@@ -628,7 +640,7 @@ export class RfaService {
|
||||
});
|
||||
|
||||
if (!rfa) {
|
||||
throw new NotFoundException(`RFA ID ${id} not found`);
|
||||
throw new NotFoundException('RFA', String(id));
|
||||
}
|
||||
|
||||
if (rawEntities) {
|
||||
@@ -657,12 +669,17 @@ export class RfaService {
|
||||
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
||||
if (!currentCorrRev || !currentCorrRev.rfaRevision)
|
||||
throw new NotFoundException('Current revision not found');
|
||||
throw new NotFoundException('Current revision');
|
||||
|
||||
const currentRfaRev = currentCorrRev.rfaRevision;
|
||||
|
||||
if (currentRfaRev.statusCode.statusCode !== 'DFT') {
|
||||
throw new BadRequestException('Only DRAFT documents can be submitted');
|
||||
throw new WorkflowException(
|
||||
'RFA_INVALID_SUBMIT_STATUS',
|
||||
'Only DRAFT documents can be submitted',
|
||||
'สามารถส่งได้เฉพาะเอกสารสถานะ DRAFT เท่านั้น',
|
||||
['ตรวจสอบสถานะเอกสารปัจจุบัน']
|
||||
);
|
||||
}
|
||||
|
||||
const template = await this.templateRepo.findOne({
|
||||
@@ -671,7 +688,12 @@ export class RfaService {
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new BadRequestException('Invalid routing template');
|
||||
throw new BusinessException(
|
||||
'ROUTING_TEMPLATE_NOT_FOUND',
|
||||
'Invalid routing template',
|
||||
'ไม่พบ Routing Template ที่กำหนด',
|
||||
['ตรวจสอบ Routing Template ที่ตั้งค่าไว้']
|
||||
);
|
||||
}
|
||||
|
||||
// Manual fetch of steps
|
||||
@@ -681,14 +703,19 @@ export class RfaService {
|
||||
});
|
||||
|
||||
if (steps.length === 0) {
|
||||
throw new BadRequestException('Routing template has no steps');
|
||||
throw new BusinessException(
|
||||
'ROUTING_TEMPLATE_EMPTY',
|
||||
'Routing template has no steps',
|
||||
'Routing Template ไม่มีขั้นตอนกำหนดไว้',
|
||||
['เพิ่ม Step ใน Routing Template']
|
||||
);
|
||||
}
|
||||
|
||||
const statusForApprove = await this.rfaStatusRepo.findOne({
|
||||
where: { statusCode: 'FAP' },
|
||||
});
|
||||
if (!statusForApprove)
|
||||
throw new InternalServerErrorException('Status FAP not found');
|
||||
throw new SystemException('Status FAP not found in Master Data');
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -765,11 +792,14 @@ export class RfaService {
|
||||
});
|
||||
|
||||
if (!currentRouting)
|
||||
throw new BadRequestException('No active workflow step found');
|
||||
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
|
||||
throw new ForbiddenException(
|
||||
'You are not authorized to process this step'
|
||||
throw new WorkflowException(
|
||||
'NO_ACTIVE_WORKFLOW_STEP',
|
||||
'No active workflow step found',
|
||||
'ไม่พบขั้นตอน Workflow ที่ยังเปิดอยู่',
|
||||
['ตรวจสอบสถานะ Workflow ของเอกสาร']
|
||||
);
|
||||
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
|
||||
throw new PermissionException('rfa workflow step', 'process');
|
||||
}
|
||||
|
||||
const template = await this.templateRepo.findOne({
|
||||
@@ -777,7 +807,10 @@ export class RfaService {
|
||||
// relations: ['steps'],
|
||||
});
|
||||
|
||||
if (!template) throw new InternalServerErrorException('Template not found');
|
||||
if (!template)
|
||||
throw new SystemException(
|
||||
'Routing Template not found for workflow processing'
|
||||
);
|
||||
|
||||
// Manual fetch steps
|
||||
const steps = await this.templateStepRepo.find({
|
||||
@@ -786,7 +819,7 @@ export class RfaService {
|
||||
});
|
||||
|
||||
if (steps.length === 0)
|
||||
throw new InternalServerErrorException('Template steps not found');
|
||||
throw new SystemException('Routing Template steps not found');
|
||||
|
||||
// Call Engine to calculate next step
|
||||
const result = this.workflowEngine.processAction(
|
||||
@@ -874,13 +907,16 @@ export class RfaService {
|
||||
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
||||
if (!currentCorrRev || !currentCorrRev.rfaRevision)
|
||||
throw new NotFoundException('Current revision not found');
|
||||
throw new NotFoundException('Current revision');
|
||||
|
||||
const currentRfaRev = currentCorrRev.rfaRevision;
|
||||
|
||||
if (currentRfaRev.statusCode.statusCode !== 'DFT') {
|
||||
throw new BadRequestException(
|
||||
'Only DRAFT documents can be edited. Submit a new revision for non-draft documents.'
|
||||
throw new WorkflowException(
|
||||
'RFA_EDIT_NON_DRAFT',
|
||||
'Only DRAFT documents can be edited',
|
||||
'สามารถแก้ไขได้เฉพาะเอกสารสถานะ DRAFT เท่านั้น',
|
||||
['ส่งเอกสารเพื่อสร้าง Revision ใหม่สำหรับเอกสารที่ไม่ใช่ DRAFT']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -915,13 +951,16 @@ export class RfaService {
|
||||
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
||||
if (!currentCorrRev || !currentCorrRev.rfaRevision)
|
||||
throw new NotFoundException('Current revision not found');
|
||||
throw new NotFoundException('Current revision');
|
||||
|
||||
const currentRfaRev = currentCorrRev.rfaRevision;
|
||||
|
||||
if (currentRfaRev.statusCode.statusCode !== 'DFT') {
|
||||
throw new BadRequestException(
|
||||
'Only DRAFT documents can be cancelled. Contact an Org Admin to cancel submitted documents.'
|
||||
throw new WorkflowException(
|
||||
'RFA_CANCEL_NON_DRAFT',
|
||||
'Only DRAFT documents can be cancelled',
|
||||
'สามารถยกเลิกได้เฉพาะเอกสารสถานะ DRAFT เท่านั้น',
|
||||
['ติดต่อ Org Admin เพื่อยกเลิกเอกสารที่ส่งแล้ว']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -929,7 +968,7 @@ export class RfaService {
|
||||
where: { statusCode: 'CC' },
|
||||
});
|
||||
if (!statusCC)
|
||||
throw new InternalServerErrorException(
|
||||
throw new SystemException(
|
||||
'Status CC (Cancelled) not found in Master Data'
|
||||
);
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
InternalServerErrorException,
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
PermissionException,
|
||||
SystemException,
|
||||
ValidationException,
|
||||
} from '../../common/exceptions';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { Transmittal } from './entities/transmittal.entity';
|
||||
@@ -57,13 +56,13 @@ export class TransmittalService {
|
||||
const type = await this.typeRepo.findOne({
|
||||
where: { typeCode: 'TRN' }, // Adjust code as per Master Data
|
||||
});
|
||||
if (!type) throw new NotFoundException('Transmittal Type (TRN) not found');
|
||||
if (!type) throw new NotFoundException('Transmittal Type (TRN)');
|
||||
|
||||
const statusDraft = await this.statusRepo.findOne({
|
||||
where: { statusCode: 'DRAFT' },
|
||||
});
|
||||
if (!statusDraft)
|
||||
throw new InternalServerErrorException('Status DRAFT not found');
|
||||
throw new SystemException('Status DRAFT not found in Master Data');
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -86,15 +85,16 @@ export class TransmittalService {
|
||||
user.user_id
|
||||
);
|
||||
if (!canManageAll) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to create documents on behalf of other organizations.'
|
||||
throw new PermissionException(
|
||||
'transmittal',
|
||||
'create on behalf of other organization'
|
||||
);
|
||||
}
|
||||
userOrgId = resolvedOriginatorId;
|
||||
}
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'User must belong to an organization to create a transmittal'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ValidationException } from '../../common/exceptions';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { UserAssignment } from './entities/user-assignment.entity';
|
||||
@@ -22,7 +23,7 @@ export class UserAssignmentService {
|
||||
(v) => v != null
|
||||
);
|
||||
if (scopes.length > 1) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
'Cannot assign multiple scopes at once. Choose one of Org, Project, or Contract.'
|
||||
);
|
||||
}
|
||||
@@ -55,7 +56,7 @@ export class UserAssignmentService {
|
||||
// Validation (Scope)
|
||||
const scopes = [organizationId, projectId].filter((v) => v != null);
|
||||
if (scopes.length > 1) {
|
||||
throw new BadRequestException(
|
||||
throw new ValidationException(
|
||||
`User ${userId}: Cannot assign multiple scopes.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// File: src/modules/user/user.service.ts
|
||||
// บันทึกการแก้ไข: แก้ไข Error TS1272 โดยใช้ 'import type' สำหรับ Cache interface (T1.3)
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
Inject,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { NotFoundException, ConflictException } from '../../common/exceptions';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
@@ -56,7 +52,12 @@ export class UserService {
|
||||
} catch (error: unknown) {
|
||||
const dbError = error as { code?: string };
|
||||
if (dbError.code === 'ER_DUP_ENTRY') {
|
||||
throw new ConflictException('Username or Email already exists');
|
||||
throw new ConflictException(
|
||||
'USER_DUPLICATE',
|
||||
'Username or Email already exists',
|
||||
'ชื่อผู้ใช้หรืออีเมลนี้มีอยู่ในระบบแล้ว',
|
||||
['ลองใช้ชื่อผู้ใช้หรืออีเมลอื่น']
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -152,7 +153,7 @@ export class UserService {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${id} not found`);
|
||||
throw new NotFoundException('User', String(id));
|
||||
}
|
||||
|
||||
return user;
|
||||
@@ -171,7 +172,7 @@ export class UserService {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with publicId ${publicId} not found`);
|
||||
throw new NotFoundException('User', publicId);
|
||||
}
|
||||
|
||||
return user;
|
||||
@@ -217,7 +218,7 @@ export class UserService {
|
||||
const result = await this.usersRepository.softDelete(user.user_id);
|
||||
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`User with UUID ${uuid} not found`);
|
||||
throw new NotFoundException('User', uuid);
|
||||
}
|
||||
// เคลียร์ Cache เมื่อลบ
|
||||
await this.clearUserCache(user.user_id);
|
||||
@@ -275,7 +276,7 @@ export class UserService {
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new NotFoundException(`Role ID ${roleId} not found`);
|
||||
throw new NotFoundException('Role', String(roleId));
|
||||
}
|
||||
|
||||
// Load permissions entities
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
NotFoundException,
|
||||
ValidationException,
|
||||
WorkflowException,
|
||||
} from '../../../common/exceptions';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { WorkflowDslSchema, WorkflowDsl } from './workflow-dsl.schema';
|
||||
@@ -36,16 +41,18 @@ export class WorkflowDslParser {
|
||||
return await this.workflowDefRepo.save(definition);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new BadRequestException(`Invalid JSON: ${error.message}`);
|
||||
throw new ValidationException(`Invalid JSON: ${error.message}`);
|
||||
}
|
||||
const err = error as {
|
||||
name?: string;
|
||||
errors?: unknown;
|
||||
errors?: unknown[];
|
||||
message?: string;
|
||||
};
|
||||
if (err.name === 'ZodError') {
|
||||
throw new BadRequestException(
|
||||
`Invalid workflow DSL: ${JSON.stringify(err.errors)}`
|
||||
throw new WorkflowException(
|
||||
'INVALID_WORKFLOW_DSL',
|
||||
`Invalid workflow DSL: ${JSON.stringify(err.errors)}`,
|
||||
'Workflow DSL ไม่ถูกต้อง'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
@@ -66,16 +73,20 @@ export class WorkflowDslParser {
|
||||
|
||||
// 1. Validate initial state
|
||||
if (!stateSet.has(dsl.initialState)) {
|
||||
throw new BadRequestException(
|
||||
`Initial state "${dsl.initialState}" not found in states array`
|
||||
throw new WorkflowException(
|
||||
'DSL_INVALID_INITIAL_STATE',
|
||||
`Initial state "${dsl.initialState}" not found in states array`,
|
||||
'Initial State ไม่พบใน States Array'
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Validate final states
|
||||
dsl.finalStates.forEach((state) => {
|
||||
if (!stateSet.has(state)) {
|
||||
throw new BadRequestException(
|
||||
`Final state "${state}" not found in states array`
|
||||
throw new WorkflowException(
|
||||
'DSL_INVALID_FINAL_STATE',
|
||||
`Final state "${state}" not found in states array`,
|
||||
'Final State ไม่พบใน States Array'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -86,15 +97,19 @@ export class WorkflowDslParser {
|
||||
dsl.transitions.forEach((transition, index) => {
|
||||
// Check 'from' state
|
||||
if (!stateSet.has(transition.from)) {
|
||||
throw new BadRequestException(
|
||||
`Transition ${index}: 'from' state "${transition.from}" not found in states array`
|
||||
throw new WorkflowException(
|
||||
'DSL_INVALID_TRANSITION_FROM',
|
||||
`Transition ${index}: 'from' state "${transition.from}" not found in states array`,
|
||||
'Transition อ้างอิง State ที่ไม่พบ'
|
||||
);
|
||||
}
|
||||
|
||||
// Check 'to' state
|
||||
if (!stateSet.has(transition.to)) {
|
||||
throw new BadRequestException(
|
||||
`Transition ${index}: 'to' state "${transition.to}" not found in states array`
|
||||
throw new WorkflowException(
|
||||
'DSL_INVALID_TRANSITION_TO',
|
||||
`Transition ${index}: 'to' state "${transition.to}" not found in states array`,
|
||||
'Transition อ้างอิง State ที่ไม่พบ'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,8 +135,10 @@ export class WorkflowDslParser {
|
||||
dsl.transitions.forEach((transition) => {
|
||||
const key = `${transition.from}-${transition.trigger}-${transition.to}`;
|
||||
if (transitionKeys.has(key)) {
|
||||
throw new BadRequestException(
|
||||
`Duplicate transition: ${transition.from} --[${transition.trigger}]--> ${transition.to}`
|
||||
throw new WorkflowException(
|
||||
'DSL_DUPLICATE_TRANSITION',
|
||||
`Duplicate transition: ${transition.from} --[${transition.trigger}]--> ${transition.to}`,
|
||||
'DSL มี Transition ซ้ำซ้อน'
|
||||
);
|
||||
}
|
||||
transitionKeys.add(key);
|
||||
@@ -158,9 +175,7 @@ export class WorkflowDslParser {
|
||||
});
|
||||
|
||||
if (!definition) {
|
||||
throw new BadRequestException(
|
||||
`Workflow definition ${definitionId} not found`
|
||||
);
|
||||
throw new NotFoundException('Workflow definition', String(definitionId));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -171,8 +186,10 @@ export class WorkflowDslParser {
|
||||
`Failed to parse stored DSL for definition ${definitionId}`,
|
||||
error
|
||||
);
|
||||
throw new BadRequestException(
|
||||
`Invalid stored DSL: ${error instanceof Error ? error.message : String(error)}`
|
||||
throw new WorkflowException(
|
||||
'INVALID_STORED_DSL',
|
||||
`Invalid stored DSL: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'DSL ที่บันทึกไว้ไม่ถูกต้อง'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// File: src/modules/workflow-engine/workflow-dsl.service.ts
|
||||
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
ValidationException,
|
||||
WorkflowException,
|
||||
} from '../../common/exceptions';
|
||||
|
||||
// ==========================================
|
||||
// 1. Interfaces for RAW DSL (Input from User)
|
||||
@@ -86,8 +90,11 @@ export class WorkflowDslService {
|
||||
for (const rawState of dsl.states) {
|
||||
if (rawState.initial) {
|
||||
if (initialFound) {
|
||||
throw new BadRequestException(
|
||||
`DSL Error: Multiple initial states found (at "${rawState.name}").`
|
||||
throw new WorkflowException(
|
||||
'DSL_MULTIPLE_INITIAL_STATES',
|
||||
`DSL Error: Multiple initial states found (at "${rawState.name}")`,
|
||||
'DSL มี Initial State หลายค่า แต่ละ Workflow ต้องมีเพียง Initial State เดียว',
|
||||
['ตรวจสอบโครงสร้าง DSL และแก้ไข Initial State']
|
||||
);
|
||||
}
|
||||
compiled.initialState = rawState.name;
|
||||
@@ -104,8 +111,11 @@ export class WorkflowDslService {
|
||||
for (const [action, rule] of Object.entries(rawState.on)) {
|
||||
// Validation: Target state must exist
|
||||
if (!definedStates.has(rule.to)) {
|
||||
throw new BadRequestException(
|
||||
`DSL Error: State "${rawState.name}" transitions via "${action}" to unknown state "${rule.to}".`
|
||||
throw new WorkflowException(
|
||||
'DSL_UNKNOWN_TRANSITION_TARGET',
|
||||
`DSL Error: State "${rawState.name}" transitions via "${action}" to unknown state "${rule.to}"`,
|
||||
'DSL อ้างอิง State ที่ไม่พบ',
|
||||
['ตรวจสอบชื่อ State ที่กำหนดใน Transition']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,7 +143,12 @@ export class WorkflowDslService {
|
||||
}
|
||||
|
||||
if (!initialFound) {
|
||||
throw new BadRequestException('DSL Error: No initial state defined.');
|
||||
throw new WorkflowException(
|
||||
'DSL_NO_INITIAL_STATE',
|
||||
'DSL Error: No initial state defined',
|
||||
'DSL ไม่มีการกำหนด Initial State',
|
||||
['เพิ่ม initial: true ใน State หนึ่ง']
|
||||
);
|
||||
}
|
||||
|
||||
return compiled;
|
||||
@@ -153,15 +168,21 @@ export class WorkflowDslService {
|
||||
|
||||
// 1. Validate State Existence
|
||||
if (!stateConfig) {
|
||||
throw new BadRequestException(
|
||||
`Runtime Error: Current state "${currentState}" is invalid.`
|
||||
throw new WorkflowException(
|
||||
'WORKFLOW_INVALID_CURRENT_STATE',
|
||||
`Runtime Error: Current state "${currentState}" is invalid`,
|
||||
'Workflow อยู่ในสถานะที่ไม่รู้จัก',
|
||||
['ตรวจสอบ DSL ของ Workflow']
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Check if terminal
|
||||
if (stateConfig.terminal) {
|
||||
throw new BadRequestException(
|
||||
`Runtime Error: Cannot transition from terminal state "${currentState}".`
|
||||
throw new WorkflowException(
|
||||
'WORKFLOW_TERMINAL_STATE',
|
||||
`Runtime Error: Cannot transition from terminal state "${currentState}"`,
|
||||
'ไม่สามารถดำเนินการจาก State สุดท้ายได้',
|
||||
['เอกสารสิ้นสุดกระบวนการแล้ว']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -169,8 +190,11 @@ export class WorkflowDslService {
|
||||
const transition = stateConfig.transitions[action];
|
||||
if (!transition) {
|
||||
const allowed = Object.keys(stateConfig.transitions).join(', ');
|
||||
throw new BadRequestException(
|
||||
`Invalid Action: "${action}" is not allowed from "${currentState}". Allowed: [${allowed}]`
|
||||
throw new WorkflowException(
|
||||
'WORKFLOW_INVALID_ACTION',
|
||||
`Invalid Action: "${action}" is not allowed from "${currentState}". Allowed: [${allowed}]`,
|
||||
`ไม่สามารถดำเนินการ "${action}" ในสถานะปัจจุบัน ทำได้: [${allowed}]`,
|
||||
['เลือกการดำเนินการที่อนุญาตจากรายการ']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,8 +205,11 @@ export class WorkflowDslService {
|
||||
if (transition.condition) {
|
||||
const isMet = this.evaluateCondition(transition.condition, context);
|
||||
if (!isMet) {
|
||||
throw new BadRequestException(
|
||||
'Condition Failed: The criteria for this transition are not met.'
|
||||
throw new WorkflowException(
|
||||
'WORKFLOW_CONDITION_NOT_MET',
|
||||
'Condition Failed: The criteria for this transition are not met',
|
||||
'เงื่อนไขสำหรับการดำเนินการนี้ไม่ผ่าน',
|
||||
['ตรวจสอบเงื่อนไขที่กำหนดใน Workflow']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -199,12 +226,12 @@ export class WorkflowDslService {
|
||||
|
||||
private validateSchemaStructure(dsl: unknown) {
|
||||
if (!dsl || typeof dsl !== 'object') {
|
||||
throw new BadRequestException('DSL must be a JSON object.');
|
||||
throw new ValidationException('DSL must be a JSON object');
|
||||
}
|
||||
const d = dsl as Record<string, unknown>;
|
||||
if (!d.workflow || !d.states || !Array.isArray(d.states)) {
|
||||
throw new BadRequestException(
|
||||
'DSL Error: Missing required fields (workflow, states).'
|
||||
throw new ValidationException(
|
||||
'DSL Error: Missing required fields (workflow, states)'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -226,15 +253,23 @@ export class WorkflowDslService {
|
||||
if (requiredRoles.length > 0) {
|
||||
const hasRole = requiredRoles.some((r) => userRoles.includes(r));
|
||||
if (!hasRole) {
|
||||
throw new BadRequestException(
|
||||
`Access Denied: Required roles [${requiredRoles.join(', ')}]`
|
||||
throw new WorkflowException(
|
||||
'WORKFLOW_ROLE_REQUIRED',
|
||||
`Access Denied: Required roles [${requiredRoles.join(', ')}]`,
|
||||
`ต้องมี Role: [${requiredRoles.join(', ')}] จึงจะดำเนินการนี้ได้`,
|
||||
['ติดต่อผู้ดูแลระบบเพื่อขอสิทธิ์']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check Specific User
|
||||
if (req.userId && String(req.userId) !== String(userId)) {
|
||||
throw new BadRequestException('Access Denied: User mismatch.');
|
||||
throw new WorkflowException(
|
||||
'WORKFLOW_USER_MISMATCH',
|
||||
'Access Denied: User mismatch',
|
||||
'ผู้ใช้ไม่ได้รับอนุญาตให้ดำเนินการนี้',
|
||||
['ตรวจสอบว่าเล็็กชื่ออีเมลที่ป้อนให้เข้าสู่ระบบ']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
// File: src/modules/workflow-engine/workflow-engine.service.ts
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { NotFoundException, WorkflowException } from '../../common/exceptions';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { WorkflowDefinition } from './entities/workflow-definition.entity';
|
||||
import { WorkflowHistory } from './entities/workflow-history.entity';
|
||||
@@ -104,9 +99,7 @@ export class WorkflowEngineService {
|
||||
): Promise<WorkflowDefinition> {
|
||||
const definition = await this.workflowDefRepo.findOne({ where: { id } });
|
||||
if (!definition) {
|
||||
throw new NotFoundException(
|
||||
`Workflow Definition with ID "${id}" not found`
|
||||
);
|
||||
throw new NotFoundException('Workflow Definition', id);
|
||||
}
|
||||
|
||||
if (dto.dsl) {
|
||||
@@ -115,8 +108,11 @@ export class WorkflowEngineService {
|
||||
definition.dsl = dto.dsl as unknown as Record<string, unknown>;
|
||||
definition.compiled = compiled as unknown as Record<string, unknown>;
|
||||
} catch (error: unknown) {
|
||||
throw new BadRequestException(
|
||||
`Invalid DSL: ${error instanceof Error ? error.message : String(error)}`
|
||||
throw new WorkflowException(
|
||||
'INVALID_WORKFLOW_DSL',
|
||||
`Invalid DSL: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'Workflow DSL ไม่ถูกต้อง กรุณาตรวจสอบโครงสร้าง',
|
||||
['ตรวจสอบ syntax ของ DSL', 'ดูตัวอย่าง DSL ที่ถูกต้อง']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -149,9 +145,7 @@ export class WorkflowEngineService {
|
||||
async getDefinitionById(id: string): Promise<WorkflowDefinition> {
|
||||
const definition = await this.workflowDefRepo.findOne({ where: { id } });
|
||||
if (!definition) {
|
||||
throw new NotFoundException(
|
||||
`Workflow Definition with ID "${id}" not found`
|
||||
);
|
||||
throw new NotFoundException('Workflow Definition', id);
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
@@ -197,9 +191,7 @@ export class WorkflowEngineService {
|
||||
});
|
||||
|
||||
if (!definition) {
|
||||
throw new NotFoundException(
|
||||
`Workflow "${workflowCode}" not found or inactive.`
|
||||
);
|
||||
throw new NotFoundException('Workflow', workflowCode);
|
||||
}
|
||||
|
||||
// 2. หา Initial State จาก Compiled Structure
|
||||
@@ -209,8 +201,11 @@ export class WorkflowEngineService {
|
||||
const initialState = compiled.initialState;
|
||||
|
||||
if (!initialState) {
|
||||
throw new BadRequestException(
|
||||
`Workflow "${workflowCode}" has no initial state defined.`
|
||||
throw new WorkflowException(
|
||||
'WORKFLOW_NO_INITIAL_STATE',
|
||||
`Workflow "${workflowCode}" has no initial state defined`,
|
||||
'Workflow ไม่มี Initial State ที่กำหนด',
|
||||
['ตรวจสอบ DSL ของ Workflow นี้']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -242,9 +237,7 @@ export class WorkflowEngineService {
|
||||
});
|
||||
|
||||
if (!instance) {
|
||||
throw new NotFoundException(
|
||||
`Workflow Instance "${instanceId}" not found`
|
||||
);
|
||||
throw new NotFoundException('Workflow Instance', instanceId);
|
||||
}
|
||||
|
||||
return instance;
|
||||
@@ -276,14 +269,15 @@ export class WorkflowEngineService {
|
||||
});
|
||||
|
||||
if (!instance) {
|
||||
throw new NotFoundException(
|
||||
`Workflow Instance "${instanceId}" not found.`
|
||||
);
|
||||
throw new NotFoundException('Workflow Instance', instanceId);
|
||||
}
|
||||
|
||||
if (instance.status !== WorkflowStatus.ACTIVE) {
|
||||
throw new BadRequestException(
|
||||
`Workflow is not active (Status: ${instance.status}).`
|
||||
throw new WorkflowException(
|
||||
'WORKFLOW_NOT_ACTIVE',
|
||||
`Workflow is not active (Status: ${instance.status})`,
|
||||
'Workflow ไม่อยู่ในสถานะ Active',
|
||||
['ตรวจสอบสถานะของ Workflow']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -427,7 +421,12 @@ export class WorkflowEngineService {
|
||||
case 'RETURN': {
|
||||
const targetStep = returnToSequence || currentSequence - 1;
|
||||
if (targetStep < 1) {
|
||||
throw new BadRequestException('Cannot return beyond the first step');
|
||||
throw new WorkflowException(
|
||||
'WORKFLOW_INVALID_RETURN_TARGET',
|
||||
'Cannot return beyond the first step',
|
||||
'ไม่สามารถส่งคืนไปเกินกว่าขั้นตอนแรกได้',
|
||||
['ตรวจสอบลำดับขั้นตอนที่ต้องการส่งคืน']
|
||||
);
|
||||
}
|
||||
return {
|
||||
nextStepSequence: targetStep,
|
||||
|
||||
Reference in New Issue
Block a user