@rytass/bpm-core-nestjs-module 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +43 -0
  3. package/package.json +3 -2
  4. package/src/lib/migrations/0000000017000-adhoc-directives.d.ts +6 -0
  5. package/src/lib/migrations/0000000017000-adhoc-directives.js +53 -0
  6. package/src/lib/migrations/0000000017000-adhoc-directives.js.map +1 -0
  7. package/src/lib/migrations/index.d.ts +1 -0
  8. package/src/lib/migrations/index.js +3 -0
  9. package/src/lib/migrations/index.js.map +1 -1
  10. package/src/lib/notification/notification.service.d.ts +15 -1
  11. package/src/lib/notification/notification.service.js +30 -0
  12. package/src/lib/notification/notification.service.js.map +1 -1
  13. package/src/lib/workflow-engine/adhoc-directive.entity.d.ts +31 -0
  14. package/src/lib/workflow-engine/adhoc-directive.entity.js +105 -0
  15. package/src/lib/workflow-engine/adhoc-directive.entity.js.map +1 -0
  16. package/src/lib/workflow-engine/adhoc.enums.d.ts +21 -0
  17. package/src/lib/workflow-engine/adhoc.enums.js +42 -0
  18. package/src/lib/workflow-engine/adhoc.enums.js.map +1 -0
  19. package/src/lib/workflow-engine/dto/adhoc-notification.input.d.ts +6 -0
  20. package/src/lib/workflow-engine/dto/adhoc-notification.input.js +28 -0
  21. package/src/lib/workflow-engine/dto/adhoc-notification.input.js.map +1 -0
  22. package/src/lib/workflow-engine/dto/adhoc-target.input.d.ts +16 -0
  23. package/src/lib/workflow-engine/dto/adhoc-target.input.js +61 -0
  24. package/src/lib/workflow-engine/dto/adhoc-target.input.js.map +1 -0
  25. package/src/lib/workflow-engine/index.d.ts +4 -0
  26. package/src/lib/workflow-engine/index.js +4 -0
  27. package/src/lib/workflow-engine/index.js.map +1 -1
  28. package/src/lib/workflow-engine/task.entity.d.ts +5 -0
  29. package/src/lib/workflow-engine/task.entity.js +21 -0
  30. package/src/lib/workflow-engine/task.entity.js.map +1 -1
  31. package/src/lib/workflow-engine/workflow-engine.enums.d.ts +3 -0
  32. package/src/lib/workflow-engine/workflow-engine.enums.js +3 -0
  33. package/src/lib/workflow-engine/workflow-engine.enums.js.map +1 -1
  34. package/src/lib/workflow-engine/workflow-engine.module.js +2 -0
  35. package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -1
  36. package/src/lib/workflow-engine/workflow-engine.mutations.d.ts +10 -0
  37. package/src/lib/workflow-engine/workflow-engine.mutations.js +112 -0
  38. package/src/lib/workflow-engine/workflow-engine.mutations.js.map +1 -1
  39. package/src/lib/workflow-engine/workflow-engine.queries.d.ts +2 -0
  40. package/src/lib/workflow-engine/workflow-engine.queries.js +12 -0
  41. package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
  42. package/src/lib/workflow-engine/workflow-engine.service.d.ts +44 -1
  43. package/src/lib/workflow-engine/workflow-engine.service.js +780 -41
  44. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
