# Task: Workflow Engine Module **Status:** Not Started **Priority:** P0 (Critical - Core Infrastructure) **Estimated Effort:** 10-14 days **Dependencies:** TASK-BE-001 (Database), TASK-BE-002 (Auth) **Owner:** Backend Team --- ## 📋 Overview สร้าง Unified Workflow Engine ที่ใช้ DSL-based configuration สำหรับจัดการ Workflow ของ Correspondences, RFAs, และ Circulations --- ## Objectives - ✅ DSL Parser และ Validator - ✅ State Machine Management - ✅ Workflow Instance Lifecycle - ✅ Transition Execution - ✅ History Tracking - ✅ Notification Integration --- ## 📝 Acceptance Criteria 1. **Definition Management:** - ✅ Create/Update workflow from JSON DSL - ✅ Validate DSL syntax และ Logic - ✅ Version management - ✅ Activate/Deactivate definitions 2. **Instance Management:** - ✅ Create instance from definition - ✅ Execute transitions - ✅ Check guards (permissions, validations) - ✅ Trigger effects (notifications, updates) - ✅ Track history 3. **Integration:** - ✅ Used by Correspondence module - ✅ Used by RFA module - ✅ Used by Circulation module - ✅ Notification service integration --- ## 🛠️ Implementation Steps ### 1. Entities ```typescript // File: backend/src/modules/workflow-engine/entities/workflow-definition.entity.ts @Entity('workflow_definitions') export class WorkflowDefinition { @PrimaryGeneratedColumn() id: number; @Column({ length: 100 }) name: string; @Column() version: number; @Column({ length: 50 }) entity_type: string; // 'correspondence', 'rfa', 'circulation' @Column({ type: 'json' }) definition: WorkflowDSL; // JSON DSL @Column({ default: true }) is_active: boolean; @CreateDateColumn() created_at: Date; @Index(['name', 'version'], { unique: true }) _nameVersionIndex: void; } ``` ```typescript // File: backend/src/modules/workflow-engine/entities/workflow-instance.entity.ts @Entity('workflow_instances') export class WorkflowInstance { @PrimaryGeneratedColumn() id: number; @Column() definition_id: number; @Column({ length: 50 }) entity_type: string; @Column() entity_id: number; @Column({ length: 50 }) current_state: string; @Column({ type: 'json', nullable: true }) context: any; // Runtime data @CreateDateColumn() started_at: Date; @Column({ type: 'timestamp', nullable: true }) completed_at: Date; @ManyToOne(() => WorkflowDefinition) @JoinColumn({ name: 'definition_id' }) definition: WorkflowDefinition; @OneToMany(() => WorkflowHistory, (history) => history.instance) history: WorkflowHistory[]; } ``` ```typescript // File: backend/src/modules/workflow-engine/entities/workflow-history.entity.ts @Entity('workflow_history') export class WorkflowHistory { @PrimaryGeneratedColumn() id: number; @Column() instance_id: number; @Column({ length: 50, nullable: true }) from_state: string; @Column({ length: 50 }) to_state: string; @Column({ length: 50 }) action: string; @Column() actor_id: number; @Column({ type: 'json', nullable: true }) metadata: any; @CreateDateColumn() transitioned_at: Date; @ManyToOne(() => WorkflowInstance, (instance) => instance.history) @JoinColumn({ name: 'instance_id' }) instance: WorkflowInstance; @ManyToOne(() => User) @JoinColumn({ name: 'actor_id' }) actor: User; } ``` ### 2. DSL Types ```typescript // File: backend/src/modules/workflow-engine/types/workflow-dsl.type.ts export interface WorkflowDSL { name: string; version: number; entity_type: string; states: WorkflowState[]; transitions: WorkflowTransition[]; } export interface WorkflowState { name: string; type: 'initial' | 'intermediate' | 'final'; allowed_transitions: string[]; } export interface WorkflowTransition { action: string; from: string; to: string; guards?: Guard[]; effects?: Effect[]; } export interface Guard { type: 'permission' | 'validation' | 'condition'; permission?: string; rules?: string[]; condition?: string; } export interface Effect { type: 'notification' | 'update_entity' | 'create_log'; template?: string; recipients?: string[]; field?: string; value?: any; } ``` ### 3. DSL Parser ```typescript // File: backend/src/modules/workflow-engine/services/dsl-parser.service.ts @Injectable() export class DslParserService { parseDefinition(dsl: WorkflowDSL): ParsedWorkflow { this.validateStructure(dsl); this.validateStates(dsl); this.validateTransitions(dsl); return { states: this.parseStates(dsl.states), transitions: this.parseTransitions(dsl.transitions), stateMap: this.buildStateMap(dsl.states), }; } private validateStructure(dsl: WorkflowDSL): void { if (!dsl.name || !dsl.states || !dsl.transitions) { throw new BadRequestException('Invalid DSL structure'); } } private validateStates(dsl: WorkflowDSL): void { const initialStates = dsl.states.filter((s) => s.type === 'initial'); if (initialStates.length !== 1) { throw new BadRequestException('Must have exactly one initial state'); } const finalStates = dsl.states.filter((s) => s.type === 'final'); if (finalStates.length === 0) { throw new BadRequestException('Must have at least one final state'); } } private validateTransitions(dsl: WorkflowDSL): void { const stateNames = new Set(dsl.states.map((s) => s.name)); for (const transition of dsl.transitions) { if (!stateNames.has(transition.from)) { throw new BadRequestException(`Unknown state: ${transition.from}`); } if (!stateNames.has(transition.to)) { throw new BadRequestException(`Unknown state: ${transition.to}`); } } } getInitialState(dsl: WorkflowDSL): string { const initialState = dsl.states.find((s) => s.type === 'initial'); return initialState.name; } buildStateMap(states: WorkflowState[]): Map { return new Map(states.map((s) => [s.name, s])); } } ``` ### 4. Workflow Engine Service ```typescript // File: backend/src/modules/workflow-engine/services/workflow-engine.service.ts @Injectable() export class WorkflowEngineService { constructor( @InjectRepository(WorkflowDefinition) private defRepo: Repository, @InjectRepository(WorkflowInstance) private instanceRepo: Repository, @InjectRepository(WorkflowHistory) private historyRepo: Repository, private dslParser: DslParserService, private guardExecutor: GuardExecutorService, private effectExecutor: EffectExecutorService, private dataSource: DataSource ) {} async createInstance( definitionName: string, entityType: string, entityId: number, manager?: EntityManager ): Promise { const repo = manager || this.instanceRepo; //Get active definition const definition = await this.defRepo.findOne({ where: { name: definitionName, entity_type: entityType, is_active: true }, order: { version: 'DESC' }, }); if (!definition) { throw new NotFoundException( `Workflow definition not found: ${definitionName}` ); } // Get initial state const initialState = this.dslParser.getInitialState(definition.definition); // Create instance const instance = repo.create({ definition_id: definition.id, entity_type: entityType, entity_id: entityId, current_state: initialState, context: {}, }); return repo.save(instance); } async executeTransition( instanceId: number, action: string, actorId: number ): Promise { return this.dataSource.transaction(async (manager) => { // 1. Get instance const instance = await manager.findOne(WorkflowInstance, { where: { id: instanceId }, relations: ['definition'], }); if (!instance) { throw new NotFoundException( `Workflow instance not found: ${instanceId}` ); } // 2. Find transition const dsl = instance.definition.definition; const transition = dsl.transitions.find( (t) => t.action === action && t.from === instance.current_state ); if (!transition) { throw new BadRequestException( `Invalid transition: ${action} from ${instance.current_state}` ); } // 3. Execute guards await this.guardExecutor.checkGuards(transition.guards, { actorId, instance, }); // 4. Update state const fromState = instance.current_state; instance.current_state = transition.to; // Check if reached final state const toStateConfig = dsl.states.find((s) => s.name === transition.to); if (toStateConfig.type === 'final') { instance.completed_at = new Date(); } await manager.save(instance); // 5. Record history await manager.save(WorkflowHistory, { instance_id: instanceId, from_state: fromState, to_state: transition.to, action, actor_id: actorId, metadata: {}, }); // 6. Execute effects await this.effectExecutor.executeEffects(transition.effects, { instance, actorId, manager, }); }); } async getInstanceHistory(instanceId: number): Promise { return this.historyRepo.find({ where: { instance_id: instanceId }, relations: ['actor'], order: { transitioned_at: 'ASC' }, }); } async getCurrentState(entityType: string, entityId: number): Promise { const instance = await this.instanceRepo.findOne({ where: { entity_type: entityType, entity_id: entityId }, order: { started_at: 'DESC' }, }); return instance?.current_state || null; } } ``` ### 5. Guard Executor ```typescript // File: backend/src/modules/workflow-engine/services/guard-executor.service.ts @Injectable() export class GuardExecutorService { constructor(private abilityFactory: AbilityFactory) {} async checkGuards(guards: Guard[], context: any): Promise { if (!guards || guards.length === 0) { return; } for (const guard of guards) { await this.checkGuard(guard, context); } } private async checkGuard(guard: Guard, context: any): Promise { switch (guard.type) { case 'permission': await this.checkPermission(guard.permission, context); break; case 'validation': await this.checkValidation(guard.rules, context); break; case 'condition': await this.checkCondition(guard.condition, context); break; default: throw new BadRequestException(`Unknown guard type: ${guard.type}`); } } private async checkPermission( permission: string, context: any ): Promise { const ability = await this.abilityFactory.createForUser({ user_id: context.actorId, }); const [action, subject] = permission.split('.'); if (!ability.can(action, subject)) { throw new ForbiddenException(`Permission denied: ${permission}`); } } private async checkValidation(rules: string[], context: any): Promise { // Implement validation rules // e.g., "hasAttachment", "hasRecipient" } private async checkCondition(condition: string, context: any): Promise { // Evaluate condition expression // e.g., "entity.status === 'draft'" } } ``` --- ## ✅ Testing & Verification ### 1. Unit Tests ```typescript describe('WorkflowEngineService', () => { it('should create instance with initial state', async () => { const instance = await service.createInstance( 'CORRESPONDENCE_ROUTING', 'correspondence', 1 ); expect(instance.current_state).toBe('DRAFT'); }); it('should execute valid transition', async () => { await service.executeTransition(instance.id, 'SUBMIT', userId); const updated = await instanceRepo.findOne(instance.id); expect(updated.current_state).toBe('SUBMITTED'); }); it('should reject invalid transition', async () => { await expect( service.executeTransition(instance.id, 'INVALID_ACTION', userId) ).rejects.toThrow('Invalid transition'); }); }); ``` --- ## 📚 Related Documents - [ADR-001: Unified Workflow Engine](../05-decisions/ADR-001-unified-workflow-engine.md) - [Unified Workflow Requirements](../01-requirements/03.6-unified-workflow.md) --- ## 📦 Deliverables - [ ] Workflow Entities (Definition, Instance, History) - [ ] DSL Parser และ Validator - [ ] WorkflowEngineService - [ ] Guard Executor - [ ] Effect Executor - [ ] Example Workflow Definitions - [ ] Unit Tests (90% coverage) - [ ] Integration Tests - [ ] Documentation --- ## 🚨 Risks & Mitigation | Risk | Impact | Mitigation | | ------------------ | -------- | --------------------------------------- | | DSL parsing errors | High | Comprehensive validation | | Guard failures | Medium | Clear error messages | | State corruption | Critical | Transaction-safe updates | | Performance issues | Medium | Optimize DSL parsing, cache definitions | --- ## 📌 Notes - DSL structure validated on save - Workflow definitions versioned - Guard checks before state changes - History tracked for audit trail - Effects executed after state update