251124:1700 Ready to Phase 7
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
// File: src/modules/workflow-engine/dto/create-workflow-definition.dto.ts
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateWorkflowDefinitionDto {
|
||||
@ApiProperty({ example: 'RFA', description: 'รหัสของ Workflow' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
workflow_code!: string; // เพิ่ม !
|
||||
|
||||
@ApiProperty({ description: 'นิยาม Workflow' })
|
||||
@IsObject()
|
||||
@IsNotEmpty()
|
||||
dsl!: any; // เพิ่ม !
|
||||
|
||||
@ApiProperty({ description: 'เปิดใช้งานทันทีหรือไม่', default: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
is_active?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// File: src/modules/workflow-engine/dto/evaluate-workflow.dto.ts
|
||||
import { IsString, IsNotEmpty, IsObject, IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class EvaluateWorkflowDto {
|
||||
@ApiProperty({ example: 'RFA', description: 'รหัส Workflow' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
workflow_code!: string; // เพิ่ม !
|
||||
|
||||
@ApiProperty({ example: 'DRAFT', description: 'สถานะปัจจุบัน' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
current_state!: string; // เพิ่ม !
|
||||
|
||||
@ApiProperty({ example: 'SUBMIT', description: 'Action ที่ต้องการทำ' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
action!: string; // เพิ่ม !
|
||||
|
||||
@ApiProperty({ description: 'Context', example: { userId: 1 } })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
context?: Record<string, any>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// File: src/modules/workflow-engine/dto/get-available-actions.dto.ts
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class GetAvailableActionsDto {
|
||||
@ApiProperty({ description: 'รหัส Workflow', example: 'RFA' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
workflow_code!: string; // เพิ่ม !
|
||||
|
||||
@ApiProperty({ description: 'สถานะปัจจุบัน', example: 'DRAFT' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
current_state!: string; // เพิ่ม !
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// File: src/modules/workflow-engine/dto/update-workflow-definition.dto.ts
|
||||
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateWorkflowDefinitionDto } from './create-workflow-definition.dto';
|
||||
|
||||
// PartialType จะทำให้ทุก field ใน CreateDto กลายเป็น Optional (?)
|
||||
// เหมาะสำหรับ PATCH method
|
||||
export class UpdateWorkflowDefinitionDto extends PartialType(
|
||||
CreateWorkflowDefinitionDto,
|
||||
) {}
|
||||
@@ -0,0 +1,37 @@
|
||||
// File: src/modules/workflow-engine/entities/workflow-definition.entity.ts
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('workflow_definitions')
|
||||
@Index(['workflow_code', 'is_active', 'version'])
|
||||
export class WorkflowDefinition {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string; // เพิ่ม !
|
||||
|
||||
@Column({ length: 50, comment: 'รหัส Workflow เช่น RFA, CORR' })
|
||||
workflow_code!: string; // เพิ่ม !
|
||||
|
||||
@Column({ type: 'int', default: 1, comment: 'หมายเลข Version' })
|
||||
version!: number; // เพิ่ม !
|
||||
|
||||
@Column({ type: 'json', comment: 'นิยาม Workflow ต้นฉบับ' })
|
||||
dsl!: any; // เพิ่ม !
|
||||
|
||||
@Column({ type: 'json', comment: 'โครงสร้างที่ Compile แล้ว' })
|
||||
compiled!: any; // เพิ่ม !
|
||||
|
||||
@Column({ default: true, comment: 'สถานะการใช้งาน' })
|
||||
is_active!: boolean; // เพิ่ม !
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at!: Date; // เพิ่ม !
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at!: Date; // เพิ่ม !
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// File: src/modules/workflow-engine/workflow-dsl.service.ts
|
||||
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
|
||||
export interface WorkflowState {
|
||||
initial?: boolean;
|
||||
terminal?: boolean;
|
||||
transitions?: Record<string, TransitionRule>;
|
||||
}
|
||||
|
||||
export interface TransitionRule {
|
||||
to: string;
|
||||
requirements?: RequirementRule[];
|
||||
events?: EventRule[];
|
||||
}
|
||||
|
||||
export interface RequirementRule {
|
||||
role?: string;
|
||||
user?: string;
|
||||
condition?: string; // e.g. "amount > 5000" (Advanced)
|
||||
}
|
||||
|
||||
export interface EventRule {
|
||||
type: 'notify' | 'webhook' | 'update_status';
|
||||
target?: string;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
export interface CompiledWorkflow {
|
||||
workflow: string;
|
||||
version: string | number;
|
||||
states: Record<string, WorkflowState>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowDslService {
|
||||
/**
|
||||
* คอมไพล์ DSL Input ให้เป็น Standard Execution Tree
|
||||
* @param dsl ข้อมูลดิบจาก User (JSON/Object)
|
||||
* @returns CompiledWorkflow Object ที่พร้อมใช้งาน
|
||||
*/
|
||||
compile(dsl: any): CompiledWorkflow {
|
||||
// 1. Basic Structure Validation
|
||||
if (!dsl.states || !Array.isArray(dsl.states)) {
|
||||
throw new BadRequestException(
|
||||
'DSL syntax error: "states" array is required.',
|
||||
);
|
||||
}
|
||||
|
||||
const compiled: CompiledWorkflow = {
|
||||
workflow: dsl.workflow || 'UNKNOWN',
|
||||
version: dsl.version || 1,
|
||||
states: {},
|
||||
};
|
||||
|
||||
const stateMap = new Set<string>();
|
||||
|
||||
// 2. First Pass: Collect all state names and normalize structure
|
||||
for (const rawState of dsl.states) {
|
||||
if (!rawState.name) {
|
||||
throw new BadRequestException(
|
||||
'DSL syntax error: All states must have a "name".',
|
||||
);
|
||||
}
|
||||
|
||||
stateMap.add(rawState.name);
|
||||
|
||||
const normalizedState: WorkflowState = {
|
||||
initial: !!rawState.initial,
|
||||
terminal: !!rawState.terminal,
|
||||
transitions: {},
|
||||
};
|
||||
|
||||
// Normalize transitions "on:"
|
||||
if (rawState.on) {
|
||||
for (const [action, rule] of Object.entries(rawState.on)) {
|
||||
const rawRule = rule as any;
|
||||
normalizedState.transitions![action] = {
|
||||
to: rawRule.to,
|
||||
requirements: rawRule.require || [],
|
||||
events: rawRule.events || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
compiled.states[rawState.name] = normalizedState;
|
||||
}
|
||||
|
||||
// 3. Second Pass: Validate Integrity
|
||||
this.validateIntegrity(compiled, stateMap);
|
||||
|
||||
return compiled;
|
||||
}
|
||||
|
||||
/**
|
||||
* ตรวจสอบความสมบูรณ์ของ Workflow Logic
|
||||
*/
|
||||
private validateIntegrity(compiled: CompiledWorkflow, stateMap: Set<string>) {
|
||||
let hasInitial = false;
|
||||
|
||||
for (const [stateName, state] of Object.entries(compiled.states)) {
|
||||
if (state.initial) {
|
||||
if (hasInitial)
|
||||
throw new BadRequestException(
|
||||
`DSL Error: Multiple initial states found.`,
|
||||
);
|
||||
hasInitial = true;
|
||||
}
|
||||
|
||||
// ตรวจสอบ Transitions
|
||||
if (state.transitions) {
|
||||
for (const [action, rule] of Object.entries(state.transitions)) {
|
||||
// 1. ปลายทางต้องมีอยู่จริง
|
||||
if (!stateMap.has(rule.to)) {
|
||||
throw new BadRequestException(
|
||||
`DSL Error: State "${stateName}" transitions via "${action}" to unknown state "${rule.to}".`,
|
||||
);
|
||||
}
|
||||
// 2. Action name convention (Optional but recommended)
|
||||
if (!/^[A-Z0-9_]+$/.test(action)) {
|
||||
// Warning or Strict Error could be here
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasInitial) {
|
||||
throw new BadRequestException('DSL Error: No initial state defined.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ประเมินผล (Evaluate) การเปลี่ยนสถานะ
|
||||
* @param compiled ข้อมูล Workflow ที่ Compile แล้ว
|
||||
* @param currentState สถานะปัจจุบัน
|
||||
* @param action การกระทำ
|
||||
* @param context ข้อมูลประกอบ (User roles, etc.)
|
||||
*/
|
||||
evaluate(
|
||||
compiled: CompiledWorkflow,
|
||||
currentState: string,
|
||||
action: string,
|
||||
context: any,
|
||||
): { nextState: string; events: EventRule[] } {
|
||||
const stateConfig = compiled.states[currentState];
|
||||
|
||||
if (!stateConfig) {
|
||||
throw new BadRequestException(
|
||||
`Runtime Error: Current state "${currentState}" not found in definition.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (stateConfig.terminal) {
|
||||
throw new BadRequestException(
|
||||
`Runtime Error: Cannot transition from terminal state "${currentState}".`,
|
||||
);
|
||||
}
|
||||
|
||||
const transition = stateConfig.transitions?.[action];
|
||||
|
||||
if (!transition) {
|
||||
throw new BadRequestException(
|
||||
`Runtime Error: Action "${action}" is not allowed from state "${currentState}". Available actions: ${Object.keys(stateConfig.transitions || {}).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check Requirements (RBAC Logic inside Engine)
|
||||
if (transition.requirements && transition.requirements.length > 0) {
|
||||
this.checkRequirements(transition.requirements, context);
|
||||
}
|
||||
|
||||
return {
|
||||
nextState: transition.to,
|
||||
events: transition.events || [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ตรวจสอบเงื่อนไขสิทธิ์ (Requirements)
|
||||
*/
|
||||
private checkRequirements(requirements: RequirementRule[], context: any) {
|
||||
const userRoles = context.roles || [];
|
||||
const userId = context.userId;
|
||||
|
||||
const isAllowed = requirements.some((req) => {
|
||||
// กรณีเช็ค Role
|
||||
if (req.role) {
|
||||
return userRoles.includes(req.role);
|
||||
}
|
||||
// กรณีเช็ค Specific User
|
||||
if (req.user) {
|
||||
return userId === req.user;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!isAllowed) {
|
||||
throw new BadRequestException(
|
||||
'Access Denied: You do not meet the requirements for this action.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// File: src/modules/workflow-engine/workflow-engine.controller.ts
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
Get,
|
||||
Query,
|
||||
Patch,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common'; // เพิ่ม Patch, Param
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { WorkflowEngineService } from './workflow-engine.service';
|
||||
import { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dto';
|
||||
import { EvaluateWorkflowDto } from './dto/evaluate-workflow.dto';
|
||||
import { GetAvailableActionsDto } from './dto/get-available-actions.dto'; // [NEW]
|
||||
import { UpdateWorkflowDefinitionDto } from './dto/update-workflow-definition.dto'; // [NEW]
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
|
||||
@ApiTags('Workflow Engine (DSL)')
|
||||
@Controller('workflow-engine')
|
||||
@UseGuards(JwtAuthGuard) // Protect all endpoints
|
||||
export class WorkflowEngineController {
|
||||
constructor(private readonly workflowService: WorkflowEngineService) {}
|
||||
|
||||
@Post('definitions')
|
||||
@ApiOperation({ summary: 'Create or Update Workflow Definition (DSL)' })
|
||||
@ApiResponse({ status: 201, description: 'Workflow compiled and saved.' })
|
||||
async createDefinition(@Body() dto: CreateWorkflowDefinitionDto) {
|
||||
return this.workflowService.createDefinition(dto);
|
||||
}
|
||||
|
||||
@Post('evaluate')
|
||||
@ApiOperation({
|
||||
summary: 'Evaluate transition (Run logic without saving state)',
|
||||
})
|
||||
async evaluate(@Body() dto: EvaluateWorkflowDto) {
|
||||
return this.workflowService.evaluate(dto);
|
||||
}
|
||||
|
||||
@Get('actions')
|
||||
@ApiOperation({ summary: 'Get available actions for current state' })
|
||||
async getAvailableActions(@Query() query: GetAvailableActionsDto) {
|
||||
// [UPDATED] ใช้ DTO แทนแยก Query
|
||||
return this.workflowService.getAvailableActions(
|
||||
query.workflow_code,
|
||||
query.current_state,
|
||||
);
|
||||
}
|
||||
|
||||
// [OPTIONAL/RECOMMENDED] เพิ่ม Endpoint สำหรับ Update (PATCH)
|
||||
@Patch('definitions/:id')
|
||||
@ApiOperation({
|
||||
summary: 'Update workflow status or details (e.g. Deactivate)',
|
||||
})
|
||||
async updateDefinition(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateWorkflowDefinitionDto, // [NEW] ใช้ Update DTO
|
||||
) {
|
||||
// *หมายเหตุ: คุณต้องไปเพิ่ม method update() ใน Service ด้วยถ้าจะใช้ Endpoint นี้
|
||||
// return this.workflowService.update(id, dto);
|
||||
return { message: 'Update logic not implemented yet', id, ...dto };
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,22 @@
|
||||
// File: src/modules/workflow-engine/workflow-engine.module.ts
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WorkflowEngineService } from './workflow-engine.service';
|
||||
import { WorkflowDslService } from './workflow-dsl.service'; // [New] ต้องสร้างไฟล์นี้ตามแผน Phase 6A
|
||||
import { WorkflowEngineController } from './workflow-engine.controller'; // [New] ต้องสร้างไฟล์นี้ตามแผน Phase 6A
|
||||
import { WorkflowDefinition } from './entities/workflow-definition.entity'; // [New] ต้องสร้างไฟล์นี้ตามแผน Phase 6A
|
||||
|
||||
@Module({
|
||||
providers: [WorkflowEngineService],
|
||||
// ✅ เพิ่มบรรทัดนี้ เพื่ออนุญาตให้ Module อื่น (เช่น Correspondence) เรียกใช้ Service นี้ได้
|
||||
exports: [WorkflowEngineService],
|
||||
imports: [
|
||||
// เชื่อมต่อกับตาราง workflow_definitions
|
||||
TypeOrmModule.forFeature([WorkflowDefinition]),
|
||||
],
|
||||
controllers: [WorkflowEngineController], // เพิ่ม Controller สำหรับรับ API
|
||||
providers: [
|
||||
WorkflowEngineService, // Service หลัก
|
||||
WorkflowDslService, // [New] Service สำหรับ Compile/Validate DSL
|
||||
],
|
||||
exports: [WorkflowEngineService], // Export ให้ module อื่นใช้เหมือนเดิม
|
||||
})
|
||||
export class WorkflowEngineModule {}
|
||||
|
||||
@@ -1,45 +1,179 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
// File: src/modules/workflow-engine/workflow-engine.service.ts
|
||||
|
||||
import {
|
||||
WorkflowStep,
|
||||
WorkflowAction,
|
||||
StepStatus,
|
||||
TransitionResult,
|
||||
} from './interfaces/workflow.interface.js';
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { WorkflowDefinition } from './entities/workflow-definition.entity';
|
||||
import { WorkflowDslService, CompiledWorkflow } from './workflow-dsl.service';
|
||||
import { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dto';
|
||||
import { EvaluateWorkflowDto } from './dto/evaluate-workflow.dto';
|
||||
import { UpdateWorkflowDefinitionDto } from './dto/update-workflow-definition.dto';
|
||||
|
||||
// Interface สำหรับ Backward Compatibility (Logic เดิม)
|
||||
export enum WorkflowAction {
|
||||
APPROVE = 'APPROVE',
|
||||
REJECT = 'REJECT',
|
||||
RETURN = 'RETURN',
|
||||
ACKNOWLEDGE = 'ACKNOWLEDGE',
|
||||
}
|
||||
|
||||
export interface TransitionResult {
|
||||
nextStepSequence: number | null;
|
||||
shouldUpdateStatus: boolean;
|
||||
documentStatus?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowEngineService {
|
||||
private readonly logger = new Logger(WorkflowEngineService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkflowDefinition)
|
||||
private readonly workflowDefRepo: Repository<WorkflowDefinition>,
|
||||
private readonly dslService: WorkflowDslService,
|
||||
) {}
|
||||
|
||||
// =================================================================
|
||||
// [NEW] DSL & Workflow Engine (Phase 6A)
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* คำนวณสถานะถัดไป (Next State Transition)
|
||||
* @param currentSequence ลำดับปัจจุบัน
|
||||
* @param totalSteps จำนวนขั้นตอนทั้งหมด
|
||||
* @param action การกระทำ (Approve/Reject/Return)
|
||||
* @param returnToSequence (Optional) ถ้า Return จะให้กลับไปขั้นไหน
|
||||
* สร้างหรืออัปเดต Workflow Definition ใหม่ (Auto Versioning)
|
||||
*/
|
||||
async createDefinition(
|
||||
dto: CreateWorkflowDefinitionDto,
|
||||
): Promise<WorkflowDefinition> {
|
||||
const compiled = this.dslService.compile(dto.dsl);
|
||||
|
||||
const latest = await this.workflowDefRepo.findOne({
|
||||
where: { workflow_code: dto.workflow_code },
|
||||
order: { version: 'DESC' },
|
||||
});
|
||||
|
||||
const nextVersion = latest ? latest.version + 1 : 1;
|
||||
|
||||
const entity = this.workflowDefRepo.create({
|
||||
workflow_code: dto.workflow_code,
|
||||
version: nextVersion,
|
||||
dsl: dto.dsl,
|
||||
compiled: compiled,
|
||||
is_active: dto.is_active ?? true,
|
||||
});
|
||||
|
||||
return this.workflowDefRepo.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
dto: UpdateWorkflowDefinitionDto,
|
||||
): Promise<WorkflowDefinition> {
|
||||
const definition = await this.workflowDefRepo.findOne({ where: { id } });
|
||||
if (!definition) {
|
||||
throw new NotFoundException(
|
||||
`Workflow Definition with ID "${id}" not found`,
|
||||
);
|
||||
}
|
||||
|
||||
if (dto.dsl) {
|
||||
try {
|
||||
const compiled = this.dslService.compile(dto.dsl);
|
||||
definition.dsl = dto.dsl;
|
||||
definition.compiled = compiled;
|
||||
} catch (error: any) {
|
||||
throw new BadRequestException(`Invalid DSL: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.is_active !== undefined) definition.is_active = dto.is_active;
|
||||
if (dto.workflow_code) definition.workflow_code = dto.workflow_code;
|
||||
|
||||
return this.workflowDefRepo.save(definition);
|
||||
}
|
||||
|
||||
async evaluate(dto: EvaluateWorkflowDto): Promise<any> {
|
||||
const definition = await this.workflowDefRepo.findOne({
|
||||
where: { workflow_code: dto.workflow_code, is_active: true },
|
||||
order: { version: 'DESC' },
|
||||
});
|
||||
|
||||
if (!definition) {
|
||||
throw new NotFoundException(
|
||||
`No active workflow definition found for "${dto.workflow_code}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const compiled: CompiledWorkflow = definition.compiled;
|
||||
const result = this.dslService.evaluate(
|
||||
compiled,
|
||||
dto.current_state,
|
||||
dto.action,
|
||||
dto.context || {},
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Workflow Evaluated: ${dto.workflow_code} [${dto.current_state}] --${dto.action}--> [${result.nextState}]`,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getAvailableActions(
|
||||
workflowCode: string,
|
||||
currentState: string,
|
||||
): Promise<string[]> {
|
||||
const definition = await this.workflowDefRepo.findOne({
|
||||
where: { workflow_code: workflowCode, is_active: true },
|
||||
order: { version: 'DESC' },
|
||||
});
|
||||
|
||||
if (!definition) return [];
|
||||
|
||||
const stateConfig = definition.compiled.states[currentState];
|
||||
if (!stateConfig || !stateConfig.transitions) return [];
|
||||
|
||||
return Object.keys(stateConfig.transitions);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// [LEGACY] Backward Compatibility for Correspondence/RFA Modules
|
||||
// คืนค่า Logic เดิมเพื่อไม่ให้ Module อื่น Error (TS2339)
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* คำนวณสถานะถัดไปแบบ Linear Sequence (Logic เดิม)
|
||||
* ใช้สำหรับ CorrespondenceService และ RfaService ที่ยังไม่ได้ Refactor
|
||||
*/
|
||||
processAction(
|
||||
currentSequence: number,
|
||||
totalSteps: number,
|
||||
action: WorkflowAction,
|
||||
action: string, // รับเป็น string เพื่อความยืดหยุ่น
|
||||
returnToSequence?: number,
|
||||
): TransitionResult {
|
||||
// Map string action to enum logic
|
||||
switch (action) {
|
||||
case WorkflowAction.APPROVE:
|
||||
case WorkflowAction.ACKNOWLEDGE:
|
||||
// ถ้าเป็นขั้นตอนสุดท้าย -> จบ Workflow
|
||||
case 'APPROVE': // Case sensitive handling fallback
|
||||
case 'ACKNOWLEDGE':
|
||||
if (currentSequence >= totalSteps) {
|
||||
return {
|
||||
nextStepSequence: null, // ไม่มีขั้นต่อไปแล้ว
|
||||
nextStepSequence: null,
|
||||
shouldUpdateStatus: true,
|
||||
documentStatus: 'COMPLETED', // หรือ APPROVED
|
||||
documentStatus: 'COMPLETED',
|
||||
};
|
||||
}
|
||||
// ถ้ายังไม่จบ -> ไปขั้นต่อไป
|
||||
return {
|
||||
nextStepSequence: currentSequence + 1,
|
||||
shouldUpdateStatus: false,
|
||||
};
|
||||
|
||||
case WorkflowAction.REJECT:
|
||||
// จบ Workflow ทันทีแบบไม่สวย
|
||||
case 'REJECT':
|
||||
return {
|
||||
nextStepSequence: null,
|
||||
shouldUpdateStatus: true,
|
||||
@@ -47,7 +181,7 @@ export class WorkflowEngineService {
|
||||
};
|
||||
|
||||
case WorkflowAction.RETURN:
|
||||
// ย้อนกลับไปขั้นตอนก่อนหน้า (หรือที่ระบุ)
|
||||
case 'RETURN':
|
||||
const targetStep = returnToSequence || currentSequence - 1;
|
||||
if (targetStep < 1) {
|
||||
throw new BadRequestException('Cannot return beyond the first step');
|
||||
@@ -55,38 +189,25 @@ export class WorkflowEngineService {
|
||||
return {
|
||||
nextStepSequence: targetStep,
|
||||
shouldUpdateStatus: true,
|
||||
documentStatus: 'REVISE_REQUIRED', // สถานะเอกสารเป็น "รอแก้ไข"
|
||||
documentStatus: 'REVISE_REQUIRED',
|
||||
};
|
||||
|
||||
default:
|
||||
throw new BadRequestException(`Invalid action: ${action}`);
|
||||
// กรณีส่ง Action อื่นมา ให้ถือว่าเป็น Approve (หรือจะ Throw Error ก็ได้)
|
||||
this.logger.warn(
|
||||
`Unknown legacy action: ${action}, treating as next step.`,
|
||||
);
|
||||
if (currentSequence >= totalSteps) {
|
||||
return {
|
||||
nextStepSequence: null,
|
||||
shouldUpdateStatus: true,
|
||||
documentStatus: 'COMPLETED',
|
||||
};
|
||||
}
|
||||
return {
|
||||
nextStepSequence: currentSequence + 1,
|
||||
shouldUpdateStatus: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ตรวจสอบว่า User คนนี้ มีสิทธิ์กด Action ในขั้นตอนนี้ไหม
|
||||
* (Logic เบื้องต้น - เดี๋ยวเราจะเชื่อมกับ RBAC จริงๆ ใน Service หลัก)
|
||||
*/
|
||||
validateAccess(
|
||||
step: WorkflowStep,
|
||||
userOrgId: number,
|
||||
userId: number,
|
||||
): boolean {
|
||||
// ถ้าขั้นตอนนี้ยังไม่ Active (เช่น PENDING หรือ SKIPPED) -> ห้ามยุ่ง
|
||||
if (step.status !== StepStatus.IN_PROGRESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// เช็คว่าตรงกับ Organization ที่กำหนดไหม
|
||||
if (step.organizationId && step.organizationId !== userOrgId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// เช็คว่าตรงกับ User ที่กำหนดไหม (ถ้าระบุ)
|
||||
if (step.assigneeId && step.assigneeId !== userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user