@@ -22,6 +22,8 @@ const approval_template_version_entity_1 = require("../template/approval-templat
22
22
  const approval_template_entity_1 = require("../template/approval-template.entity");
23
23
  const template_enums_1 = require("../template/template.enums");
24
24
  const activity_log_entity_1 = require("./activity-log.entity");
25
+ const adhoc_directive_entity_1 = require("./adhoc-directive.entity");
26
+ const adhoc_enums_1 = require("./adhoc.enums");
25
27
  const approval_instance_entity_1 = require("./approval-instance.entity");
26
28
  const task_decision_entity_1 = require("./task-decision.entity");
27
29
  const task_candidate_entity_1 = require("./task-candidate.entity");
@@ -41,7 +43,7 @@ const WORKFLOW_READ_ALL_PERMISSIONS = new Set([
41
43
  'workflow.read_all',
42
44
  ]);
43
45
  let WorkflowEngineService = class WorkflowEngineService {
44
- constructor(approvalInstanceRepository, workflowTokenRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, notificationRepository, activityLogRepository, approvalTemplateRepository, approvalTemplateVersionRepository, formDefinitionVersionRepository, attachmentService, conditionService, delegationService, notificationService, signatureService, serviceTaskDispatcher = new workflow_service_task_dispatcher_token_1.DefaultWorkflowServiceTaskDispatcher()) {
46
+ constructor(approvalInstanceRepository, workflowTokenRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, notificationRepository, activityLogRepository, adhocDirectiveRepository, approvalTemplateRepository, approvalTemplateVersionRepository, formDefinitionVersionRepository, attachmentService, conditionService, delegationService, notificationService, signatureService, serviceTaskDispatcher = new workflow_service_task_dispatcher_token_1.DefaultWorkflowServiceTaskDispatcher()) {
45
47
  this.approvalInstanceRepository = approvalInstanceRepository;
46
48
  this.workflowTokenRepository = workflowTokenRepository;
47
49
  this.taskRepository = taskRepository;
@@ -49,6 +51,7 @@ let WorkflowEngineService = class WorkflowEngineService {
49
51
  this.taskDecisionRepository = taskDecisionRepository;
50
52
  this.notificationRepository = notificationRepository;
51
53
  this.activityLogRepository = activityLogRepository;
54
+ this.adhocDirectiveRepository = adhocDirectiveRepository;
52
55
  this.approvalTemplateRepository = approvalTemplateRepository;
53
56
  this.approvalTemplateVersionRepository = approvalTemplateVersionRepository;
54
57
  this.formDefinitionVersionRepository = formDefinitionVersionRepository;
@@ -187,19 +190,7 @@ let WorkflowEngineService = class WorkflowEngineService {
187
190
  task.status !== workflow_engine_enums_1.TaskStatusEnum.IN_PROGRESS) {
188
191
  throw new common_1.ConflictException(`Task ${task.id} is not pending`);
189
192
  }
190
- const taskCandidates = await taskCandidateRepository.find({
191
- where: { taskId: task.id },
192
- });
193
- const actorCandidate = taskCandidates.find((candidate) => candidate.memberId === input.decidedByMemberId);
194
- const isDirectAssignee = task.assigneeMemberId === input.decidedByMemberId;
195
- if (!isDirectAssignee && !actorCandidate) {
196
- throw new common_1.ConflictException(`Task ${task.id} is assigned to another member`);
197
- }
198
- if (actorCandidate &&
199
- actorCandidate.status !== workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING &&
200
- actorCandidate.status !== workflow_engine_enums_1.TaskCandidateStatusEnum.CLAIMED) {
201
- throw new common_1.ConflictException(`Task ${task.id} was already decided by this member`);
202
- }
193
+ const { actorCandidate, taskCandidates } = await this.readTaskActorContext(manager, task, input.decidedByMemberId);
203
194
  if (input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED &&
204
195
  input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.REJECTED &&
205
196
  input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.RETURNED &&
@@ -332,6 +323,13 @@ let WorkflowEngineService = class WorkflowEngineService {
332
323
  });
333
324
  if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED &&
334
325
  shouldCompleteTask) {
326
+ // Ad-hoc gate: the token may only advance once every task bound to
327
+ // this token+node (the original task plus any ad-hoc countersign /
328
+ // pre-approval tasks) has reached a terminal state. Nodes without
329
+ // ad-hoc tasks keep the original single-task behaviour.
330
+ if (await this.hasOpenTasksForTokenNode(manager, claimedTask.tokenId, claimedTask.nodeId)) {
331
+ return decision;
332
+ }
335
333
  const token = await tokenRepository.findOne({
336
334
  where: { id: claimedTask.tokenId },
337
335
  });
@@ -346,6 +344,7 @@ let WorkflowEngineService = class WorkflowEngineService {
346
344
  consumedAt: null,
347
345
  status: workflow_engine_enums_1.WorkflowTokenStatusEnum.ACTIVE,
348
346
  });
347
+ await this.dispatchAdhocStageNotifications(manager, instance, claimedTask.nodeId, 'APPROVED');
349
348
  await this.advanceTokenToOutgoingNodes(manager, instance, activeToken, readWorkflowNodeOrThrow(instance.workflowSnapshot, claimedTask.nodeId));
350
349
  await this.processRunningInstance(manager, instance);
351
350
  return decision;
@@ -357,6 +356,9 @@ let WorkflowEngineService = class WorkflowEngineService {
357
356
  await this.returnInstanceToNode(manager, instance, completedTask, returnToNodeId, decidedAt);
358
357
  return decision;
359
358
  }
359
+ if (await this.handleAdhocPreApprovalRejection(manager, instance, completedTask, decidedAt, decisionComment)) {
360
+ return decision;
361
+ }
360
362
  await this.rejectInstance(manager, instance, completedTask, decidedAt);
361
363
  return decision;
362
364
  });
@@ -391,6 +393,8 @@ let WorkflowEngineService = class WorkflowEngineService {
391
393
  completedAt: cancelledAt,
392
394
  state: workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED,
393
395
  });
396
+ await this.dispatchAdhocCompletionNotifications(manager, cancelledInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED);
397
+ await this.cancelPendingAdhocDirectives(manager, instance.id, null);
394
398
  await manager.getRepository(activity_log_entity_1.ActivityLogEntity).save(manager.getRepository(activity_log_entity_1.ActivityLogEntity).create({
395
399
  actorMemberId: input.cancelledByMemberId,
396
400
  eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.INSTANCE_CANCELLED,
@@ -453,6 +457,567 @@ let WorkflowEngineService = class WorkflowEngineService {
453
457
  return runtimeInstance;
454
458
  });
455
459
  }
460
+ async requestAdhocCountersign({ comment, requestedByMemberId, target, taskId, }) {
461
+ return this.approvalInstanceRepository.manager.transaction(async (manager) => {
462
+ const { instance, node, task } = await this.loadAdhocOperationContext(manager, taskId, requestedByMemberId, { requireAllowAddSigner: true });
463
+ const targetValue = buildAdhocTargetValue(target);
464
+ // Rejects WEBHOOK targets — countersigners must be members.
465
+ buildAdhocApproverResolver(targetValue);
466
+ const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
467
+ const directive = await directiveRepository.save(directiveRepository.create({
468
+ channels: null,
469
+ comment: comment?.trim() || null,
470
+ consumedAt: null,
471
+ createdByMemberId: requestedByMemberId,
472
+ instanceId: instance.id,
473
+ onReject: null,
474
+ originNodeId: task.nodeId,
475
+ originTaskId: task.id,
476
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.PENDING,
477
+ targetKind: targetValue.kind,
478
+ targetValue: { ...targetValue },
479
+ type: adhoc_enums_1.AdhocDirectiveTypeEnum.COUNTERSIGN,
480
+ }));
481
+ await this.recordAdhocDirectiveActivity(manager, directive, workflow_engine_enums_1.ActivityLogEventTypeEnum.ADHOC_DIRECTIVE_CREATED, requestedByMemberId, { nodeLabel: node.data.label });
482
+ return directive;
483
+ });
484
+ }
485
+ async requestAdhocPreApproval({ comment, onReject, requestedByMemberId, target, taskId, }) {
486
+ return this.approvalInstanceRepository.manager.transaction(async (manager) => {
487
+ const { instance, node, task } = await this.loadAdhocOperationContext(manager, taskId, requestedByMemberId, { requireAllowAddSigner: true });
488
+ const targetValue = buildAdhocTargetValue(target);
489
+ // Rejects WEBHOOK targets — pre-approvers must be members.
490
+ buildAdhocApproverResolver(targetValue);
491
+ const now = new Date();
492
+ const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
493
+ const directive = await directiveRepository.save(directiveRepository.create({
494
+ channels: null,
495
+ comment: comment?.trim() || null,
496
+ consumedAt: now,
497
+ createdByMemberId: requestedByMemberId,
498
+ instanceId: instance.id,
499
+ onReject,
500
+ originNodeId: task.nodeId,
501
+ originTaskId: task.id,
502
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.CONSUMED,
503
+ targetKind: targetValue.kind,
504
+ targetValue: { ...targetValue },
505
+ type: adhoc_enums_1.AdhocDirectiveTypeEnum.PRE_APPROVAL,
506
+ }));
507
+ const adhocTask = await this.createAdhocTaskForDirective(manager, instance, task.tokenId, node, directive, now);
508
+ await this.recordAdhocDirectiveActivity(manager, directive, workflow_engine_enums_1.ActivityLogEventTypeEnum.ADHOC_DIRECTIVE_CREATED, requestedByMemberId, { adhocTaskId: adhocTask.id, nodeLabel: node.data.label, onReject });
509
+ return adhocTask;
510
+ });
511
+ }
512
+ async configureAdhocNotification({ channels, requestedByMemberId, target, taskId, type, }) {
513
+ return this.approvalInstanceRepository.manager.transaction(async (manager) => {
514
+ const { instance, node, task } = await this.loadAdhocOperationContext(manager, taskId, requestedByMemberId, { requireAllowAddSigner: false });
515
+ const targetValue = buildAdhocTargetValue(target);
516
+ const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
517
+ const directive = await directiveRepository.save(directiveRepository.create({
518
+ channels: channels?.length ? [...channels] : null,
519
+ comment: null,
520
+ consumedAt: null,
521
+ createdByMemberId: requestedByMemberId,
522
+ instanceId: instance.id,
523
+ onReject: null,
524
+ originNodeId: task.nodeId,
525
+ originTaskId: task.id,
526
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.PENDING,
527
+ targetKind: targetValue.kind,
528
+ targetValue: { ...targetValue },
529
+ type,
530
+ }));
531
+ await this.recordAdhocDirectiveActivity(manager, directive, workflow_engine_enums_1.ActivityLogEventTypeEnum.ADHOC_DIRECTIVE_CREATED, requestedByMemberId, { nodeLabel: node.data.label });
532
+ return directive;
533
+ });
534
+ }
535
+ async cancelAdhocDirective({ cancelledByMemberId, directiveId, }) {
536
+ return this.approvalInstanceRepository.manager.transaction(async (manager) => {
537
+ const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
538
+ const directive = await directiveRepository.findOne({
539
+ where: { id: directiveId },
540
+ });
541
+ if (!directive) {
542
+ throw new common_1.NotFoundException(`Ad-hoc directive ${directiveId} was not found`);
543
+ }
544
+ await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
545
+ directive.instanceId,
546
+ ]);
547
+ if (directive.createdByMemberId !== cancelledByMemberId) {
548
+ throw new common_1.ConflictException(`Ad-hoc directive ${directive.id} can only be cancelled by its creator`);
549
+ }
550
+ if (directive.status !== adhoc_enums_1.AdhocDirectiveStatusEnum.PENDING) {
551
+ throw new common_1.ConflictException(`Ad-hoc directive ${directive.id} is not pending`);
552
+ }
553
+ const cancelledDirective = await directiveRepository.save({
554
+ ...directive,
555
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.CANCELLED,
556
+ });
557
+ await this.recordAdhocDirectiveActivity(manager, cancelledDirective, workflow_engine_enums_1.ActivityLogEventTypeEnum.ADHOC_DIRECTIVE_CANCELLED, cancelledByMemberId);
558
+ return cancelledDirective;
559
+ });
560
+ }
561
+ async listAdhocDirectives(instanceId, scope) {
562
+ await this.getApprovalInstance(instanceId, scope);
563
+ return this.adhocDirectiveRepository.find({
564
+ order: { createdAt: 'ASC' },
565
+ where: { instanceId },
566
+ });
567
+ }
568
+ async readTaskActorContext(manager, task, actingMemberId) {
569
+ const taskCandidates = await manager
570
+ .getRepository(task_candidate_entity_1.TaskCandidateEntity)
571
+ .find({ where: { taskId: task.id } });
572
+ const actorCandidate = taskCandidates.find((candidate) => candidate.memberId === actingMemberId) ?? null;
573
+ const isDirectAssignee = task.assigneeMemberId === actingMemberId;
574
+ if (!isDirectAssignee && !actorCandidate) {
575
+ throw new common_1.ConflictException(`Task ${task.id} is assigned to another member`);
576
+ }
577
+ if (actorCandidate &&
578
+ actorCandidate.status !== workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING &&
579
+ actorCandidate.status !== workflow_engine_enums_1.TaskCandidateStatusEnum.CLAIMED) {
580
+ throw new common_1.ConflictException(`Task ${task.id} was already decided by this member`);
581
+ }
582
+ return { actorCandidate, taskCandidates };
583
+ }
584
+ async loadAdhocOperationContext(manager, taskId, actingMemberId, options) {
585
+ const task = await manager
586
+ .getRepository(task_entity_1.TaskEntity)
587
+ .findOne({ where: { id: taskId } });
588
+ if (!task) {
589
+ throw new common_1.NotFoundException(`Task ${taskId} was not found`);
590
+ }
591
+ await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
592
+ task.instanceId,
593
+ ]);
594
+ if (task.status !== workflow_engine_enums_1.TaskStatusEnum.PENDING &&
595
+ task.status !== workflow_engine_enums_1.TaskStatusEnum.IN_PROGRESS) {
596
+ throw new common_1.ConflictException(`Task ${task.id} is not pending`);
597
+ }
598
+ await this.readTaskActorContext(manager, task, actingMemberId);
599
+ const instance = await manager
600
+ .getRepository(approval_instance_entity_1.ApprovalInstanceEntity)
601
+ .findOne({ where: { id: task.instanceId } });
602
+ if (!instance) {
603
+ throw new common_1.NotFoundException(`Approval instance ${task.instanceId} was not found`);
604
+ }
605
+ if (instance.state !== workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING) {
606
+ throw new common_1.ConflictException(`Approval instance ${instance.id} is not running`);
607
+ }
608
+ const node = readWorkflowNodeOrThrow(instance.workflowSnapshot, task.nodeId);
609
+ if (node.type !== 'userTask') {
610
+ throw new common_1.ConflictException(`Task ${task.id} is not bound to a user task node`);
611
+ }
612
+ if (options.requireAllowAddSigner && !node.data.allowAddSigner) {
613
+ throw new common_1.ForbiddenException(`簽核節點「${node.data.label}」does not allow ad-hoc signers`);
614
+ }
615
+ return { instance, node, task };
616
+ }
617
+ async recordAdhocDirectiveActivity(manager, directive, eventType, actorMemberId, extraPayload = {}) {
618
+ const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
619
+ await activityRepository.save(activityRepository.create({
620
+ actorMemberId,
621
+ eventType,
622
+ instanceId: directive.instanceId,
623
+ nodeId: directive.originNodeId,
624
+ payload: {
625
+ ...extraPayload,
626
+ directiveId: directive.id,
627
+ directiveStatus: directive.status,
628
+ directiveType: directive.type,
629
+ targetKind: directive.targetKind,
630
+ },
631
+ taskId: directive.originTaskId,
632
+ }));
633
+ }
634
+ async createAdhocTaskForDirective(manager, instance, tokenId, node, directive, now) {
635
+ const taskRepository = manager.getRepository(task_entity_1.TaskEntity);
636
+ const taskCandidateRepository = manager.getRepository(task_candidate_entity_1.TaskCandidateEntity);
637
+ const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
638
+ const label = directive.type === adhoc_enums_1.AdhocDirectiveTypeEnum.COUNTERSIGN
639
+ ? '臨時會簽'
640
+ : '臨時加簽';
641
+ const resolvedCandidates = await this.resolveApproverResolver(manager, instance, buildAdhocApproverResolver(readAdhocTargetValue(directive)), `${label}「${node.data.label}」`);
642
+ const candidates = await this.applyDelegationToResolvedCandidates(instance, node.id, resolvedCandidates);
643
+ if (candidates.length === 0) {
644
+ throw new common_1.ConflictException(`${label}「${node.data.label}」 did not resolve to any member id`);
645
+ }
646
+ const primaryCandidate = candidates[0];
647
+ const task = await taskRepository.save(taskRepository.create({
648
+ adhocDirectiveId: directive.id,
649
+ adhocOriginTaskId: directive.originTaskId,
650
+ adhocType: directive.type,
651
+ assigneeMemberId: candidates.length === 1 ? primaryCandidate.memberId : null,
652
+ assignmentType: candidates.length === 1
653
+ ? workflow_engine_enums_1.TaskAssignmentTypeEnum.DIRECT_MEMBER
654
+ : workflow_engine_enums_1.TaskAssignmentTypeEnum.CANDIDATE_GROUP,
655
+ completedAt: null,
656
+ createdAt: now,
657
+ decisionPolicySnapshot: { type: 'SINGLE' },
658
+ delegationChain: candidates.length === 1 ? primaryCandidate.delegationChain : [],
659
+ instanceId: instance.id,
660
+ isAdhoc: true,
661
+ nodeId: node.id,
662
+ openedAt: null,
663
+ originalAssigneeMemberId: candidates.length === 1 ? primaryCandidate.originalMemberId : null,
664
+ slaDueAt: null,
665
+ status: workflow_engine_enums_1.TaskStatusEnum.PENDING,
666
+ tokenId,
667
+ }));
668
+ const savedCandidates = await taskCandidateRepository.save(candidates.map((candidate) => taskCandidateRepository.create({
669
+ claimedAt: null,
670
+ createdAt: now,
671
+ decidedAt: null,
672
+ delegationChain: candidate.delegationChain,
673
+ memberId: candidate.memberId,
674
+ originalMemberId: candidate.originalMemberId,
675
+ sourceType: candidate.sourceType,
676
+ status: workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING,
677
+ taskId: task.id,
678
+ })));
679
+ task.candidateMemberIds = savedCandidates.map((candidate) => candidate.memberId);
680
+ await activityRepository.save(activityRepository.create({
681
+ actorMemberId: directive.createdByMemberId,
682
+ eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.TASK_CREATED,
683
+ instanceId: instance.id,
684
+ nodeId: node.id,
685
+ payload: {
686
+ adhocDirectiveId: directive.id,
687
+ adhocType: directive.type,
688
+ assigneeMemberId: task.assigneeMemberId,
689
+ assignmentType: task.assignmentType,
690
+ candidateMemberIds: savedCandidates.map((candidate) => candidate.memberId),
691
+ originTaskId: directive.originTaskId,
692
+ tokenId,
693
+ },
694
+ taskId: task.id,
695
+ }));
696
+ await savedCandidates.reduce(async (previous, candidate) => {
697
+ await previous;
698
+ await this.notificationService.createTaskAssignedNotification({
699
+ instance,
700
+ manager,
701
+ node,
702
+ task: Object.assign(new task_entity_1.TaskEntity(), task, {
703
+ assigneeMemberId: candidate.memberId,
704
+ delegationChain: candidate.delegationChain,
705
+ originalAssigneeMemberId: candidate.originalMemberId,
706
+ }),
707
+ });
708
+ }, Promise.resolve());
709
+ return task;
710
+ }
711
+ async spawnCountersignTasksForNode(manager, instance, token, node, now) {
712
+ const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
713
+ const directives = await directiveRepository.find({
714
+ order: { createdAt: 'ASC' },
715
+ where: {
716
+ instanceId: instance.id,
717
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.PENDING,
718
+ type: adhoc_enums_1.AdhocDirectiveTypeEnum.COUNTERSIGN,
719
+ },
720
+ });
721
+ if (directives.length === 0) {
722
+ return;
723
+ }
724
+ await directives.reduce(async (previous, directive) => {
725
+ await previous;
726
+ try {
727
+ await this.createAdhocTaskForDirective(manager, instance, token.id, node, directive, now);
728
+ await directiveRepository.save({
729
+ ...directive,
730
+ consumedAt: now,
731
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.CONSUMED,
732
+ });
733
+ }
734
+ catch (error) {
735
+ // A countersign target that no longer resolves must not block the
736
+ // main flow — cancel the directive and record the failure.
737
+ await directiveRepository.save({
738
+ ...directive,
739
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.CANCELLED,
740
+ });
741
+ await this.recordAdhocDirectiveActivity(manager, directive, workflow_engine_enums_1.ActivityLogEventTypeEnum.ADHOC_DIRECTIVE_CANCELLED, null, {
742
+ error: error instanceof Error
743
+ ? error.message
744
+ : 'Countersign target resolution failed',
745
+ });
746
+ }
747
+ }, Promise.resolve());
748
+ }
749
+ async hasOpenTasksForTokenNode(manager, tokenId, nodeId) {
750
+ const tasks = await manager.getRepository(task_entity_1.TaskEntity).find({
751
+ where: { nodeId, tokenId },
752
+ });
753
+ return tasks.some((task) => task.tokenId === tokenId &&
754
+ task.nodeId === nodeId &&
755
+ (task.status === workflow_engine_enums_1.TaskStatusEnum.PENDING ||
756
+ task.status === workflow_engine_enums_1.TaskStatusEnum.IN_PROGRESS));
757
+ }
758
+ async handleAdhocPreApprovalRejection(manager, instance, task, decidedAt, decisionComment) {
759
+ if (!task.isAdhoc ||
760
+ task.adhocType !== adhoc_enums_1.AdhocDirectiveTypeEnum.PRE_APPROVAL ||
761
+ !task.adhocDirectiveId) {
762
+ return false;
763
+ }
764
+ const directive = await manager
765
+ .getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity)
766
+ .findOne({ where: { id: task.adhocDirectiveId } });
767
+ if (directive?.onReject !==
768
+ adhoc_enums_1.AdhocPreApprovalRejectBehaviorEnum.RETURN_TO_ORIGIN) {
769
+ return false;
770
+ }
771
+ const taskRepository = manager.getRepository(task_entity_1.TaskEntity);
772
+ const taskCandidateRepository = manager.getRepository(task_candidate_entity_1.TaskCandidateEntity);
773
+ const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
774
+ const originTask = task.adhocOriginTaskId
775
+ ? await taskRepository.findOne({ where: { id: task.adhocOriginTaskId } })
776
+ : null;
777
+ const originTaskIsOpen = originTask?.status === workflow_engine_enums_1.TaskStatusEnum.PENDING ||
778
+ originTask?.status === workflow_engine_enums_1.TaskStatusEnum.IN_PROGRESS;
779
+ // Safety: when there is nobody to hand the decision back to (origin task
780
+ // missing, or already closed without a resolvable assignee), fall back to
781
+ // the default rejection path instead of leaving the token stuck WAITING
782
+ // with no open task.
783
+ if (!originTask || (!originTaskIsOpen && !originTask.assigneeMemberId)) {
784
+ return false;
785
+ }
786
+ const node = readWorkflowNodeOrThrow(instance.workflowSnapshot, task.nodeId);
787
+ if (!originTaskIsOpen && originTask.assigneeMemberId) {
788
+ // The origin approver already decided — reopen a fresh decision task so
789
+ // the stage can be re-decided after the rejected pre-approval.
790
+ const reopenedTask = await taskRepository.save(taskRepository.create({
791
+ assigneeMemberId: originTask.assigneeMemberId,
792
+ assignmentType: workflow_engine_enums_1.TaskAssignmentTypeEnum.DIRECT_MEMBER,
793
+ completedAt: null,
794
+ createdAt: decidedAt,
795
+ decisionPolicySnapshot: { type: 'SINGLE' },
796
+ delegationChain: originTask.delegationChain,
797
+ instanceId: instance.id,
798
+ nodeId: originTask.nodeId,
799
+ openedAt: null,
800
+ originalAssigneeMemberId: originTask.originalAssigneeMemberId,
801
+ slaDueAt: null,
802
+ status: workflow_engine_enums_1.TaskStatusEnum.PENDING,
803
+ tokenId: originTask.tokenId,
804
+ }));
805
+ await taskCandidateRepository.save(taskCandidateRepository.create({
806
+ claimedAt: null,
807
+ createdAt: decidedAt,
808
+ decidedAt: null,
809
+ delegationChain: originTask.delegationChain,
810
+ memberId: originTask.assigneeMemberId,
811
+ originalMemberId: originTask.originalAssigneeMemberId ?? originTask.assigneeMemberId,
812
+ sourceType: 'DIRECT',
813
+ status: workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING,
814
+ taskId: reopenedTask.id,
815
+ }));
816
+ await activityRepository.save(activityRepository.create({
817
+ actorMemberId: task.assigneeMemberId,
818
+ eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.TASK_CREATED,
819
+ instanceId: instance.id,
820
+ nodeId: originTask.nodeId,
821
+ payload: {
822
+ adhocDirectiveId: directive.id,
823
+ assigneeMemberId: originTask.assigneeMemberId,
824
+ reopenedFromTaskId: originTask.id,
825
+ rejectedAdhocTaskId: task.id,
826
+ tokenId: originTask.tokenId,
827
+ },
828
+ taskId: reopenedTask.id,
829
+ }));
830
+ if (node.type === 'userTask') {
831
+ await this.notificationService.createTaskAssignedNotification({
832
+ instance,
833
+ manager,
834
+ node,
835
+ task: reopenedTask,
836
+ });
837
+ }
838
+ }
839
+ const recipientMemberIds = await this.readOpenTaskRecipientMemberIds(manager, originTask);
840
+ if (recipientMemberIds.length > 0) {
841
+ await this.notificationService.createAdhocWorkflowNotifications({
842
+ channels: null,
843
+ instance,
844
+ manager,
845
+ message: `案件「${instance.title}」的臨時加簽已被拒絕${decisionComment ? `:${decisionComment}` : '。'}`,
846
+ payload: {
847
+ adhocTaskId: task.id,
848
+ directiveId: directive.id,
849
+ nodeId: task.nodeId,
850
+ originTaskId: originTask.id,
851
+ type: 'PRE_APPROVAL_RETURNED',
852
+ },
853
+ recipientMemberIds,
854
+ });
855
+ }
856
+ await activityRepository.save(activityRepository.create({
857
+ actorMemberId: task.assigneeMemberId,
858
+ eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.ADHOC_PRE_APPROVAL_RETURNED,
859
+ instanceId: instance.id,
860
+ nodeId: task.nodeId,
861
+ payload: {
862
+ comment: decisionComment,
863
+ directiveId: directive.id,
864
+ originTaskId: originTask.id,
865
+ rejectedAdhocTaskId: task.id,
866
+ },
867
+ taskId: task.id,
868
+ }));
869
+ return true;
870
+ }
871
+ async readOpenTaskRecipientMemberIds(manager, task) {
872
+ if (task.assigneeMemberId) {
873
+ return [task.assigneeMemberId];
874
+ }
875
+ const candidates = await manager
876
+ .getRepository(task_candidate_entity_1.TaskCandidateEntity)
877
+ .find({ where: { taskId: task.id } });
878
+ return uniqueTexts(candidates
879
+ .filter((candidate) => candidate.status === workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING ||
880
+ candidate.status === workflow_engine_enums_1.TaskCandidateStatusEnum.CLAIMED)
881
+ .map((candidate) => candidate.memberId));
882
+ }
883
+ async dispatchAdhocStageNotifications(manager, instance, nodeId, outcome) {
884
+ const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
885
+ const directives = await directiveRepository.find({
886
+ order: { createdAt: 'ASC' },
887
+ where: {
888
+ instanceId: instance.id,
889
+ originNodeId: nodeId,
890
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.PENDING,
891
+ type: adhoc_enums_1.AdhocDirectiveTypeEnum.STAGE_NOTIFY,
892
+ },
893
+ });
894
+ if (directives.length === 0) {
895
+ return;
896
+ }
897
+ const node = readWorkflowNodeOrThrow(instance.workflowSnapshot, nodeId);
898
+ const nodeLabel = node.data.label;
899
+ const outcomeLabel = readAdhocStageOutcomeLabel(outcome);
900
+ const consumedAt = new Date();
901
+ await directives.reduce(async (previous, directive) => {
902
+ await previous;
903
+ await this.dispatchAdhocDirectiveNotification(manager, instance, directive, `案件「${instance.title}」的階段「${nodeLabel}」已${outcomeLabel}。`, {
904
+ nodeId,
905
+ nodeLabel,
906
+ outcome,
907
+ type: adhoc_enums_1.AdhocDirectiveTypeEnum.STAGE_NOTIFY,
908
+ });
909
+ await directiveRepository.save({
910
+ ...directive,
911
+ consumedAt,
912
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.CONSUMED,
913
+ });
914
+ }, Promise.resolve());
915
+ }
916
+ async dispatchAdhocCompletionNotifications(manager, instance, finalState) {
917
+ const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
918
+ const directives = await directiveRepository.find({
919
+ order: { createdAt: 'ASC' },
920
+ where: {
921
+ instanceId: instance.id,
922
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.PENDING,
923
+ type: adhoc_enums_1.AdhocDirectiveTypeEnum.COMPLETION_NOTIFY,
924
+ },
925
+ });
926
+ if (directives.length === 0) {
927
+ return;
928
+ }
929
+ const finalStateLabel = readInstanceFinalStateLabel(finalState);
930
+ const consumedAt = new Date();
931
+ await directives.reduce(async (previous, directive) => {
932
+ await previous;
933
+ await this.dispatchAdhocDirectiveNotification(manager, instance, directive, `案件「${instance.title}」已結案(${finalStateLabel})。`, {
934
+ finalState,
935
+ type: adhoc_enums_1.AdhocDirectiveTypeEnum.COMPLETION_NOTIFY,
936
+ });
937
+ await directiveRepository.save({
938
+ ...directive,
939
+ consumedAt,
940
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.CONSUMED,
941
+ });
942
+ }, Promise.resolve());
943
+ }
944
+ async dispatchAdhocDirectiveNotification(manager, instance, directive, message, payload) {
945
+ const targetValue = readAdhocTargetValue(directive);
946
+ const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
947
+ if (targetValue.kind === adhoc_enums_1.AdhocTargetKindEnum.WEBHOOK) {
948
+ const result = await executeAdhocWebhookDispatch(this.serviceTaskDispatcher, targetValue, {
949
+ ...payload,
950
+ instanceId: instance.id,
951
+ instanceTitle: instance.title,
952
+ message,
953
+ triggeredAt: new Date().toISOString(),
954
+ });
955
+ await activityRepository.save(activityRepository.create({
956
+ actorMemberId: null,
957
+ eventType: result.ok
958
+ ? workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED
959
+ : workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
960
+ instanceId: instance.id,
961
+ nodeId: directive.originNodeId,
962
+ payload: {
963
+ action: 'ADHOC_WEBHOOK',
964
+ directiveId: directive.id,
965
+ error: result.error,
966
+ ok: result.ok,
967
+ status: result.status,
968
+ url: targetValue.webhookUrl,
969
+ },
970
+ taskId: null,
971
+ }));
972
+ return;
973
+ }
974
+ try {
975
+ const recipients = await this.resolveApproverResolver(manager, instance, buildAdhocApproverResolver(targetValue), '臨時通知對象');
976
+ await this.notificationService.createAdhocWorkflowNotifications({
977
+ channels: directive.channels,
978
+ instance,
979
+ manager,
980
+ message,
981
+ payload: { ...payload, directiveId: directive.id },
982
+ recipientMemberIds: recipients.map((recipient) => recipient.memberId),
983
+ });
984
+ }
985
+ catch (error) {
986
+ // Notification target resolution must never block the workflow
987
+ // transition that triggered it — record the failure instead.
988
+ await activityRepository.save(activityRepository.create({
989
+ actorMemberId: null,
990
+ eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
991
+ instanceId: instance.id,
992
+ nodeId: directive.originNodeId,
993
+ payload: {
994
+ action: 'ADHOC_NOTIFY',
995
+ directiveId: directive.id,
996
+ error: error instanceof Error
997
+ ? error.message
998
+ : 'Ad-hoc notification dispatch failed',
999
+ },
1000
+ taskId: null,
1001
+ }));
1002
+ }
1003
+ }
1004
+ async cancelPendingAdhocDirectives(manager, instanceId, types) {
1005
+ const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
1006
+ const directives = await directiveRepository.find({
1007
+ where: {
1008
+ instanceId,
1009
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.PENDING,
1010
+ ...(types ? { type: (0, typeorm_2.In)([...types]) } : {}),
1011
+ },
1012
+ });
1013
+ if (directives.length === 0) {
1014
+ return;
1015
+ }
1016
+ await directiveRepository.save(directives.map((directive) => ({
1017
+ ...directive,
1018
+ status: adhoc_enums_1.AdhocDirectiveStatusEnum.CANCELLED,
1019
+ })));
1020
+ }
456
1021
  dryRunApprovalWorkflow(input) {
457
1022
  const workflowDefinition = parseWorkflowDefinition(input.workflowDefinitionJson);
458
1023
  const formData = parseJsonObject(input.formDataJson, 'formDataJson');
@@ -966,7 +1531,16 @@ let WorkflowEngineService = class WorkflowEngineService {
966
1531
  const decisions = await this.taskDecisionRepository.find({
967
1532
  where: { taskId: (0, typeorm_2.In)(taskIds) },
968
1533
  });
969
- return decisions.some((decision) => decision.decidedByMemberId === memberId);
1534
+ if (decisions.some((decision) => decision.decidedByMemberId === memberId)) {
1535
+ return true;
1536
+ }
1537
+ // Members who received any notification for this instance (e.g. ad-hoc
1538
+ // stage / completion notify recipients, CC'd service-task recipients) may
1539
+ // open the instance they were notified about.
1540
+ const notification = await this.notificationRepository.findOne({
1541
+ where: { instanceId: instance.id, recipientMemberId: memberId },
1542
+ });
1543
+ return Boolean(notification);
970
1544
  }
971
1545
  async processActiveToken(manager, instance, token) {
972
1546
  const node = readWorkflowNodeOrThrow(instance.workflowSnapshot, token.currentNodeId);
@@ -1047,6 +1621,8 @@ let WorkflowEngineService = class WorkflowEngineService {
1047
1621
  instance: completedInstance,
1048
1622
  manager,
1049
1623
  });
1624
+ await this.dispatchAdhocCompletionNotifications(manager, completedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED);
1625
+ await this.cancelPendingAdhocDirectives(manager, instance.id, null);
1050
1626
  }
1051
1627
  async isNodeEntryReady(manager, instance, token, node) {
1052
1628
  if (node.type === 'startEvent') {
@@ -1281,6 +1857,8 @@ let WorkflowEngineService = class WorkflowEngineService {
1281
1857
  manager,
1282
1858
  });
1283
1859
  }
1860
+ await this.dispatchAdhocCompletionNotifications(manager, completedInstance, instanceState);
1861
+ await this.cancelPendingAdhocDirectives(manager, instance.id, null);
1284
1862
  }
1285
1863
  await activityRepository.save(activityRepository.create({
1286
1864
  actorMemberId: null,
@@ -1302,6 +1880,7 @@ let WorkflowEngineService = class WorkflowEngineService {
1302
1880
  const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
1303
1881
  const existingTask = await taskRepository.findOne({
1304
1882
  where: {
1883
+ isAdhoc: false,
1305
1884
  nodeId: node.id,
1306
1885
  tokenId: token.id,
1307
1886
  },
@@ -1377,6 +1956,9 @@ let WorkflowEngineService = class WorkflowEngineService {
1377
1956
  }),
1378
1957
  });
1379
1958
  }, Promise.resolve());
1959
+ // Pending ad-hoc countersign directives attach to the next user task
1960
+ // created after they were requested — spawn their parallel tasks now.
1961
+ await this.spawnCountersignTasksForNode(manager, instance, token, node, now);
1380
1962
  }
1381
1963
  async markTaskCandidateDecision({ action, candidate, candidates, decidedAt, manager, task, }) {
1382
1964
  const taskCandidateRepository = manager.getRepository(task_candidate_entity_1.TaskCandidateEntity);
@@ -1530,12 +2112,19 @@ let WorkflowEngineService = class WorkflowEngineService {
1530
2112
  }
1531
2113
  async resolveRuntimeTaskCandidates(manager, instance, node) {
1532
2114
  const resolvedCandidates = await this.resolveApproverResolver(manager, instance, node.data.approverResolver, `簽核節點「${node.data.label}」`);
2115
+ const uniqueCandidates = await this.applyDelegationToResolvedCandidates(instance, node.id, resolvedCandidates);
2116
+ if (uniqueCandidates.length === 0) {
2117
+ throw new common_1.ConflictException(`簽核節點「${node.data.label}」 did not resolve to any member id`);
2118
+ }
2119
+ return uniqueCandidates;
2120
+ }
2121
+ async applyDelegationToResolvedCandidates(instance, nodeId, resolvedCandidates) {
1533
2122
  const context = {
1534
2123
  formData: instance.formData,
1535
2124
  initiatorMemberId: instance.initiatorMemberId,
1536
2125
  initiatorMetadataSnapshot: instance.initiatorMetadataSnapshot,
1537
2126
  instanceId: instance.id,
1538
- nodeId: node.id,
2127
+ nodeId,
1539
2128
  state: instance.state,
1540
2129
  templateId: instance.templateId,
1541
2130
  templateVersionId: instance.templateVersionId,
@@ -1550,11 +2139,7 @@ let WorkflowEngineService = class WorkflowEngineService {
1550
2139
  sourceType: candidate.sourceType,
1551
2140
  };
1552
2141
  }));
1553
- const uniqueCandidates = uniqueRuntimeCandidates(delegatedCandidates);
1554
- if (uniqueCandidates.length === 0) {
1555
- throw new common_1.ConflictException(`簽核節點「${node.data.label}」 did not resolve to any member id`);
1556
- }
1557
- return uniqueCandidates;
2142
+ return uniqueRuntimeCandidates(delegatedCandidates);
1558
2143
  }
1559
2144
  async resolveApproverResolver(manager, instance, resolver, label) {
1560
2145
  if (resolver.type === 'DIRECT') {
@@ -1851,27 +2436,25 @@ let WorkflowEngineService = class WorkflowEngineService {
1851
2436
  }));
1852
2437
  }
1853
2438
  async rejectInstance(manager, instance, task, rejectedAt) {
1854
- const tokenRepository = manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity);
1855
2439
  const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
1856
- const activeTokens = await tokenRepository.find({
1857
- where: { instanceId: instance.id },
2440
+ // Dispatch pending ad-hoc notifications for the rejecting node before the
2441
+ // remaining directives are cancelled by the terminal cleanup below.
2442
+ await this.dispatchAdhocStageNotifications(manager, instance, task.nodeId, 'REJECTED');
2443
+ // Consume open tokens and cancel any still-open tasks (parallel branches,
2444
+ // ad-hoc countersign / pre-approval tasks) so nothing actionable lingers
2445
+ // on a rejected instance.
2446
+ await this.consumeOpenRuntimeState(manager, instance, rejectedAt, workflow_engine_enums_1.TaskStatusEnum.CANCELLED);
2447
+ await this.notificationService.supersedeInstanceTaskNotifications({
2448
+ instanceId: instance.id,
2449
+ manager,
1858
2450
  });
1859
- const consumedTokens = activeTokens
1860
- .filter((token) => token.status === workflow_engine_enums_1.WorkflowTokenStatusEnum.ACTIVE ||
1861
- token.status === workflow_engine_enums_1.WorkflowTokenStatusEnum.WAITING)
1862
- .map((token) => ({
1863
- ...token,
1864
- consumedAt: rejectedAt,
1865
- status: workflow_engine_enums_1.WorkflowTokenStatusEnum.CONSUMED,
1866
- }));
1867
- if (consumedTokens.length) {
1868
- await tokenRepository.save(consumedTokens);
1869
- }
1870
- await instanceRepository.save({
2451
+ const rejectedInstance = await instanceRepository.save({
1871
2452
  ...instance,
1872
2453
  completedAt: rejectedAt,
1873
2454
  state: workflow_engine_enums_1.ApprovalInstanceStateEnum.REJECTED,
1874
2455
  });
2456
+ await this.dispatchAdhocCompletionNotifications(manager, rejectedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.REJECTED);
2457
+ await this.cancelPendingAdhocDirectives(manager, instance.id, null);
1875
2458
  await manager.getRepository(activity_log_entity_1.ActivityLogEntity).save(manager.getRepository(activity_log_entity_1.ActivityLogEntity).create({
1876
2459
  actorMemberId: task.assigneeMemberId,
1877
2460
  eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.TOKEN_ADVANCED,
@@ -1892,6 +2475,14 @@ let WorkflowEngineService = class WorkflowEngineService {
1892
2475
  const tokenRepository = manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity);
1893
2476
  const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
1894
2477
  const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
2478
+ // The returning node's stage has ended (outcome: RETURNED) — dispatch its
2479
+ // pending ad-hoc stage notifications, then drop flow-affecting ad-hoc
2480
+ // directives so they cannot replay after the return/resubmit cycle.
2481
+ await this.dispatchAdhocStageNotifications(manager, instance, task.nodeId, 'RETURNED');
2482
+ await this.cancelPendingAdhocDirectives(manager, instance.id, [
2483
+ adhoc_enums_1.AdhocDirectiveTypeEnum.COUNTERSIGN,
2484
+ adhoc_enums_1.AdhocDirectiveTypeEnum.PRE_APPROVAL,
2485
+ ]);
1895
2486
  await this.consumeOpenRuntimeState(manager, instance, returnedAt, workflow_engine_enums_1.TaskStatusEnum.CANCELLED);
1896
2487
  const returnedInstance = await instanceRepository.save({
1897
2488
  ...instance,
@@ -2121,11 +2712,12 @@ exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
2121
2712
  tslib_1.__param(4, (0, typeorm_1.InjectRepository)(task_decision_entity_1.TaskDecisionEntity)),
2122
2713
  tslib_1.__param(5, (0, typeorm_1.InjectRepository)(notification_entity_1.NotificationEntity)),
2123
2714
  tslib_1.__param(6, (0, typeorm_1.InjectRepository)(activity_log_entity_1.ActivityLogEntity)),
2124
- tslib_1.__param(7, (0, typeorm_1.InjectRepository)(approval_template_entity_1.ApprovalTemplateEntity)),
2125
- tslib_1.__param(8, (0, typeorm_1.InjectRepository)(approval_template_version_entity_1.ApprovalTemplateVersionEntity)),
2126
- tslib_1.__param(9, (0, typeorm_1.InjectRepository)(form_definition_version_entity_1.FormDefinitionVersionEntity)),
2127
- tslib_1.__param(15, (0, common_1.Optional)()),
2128
- tslib_1.__param(15, (0, common_1.Inject)(workflow_service_task_dispatcher_token_1.BPM_WORKFLOW_SERVICE_TASK_DISPATCHER)),
2715
+ tslib_1.__param(7, (0, typeorm_1.InjectRepository)(adhoc_directive_entity_1.AdhocDirectiveEntity)),
2716
+ tslib_1.__param(8, (0, typeorm_1.InjectRepository)(approval_template_entity_1.ApprovalTemplateEntity)),
2717
+ tslib_1.__param(9, (0, typeorm_1.InjectRepository)(approval_template_version_entity_1.ApprovalTemplateVersionEntity)),
2718
+ tslib_1.__param(10, (0, typeorm_1.InjectRepository)(form_definition_version_entity_1.FormDefinitionVersionEntity)),
2719
+ tslib_1.__param(16, (0, common_1.Optional)()),
2720
+ tslib_1.__param(16, (0, common_1.Inject)(workflow_service_task_dispatcher_token_1.BPM_WORKFLOW_SERVICE_TASK_DISPATCHER)),
2129
2721
  tslib_1.__metadata("design:paramtypes", [typeorm_2.Repository,
2130
2722
  typeorm_2.Repository,
2131
2723
  typeorm_2.Repository,
@@ -2136,6 +2728,7 @@ exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
2136
2728
  typeorm_2.Repository,
2137
2729
  typeorm_2.Repository,
2138
2730
  typeorm_2.Repository,
2731
+ typeorm_2.Repository,
2139
2732
  attachment_service_1.AttachmentService,
2140
2733
  condition_service_1.ConditionService,
2141
2734
  delegation_service_1.DelegationService,
@@ -2540,6 +3133,152 @@ async function executeWebhookServiceAction(serviceTaskDispatcher, action, payloa
2540
3133
  };
2541
3134
  }
2542
3135
  }
3136
+ async function executeAdhocWebhookDispatch(serviceTaskDispatcher, target, payload) {
3137
+ if (!target.webhookUrl) {
3138
+ return { error: 'Webhook URL is missing', ok: false, status: null };
3139
+ }
3140
+ try {
3141
+ return await serviceTaskDispatcher.dispatchWebhook({
3142
+ headers: target.webhookHeaders,
3143
+ payload,
3144
+ url: target.webhookUrl,
3145
+ });
3146
+ }
3147
+ catch (error) {
3148
+ return {
3149
+ error: error instanceof Error ? error.message : 'Unknown webhook error',
3150
+ ok: false,
3151
+ status: null,
3152
+ };
3153
+ }
3154
+ }
3155
+ function buildAdhocTargetValue(input) {
3156
+ if (input.kind === adhoc_enums_1.AdhocTargetKindEnum.MEMBER) {
3157
+ const memberIds = uniqueTexts([...(input.memberIds ?? [])]);
3158
+ if (memberIds.length === 0) {
3159
+ throw new common_1.BadRequestException('memberIds is required for a MEMBER ad-hoc target');
3160
+ }
3161
+ return { kind: input.kind, memberIds };
3162
+ }
3163
+ if (input.kind === adhoc_enums_1.AdhocTargetKindEnum.POSITION) {
3164
+ const positionId = input.positionId?.trim();
3165
+ if (!positionId) {
3166
+ throw new common_1.BadRequestException('positionId is required for a POSITION ad-hoc target');
3167
+ }
3168
+ return { kind: input.kind, positionId };
3169
+ }
3170
+ if (input.kind === adhoc_enums_1.AdhocTargetKindEnum.ORG_UNIT_MEMBER) {
3171
+ const orgUnitId = input.orgUnitId?.trim();
3172
+ if (!orgUnitId) {
3173
+ throw new common_1.BadRequestException('orgUnitId is required for an ORG_UNIT_MEMBER ad-hoc target');
3174
+ }
3175
+ return {
3176
+ includeDescendants: Boolean(input.includeDescendants),
3177
+ kind: input.kind,
3178
+ orgUnitId,
3179
+ };
3180
+ }
3181
+ const webhookUrl = input.webhookUrl?.trim();
3182
+ if (!webhookUrl) {
3183
+ throw new common_1.BadRequestException('webhookUrl is required for a WEBHOOK ad-hoc target');
3184
+ }
3185
+ return {
3186
+ kind: input.kind,
3187
+ webhookHeaders: input.webhookHeadersJson
3188
+ ? parseAdhocWebhookHeaders(input.webhookHeadersJson)
3189
+ : undefined,
3190
+ webhookUrl,
3191
+ };
3192
+ }
3193
+ function parseAdhocWebhookHeaders(json) {
3194
+ try {
3195
+ const parsed = JSON.parse(json);
3196
+ if (!isRecord(parsed)) {
3197
+ throw new common_1.BadRequestException('webhookHeadersJson must be a JSON object of string values');
3198
+ }
3199
+ return Object.entries(parsed).reduce((accumulator, [key, value]) => ({
3200
+ ...accumulator,
3201
+ ...(typeof value === 'string' ? { [key]: value } : {}),
3202
+ }), {});
3203
+ }
3204
+ catch (error) {
3205
+ if (error instanceof common_1.BadRequestException) {
3206
+ throw error;
3207
+ }
3208
+ throw new common_1.BadRequestException('webhookHeadersJson is not valid JSON');
3209
+ }
3210
+ }
3211
+ function readAdhocTargetValue(directive) {
3212
+ const value = directive.targetValue;
3213
+ const kind = directive.targetKind;
3214
+ if (kind === adhoc_enums_1.AdhocTargetKindEnum.MEMBER) {
3215
+ return {
3216
+ kind,
3217
+ memberIds: Array.isArray(value.memberIds)
3218
+ ? value.memberIds.filter((memberId) => typeof memberId === 'string')
3219
+ : [],
3220
+ };
3221
+ }
3222
+ if (kind === adhoc_enums_1.AdhocTargetKindEnum.POSITION) {
3223
+ return {
3224
+ kind,
3225
+ positionId: typeof value.positionId === 'string' ? value.positionId : '',
3226
+ };
3227
+ }
3228
+ if (kind === adhoc_enums_1.AdhocTargetKindEnum.ORG_UNIT_MEMBER) {
3229
+ return {
3230
+ includeDescendants: Boolean(value.includeDescendants),
3231
+ kind,
3232
+ orgUnitId: typeof value.orgUnitId === 'string' ? value.orgUnitId : '',
3233
+ };
3234
+ }
3235
+ return {
3236
+ kind,
3237
+ webhookHeaders: isRecord(value.webhookHeaders)
3238
+ ? Object.entries(value.webhookHeaders).reduce((accumulator, [key, headerValue]) => ({
3239
+ ...accumulator,
3240
+ ...(typeof headerValue === 'string'
3241
+ ? { [key]: headerValue }
3242
+ : {}),
3243
+ }), {})
3244
+ : undefined,
3245
+ webhookUrl: typeof value.webhookUrl === 'string' ? value.webhookUrl : '',
3246
+ };
3247
+ }
3248
+ function buildAdhocApproverResolver(target) {
3249
+ if (target.kind === adhoc_enums_1.AdhocTargetKindEnum.MEMBER) {
3250
+ return { memberIds: target.memberIds ?? [], type: 'DIRECT' };
3251
+ }
3252
+ if (target.kind === adhoc_enums_1.AdhocTargetKindEnum.POSITION) {
3253
+ return { positionId: target.positionId ?? '', type: 'POSITION' };
3254
+ }
3255
+ if (target.kind === adhoc_enums_1.AdhocTargetKindEnum.ORG_UNIT_MEMBER) {
3256
+ return {
3257
+ includeDescendants: target.includeDescendants,
3258
+ orgUnitId: target.orgUnitId ?? '',
3259
+ type: 'ORG_UNIT_MEMBER',
3260
+ };
3261
+ }
3262
+ throw new common_1.BadRequestException('WEBHOOK ad-hoc targets cannot be used as approvers');
3263
+ }
3264
+ function readAdhocStageOutcomeLabel(outcome) {
3265
+ if (outcome === 'APPROVED') {
3266
+ return '通過';
3267
+ }
3268
+ if (outcome === 'REJECTED') {
3269
+ return '拒絕';
3270
+ }
3271
+ return '退回';
3272
+ }
3273
+ function readInstanceFinalStateLabel(finalState) {
3274
+ if (finalState === workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED) {
3275
+ return '核准';
3276
+ }
3277
+ if (finalState === workflow_engine_enums_1.ApprovalInstanceStateEnum.REJECTED) {
3278
+ return '拒絕';
3279
+ }
3280
+ return '取消';
3281
+ }
2543
3282
  function readManagerMemberIdFromInitiatorSnapshot(initiatorMetadataSnapshot, levelsUp) {
2544
3283
  const managerChain = initiatorMetadataSnapshot.managerChain;
2545
3284
  if (Array.isArray(managerChain)) {