@rytass/bpm-core-nestjs-module 0.1.10 → 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 (90) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +43 -0
  3. package/package.json +3 -2
  4. package/src/lib/database/migration-runner.js +2 -0
  5. package/src/lib/database/migration-runner.js.map +1 -1
  6. package/src/lib/database/reconcile-legacy-migrations.d.ts +13 -0
  7. package/src/lib/database/reconcile-legacy-migrations.js +70 -0
  8. package/src/lib/database/reconcile-legacy-migrations.js.map +1 -0
  9. package/src/lib/form/dto/form-definition.input.d.ts +5 -0
  10. package/src/lib/form/dto/form-definition.input.js +24 -1
  11. package/src/lib/form/dto/form-definition.input.js.map +1 -1
  12. package/src/lib/form/form.mutations.d.ts +2 -2
  13. package/src/lib/form/form.mutations.js +6 -5
  14. package/src/lib/form/form.mutations.js.map +1 -1
  15. package/src/lib/form/form.service.d.ts +20 -6
  16. package/src/lib/form/form.service.js +129 -80
  17. package/src/lib/form/form.service.js.map +1 -1
  18. package/src/lib/migrations/0000000014000-notification-resolution.d.ts +6 -0
  19. package/src/lib/migrations/0000000014000-notification-resolution.js +30 -0
  20. package/src/lib/migrations/0000000014000-notification-resolution.js.map +1 -0
  21. package/src/lib/migrations/0000000015000-backfill-stale-notification-resolution.d.ts +15 -0
  22. package/src/lib/migrations/0000000015000-backfill-stale-notification-resolution.js +37 -0
  23. package/src/lib/migrations/0000000015000-backfill-stale-notification-resolution.js.map +1 -0
  24. package/src/lib/migrations/0000000016000-archive-parallel-form-drafts.d.ts +15 -0
  25. package/src/lib/migrations/0000000016000-archive-parallel-form-drafts.js +34 -0
  26. package/src/lib/migrations/0000000016000-archive-parallel-form-drafts.js.map +1 -0
  27. package/src/lib/migrations/0000000017000-adhoc-directives.d.ts +6 -0
  28. package/src/lib/migrations/0000000017000-adhoc-directives.js +53 -0
  29. package/src/lib/migrations/0000000017000-adhoc-directives.js.map +1 -0
  30. package/src/lib/migrations/index.d.ts +4 -0
  31. package/src/lib/migrations/index.js +12 -0
  32. package/src/lib/migrations/index.js.map +1 -1
  33. package/src/lib/notification/notification.entity.d.ts +9 -1
  34. package/src/lib/notification/notification.entity.js +27 -0
  35. package/src/lib/notification/notification.entity.js.map +1 -1
  36. package/src/lib/notification/notification.enums.d.ts +14 -0
  37. package/src/lib/notification/notification.enums.js +19 -1
  38. package/src/lib/notification/notification.enums.js.map +1 -1
  39. package/src/lib/notification/notification.service.d.ts +40 -0
  40. package/src/lib/notification/notification.service.js +92 -0
  41. package/src/lib/notification/notification.service.js.map +1 -1
  42. package/src/lib/template/compose-approval-template.object.d.ts +11 -0
  43. package/src/lib/template/compose-approval-template.object.js +36 -0
  44. package/src/lib/template/compose-approval-template.object.js.map +1 -0
  45. package/src/lib/template/compose-template.mutations.d.ts +8 -0
  46. package/src/lib/template/compose-template.mutations.js +32 -0
  47. package/src/lib/template/compose-template.mutations.js.map +1 -0
  48. package/src/lib/template/dto/compose-approval-template.input.d.ts +17 -0
  49. package/src/lib/template/dto/compose-approval-template.input.js +99 -0
  50. package/src/lib/template/dto/compose-approval-template.input.js.map +1 -0
  51. package/src/lib/template/index.d.ts +2 -0
  52. package/src/lib/template/index.js +2 -0
  53. package/src/lib/template/index.js.map +1 -1
  54. package/src/lib/template/template.module.js +9 -1
  55. package/src/lib/template/template.module.js.map +1 -1
  56. package/src/lib/template/template.service.d.ts +25 -6
  57. package/src/lib/template/template.service.js +216 -83
  58. package/src/lib/template/template.service.js.map +1 -1
  59. package/src/lib/workflow-engine/adhoc-directive.entity.d.ts +31 -0
  60. package/src/lib/workflow-engine/adhoc-directive.entity.js +105 -0
  61. package/src/lib/workflow-engine/adhoc-directive.entity.js.map +1 -0
  62. package/src/lib/workflow-engine/adhoc.enums.d.ts +21 -0
  63. package/src/lib/workflow-engine/adhoc.enums.js +42 -0
  64. package/src/lib/workflow-engine/adhoc.enums.js.map +1 -0
  65. package/src/lib/workflow-engine/dto/adhoc-notification.input.d.ts +6 -0
  66. package/src/lib/workflow-engine/dto/adhoc-notification.input.js +28 -0
  67. package/src/lib/workflow-engine/dto/adhoc-notification.input.js.map +1 -0
  68. package/src/lib/workflow-engine/dto/adhoc-target.input.d.ts +16 -0
  69. package/src/lib/workflow-engine/dto/adhoc-target.input.js +61 -0
  70. package/src/lib/workflow-engine/dto/adhoc-target.input.js.map +1 -0
  71. package/src/lib/workflow-engine/index.d.ts +4 -0
  72. package/src/lib/workflow-engine/index.js +4 -0
  73. package/src/lib/workflow-engine/index.js.map +1 -1
  74. package/src/lib/workflow-engine/task.entity.d.ts +5 -0
  75. package/src/lib/workflow-engine/task.entity.js +21 -0
  76. package/src/lib/workflow-engine/task.entity.js.map +1 -1
  77. package/src/lib/workflow-engine/workflow-engine.enums.d.ts +3 -0
  78. package/src/lib/workflow-engine/workflow-engine.enums.js +3 -0
  79. package/src/lib/workflow-engine/workflow-engine.enums.js.map +1 -1
  80. package/src/lib/workflow-engine/workflow-engine.module.js +2 -0
  81. package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -1
  82. package/src/lib/workflow-engine/workflow-engine.mutations.d.ts +10 -0
  83. package/src/lib/workflow-engine/workflow-engine.mutations.js +112 -0
  84. package/src/lib/workflow-engine/workflow-engine.mutations.js.map +1 -1
  85. package/src/lib/workflow-engine/workflow-engine.queries.d.ts +2 -0
  86. package/src/lib/workflow-engine/workflow-engine.queries.js +12 -0
  87. package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
  88. package/src/lib/workflow-engine/workflow-engine.service.d.ts +44 -1
  89. package/src/lib/workflow-engine/workflow-engine.service.js +817 -41
  90. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
@@ -11,6 +11,7 @@ const form_definition_version_entity_1 = require("../form/form-definition-versio
11
11
  const form_enums_1 = require("../form/form.enums");
12
12
  const notification_service_1 = require("../notification/notification.service");
13
13
  const notification_entity_1 = require("../notification/notification.entity");
14
+ const notification_enums_1 = require("../notification/notification.enums");
14
15
  const signature_service_1 = require("../signature/signature.service");
15
16
  const condition_service_1 = require("../condition/condition.service");
16
17
  const manager_resolution_entity_1 = require("../organization/manager-resolution.entity");
@@ -21,6 +22,8 @@ const approval_template_version_entity_1 = require("../template/approval-templat
21
22
  const approval_template_entity_1 = require("../template/approval-template.entity");
22
23
  const template_enums_1 = require("../template/template.enums");
23
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");
24
27
  const approval_instance_entity_1 = require("./approval-instance.entity");
25
28
  const task_decision_entity_1 = require("./task-decision.entity");
26
29
  const task_candidate_entity_1 = require("./task-candidate.entity");
@@ -40,7 +43,7 @@ const WORKFLOW_READ_ALL_PERMISSIONS = new Set([
40
43
  'workflow.read_all',
41
44
  ]);
42
45
  let WorkflowEngineService = class WorkflowEngineService {
43
- 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()) {
44
47
  this.approvalInstanceRepository = approvalInstanceRepository;
45
48
  this.workflowTokenRepository = workflowTokenRepository;
46
49
  this.taskRepository = taskRepository;
@@ -48,6 +51,7 @@ let WorkflowEngineService = class WorkflowEngineService {
48
51
  this.taskDecisionRepository = taskDecisionRepository;
49
52
  this.notificationRepository = notificationRepository;
50
53
  this.activityLogRepository = activityLogRepository;
54
+ this.adhocDirectiveRepository = adhocDirectiveRepository;
51
55
  this.approvalTemplateRepository = approvalTemplateRepository;
52
56
  this.approvalTemplateVersionRepository = approvalTemplateVersionRepository;
53
57
  this.formDefinitionVersionRepository = formDefinitionVersionRepository;
@@ -186,19 +190,7 @@ let WorkflowEngineService = class WorkflowEngineService {
186
190
  task.status !== workflow_engine_enums_1.TaskStatusEnum.IN_PROGRESS) {
187
191
  throw new common_1.ConflictException(`Task ${task.id} is not pending`);
188
192
  }
189
- const taskCandidates = await taskCandidateRepository.find({
190
- where: { taskId: task.id },
191
- });
192
- const actorCandidate = taskCandidates.find((candidate) => candidate.memberId === input.decidedByMemberId);
193
- const isDirectAssignee = task.assigneeMemberId === input.decidedByMemberId;
194
- if (!isDirectAssignee && !actorCandidate) {
195
- throw new common_1.ConflictException(`Task ${task.id} is assigned to another member`);
196
- }
197
- if (actorCandidate &&
198
- actorCandidate.status !== workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING &&
199
- actorCandidate.status !== workflow_engine_enums_1.TaskCandidateStatusEnum.CLAIMED) {
200
- throw new common_1.ConflictException(`Task ${task.id} was already decided by this member`);
201
- }
193
+ const { actorCandidate, taskCandidates } = await this.readTaskActorContext(manager, task, input.decidedByMemberId);
202
194
  if (input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED &&
203
195
  input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.REJECTED &&
204
196
  input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.RETURNED &&
@@ -270,6 +262,13 @@ let WorkflowEngineService = class WorkflowEngineService {
270
262
  }));
271
263
  if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.TRANSFERRED) {
272
264
  await this.transferTask(manager, instance, claimedTask, decision, signature, transferToMemberId, decisionComment, decidedAt, claimedCandidate);
265
+ await this.notificationService.resolveTaskNotifications({
266
+ actingMemberId: input.decidedByMemberId,
267
+ manager,
268
+ resolution: notification_enums_1.NotificationResolutionEnum.TRANSFERRED,
269
+ supersedeOthers: true,
270
+ taskId: claimedTask.id,
271
+ });
273
272
  return decision;
274
273
  }
275
274
  const nextCandidateRows = await this.markTaskCandidateDecision({
@@ -309,8 +308,28 @@ let WorkflowEngineService = class WorkflowEngineService {
309
308
  },
310
309
  taskId: completedTask.id,
311
310
  }));
311
+ // Resolve the recipient's "待簽" notification so its inline 同意/拒絕
312
+ // actions disappear once the task is decided. When the task has ended
313
+ // (rejected / returned, or an approval that completed it), every other
314
+ // candidate's notification is superseded too; a non-completing approval
315
+ // on a multi-approver task leaves the remaining approvers' open.
316
+ const taskEnded = input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED || shouldCompleteTask;
317
+ await this.notificationService.resolveTaskNotifications({
318
+ actingMemberId: input.decidedByMemberId,
319
+ manager,
320
+ resolution: mapDecisionToResolution(input.action),
321
+ supersedeOthers: taskEnded,
322
+ taskId: completedTask.id,
323
+ });
312
324
  if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED &&
313
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
+ }
314
333
  const token = await tokenRepository.findOne({
315
334
  where: { id: claimedTask.tokenId },
316
335
  });
@@ -325,6 +344,7 @@ let WorkflowEngineService = class WorkflowEngineService {
325
344
  consumedAt: null,
326
345
  status: workflow_engine_enums_1.WorkflowTokenStatusEnum.ACTIVE,
327
346
  });
347
+ await this.dispatchAdhocStageNotifications(manager, instance, claimedTask.nodeId, 'APPROVED');
328
348
  await this.advanceTokenToOutgoingNodes(manager, instance, activeToken, readWorkflowNodeOrThrow(instance.workflowSnapshot, claimedTask.nodeId));
329
349
  await this.processRunningInstance(manager, instance);
330
350
  return decision;
@@ -336,6 +356,9 @@ let WorkflowEngineService = class WorkflowEngineService {
336
356
  await this.returnInstanceToNode(manager, instance, completedTask, returnToNodeId, decidedAt);
337
357
  return decision;
338
358
  }
359
+ if (await this.handleAdhocPreApprovalRejection(manager, instance, completedTask, decidedAt, decisionComment)) {
360
+ return decision;
361
+ }
339
362
  await this.rejectInstance(manager, instance, completedTask, decidedAt);
340
363
  return decision;
341
364
  });
@@ -361,11 +384,17 @@ let WorkflowEngineService = class WorkflowEngineService {
361
384
  }
362
385
  const cancelledAt = new Date();
363
386
  await this.consumeOpenRuntimeState(manager, instance, cancelledAt, workflow_engine_enums_1.TaskStatusEnum.CANCELLED);
387
+ await this.notificationService.supersedeInstanceTaskNotifications({
388
+ instanceId: instance.id,
389
+ manager,
390
+ });
364
391
  const cancelledInstance = await instanceRepository.save({
365
392
  ...instance,
366
393
  completedAt: cancelledAt,
367
394
  state: workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED,
368
395
  });
396
+ await this.dispatchAdhocCompletionNotifications(manager, cancelledInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED);
397
+ await this.cancelPendingAdhocDirectives(manager, instance.id, null);
369
398
  await manager.getRepository(activity_log_entity_1.ActivityLogEntity).save(manager.getRepository(activity_log_entity_1.ActivityLogEntity).create({
370
399
  actorMemberId: input.cancelledByMemberId,
371
400
  eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.INSTANCE_CANCELLED,
@@ -428,6 +457,567 @@ let WorkflowEngineService = class WorkflowEngineService {
428
457
  return runtimeInstance;
429
458
  });
430
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
+ }
431
1021
  dryRunApprovalWorkflow(input) {
432
1022
  const workflowDefinition = parseWorkflowDefinition(input.workflowDefinitionJson);
433
1023
  const formData = parseJsonObject(input.formDataJson, 'formDataJson');
@@ -941,7 +1531,16 @@ let WorkflowEngineService = class WorkflowEngineService {
941
1531
  const decisions = await this.taskDecisionRepository.find({
942
1532
  where: { taskId: (0, typeorm_2.In)(taskIds) },
943
1533
  });
944
- 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);
945
1544
  }
946
1545
  async processActiveToken(manager, instance, token) {
947
1546
  const node = readWorkflowNodeOrThrow(instance.workflowSnapshot, token.currentNodeId);
@@ -1022,6 +1621,8 @@ let WorkflowEngineService = class WorkflowEngineService {
1022
1621
  instance: completedInstance,
1023
1622
  manager,
1024
1623
  });
1624
+ await this.dispatchAdhocCompletionNotifications(manager, completedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED);
1625
+ await this.cancelPendingAdhocDirectives(manager, instance.id, null);
1025
1626
  }
1026
1627
  async isNodeEntryReady(manager, instance, token, node) {
1027
1628
  if (node.type === 'startEvent') {
@@ -1256,6 +1857,8 @@ let WorkflowEngineService = class WorkflowEngineService {
1256
1857
  manager,
1257
1858
  });
1258
1859
  }
1860
+ await this.dispatchAdhocCompletionNotifications(manager, completedInstance, instanceState);
1861
+ await this.cancelPendingAdhocDirectives(manager, instance.id, null);
1259
1862
  }
1260
1863
  await activityRepository.save(activityRepository.create({
1261
1864
  actorMemberId: null,
@@ -1277,6 +1880,7 @@ let WorkflowEngineService = class WorkflowEngineService {
1277
1880
  const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
1278
1881
  const existingTask = await taskRepository.findOne({
1279
1882
  where: {
1883
+ isAdhoc: false,
1280
1884
  nodeId: node.id,
1281
1885
  tokenId: token.id,
1282
1886
  },
@@ -1352,6 +1956,9 @@ let WorkflowEngineService = class WorkflowEngineService {
1352
1956
  }),
1353
1957
  });
1354
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);
1355
1962
  }
1356
1963
  async markTaskCandidateDecision({ action, candidate, candidates, decidedAt, manager, task, }) {
1357
1964
  const taskCandidateRepository = manager.getRepository(task_candidate_entity_1.TaskCandidateEntity);
@@ -1505,12 +2112,19 @@ let WorkflowEngineService = class WorkflowEngineService {
1505
2112
  }
1506
2113
  async resolveRuntimeTaskCandidates(manager, instance, node) {
1507
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) {
1508
2122
  const context = {
1509
2123
  formData: instance.formData,
1510
2124
  initiatorMemberId: instance.initiatorMemberId,
1511
2125
  initiatorMetadataSnapshot: instance.initiatorMetadataSnapshot,
1512
2126
  instanceId: instance.id,
1513
- nodeId: node.id,
2127
+ nodeId,
1514
2128
  state: instance.state,
1515
2129
  templateId: instance.templateId,
1516
2130
  templateVersionId: instance.templateVersionId,
@@ -1525,11 +2139,7 @@ let WorkflowEngineService = class WorkflowEngineService {
1525
2139
  sourceType: candidate.sourceType,
1526
2140
  };
1527
2141
  }));
1528
- const uniqueCandidates = uniqueRuntimeCandidates(delegatedCandidates);
1529
- if (uniqueCandidates.length === 0) {
1530
- throw new common_1.ConflictException(`簽核節點「${node.data.label}」 did not resolve to any member id`);
1531
- }
1532
- return uniqueCandidates;
2142
+ return uniqueRuntimeCandidates(delegatedCandidates);
1533
2143
  }
1534
2144
  async resolveApproverResolver(manager, instance, resolver, label) {
1535
2145
  if (resolver.type === 'DIRECT') {
@@ -1826,27 +2436,25 @@ let WorkflowEngineService = class WorkflowEngineService {
1826
2436
  }));
1827
2437
  }
1828
2438
  async rejectInstance(manager, instance, task, rejectedAt) {
1829
- const tokenRepository = manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity);
1830
2439
  const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
1831
- const activeTokens = await tokenRepository.find({
1832
- 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,
1833
2450
  });
1834
- const consumedTokens = activeTokens
1835
- .filter((token) => token.status === workflow_engine_enums_1.WorkflowTokenStatusEnum.ACTIVE ||
1836
- token.status === workflow_engine_enums_1.WorkflowTokenStatusEnum.WAITING)
1837
- .map((token) => ({
1838
- ...token,
1839
- consumedAt: rejectedAt,
1840
- status: workflow_engine_enums_1.WorkflowTokenStatusEnum.CONSUMED,
1841
- }));
1842
- if (consumedTokens.length) {
1843
- await tokenRepository.save(consumedTokens);
1844
- }
1845
- await instanceRepository.save({
2451
+ const rejectedInstance = await instanceRepository.save({
1846
2452
  ...instance,
1847
2453
  completedAt: rejectedAt,
1848
2454
  state: workflow_engine_enums_1.ApprovalInstanceStateEnum.REJECTED,
1849
2455
  });
2456
+ await this.dispatchAdhocCompletionNotifications(manager, rejectedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.REJECTED);
2457
+ await this.cancelPendingAdhocDirectives(manager, instance.id, null);
1850
2458
  await manager.getRepository(activity_log_entity_1.ActivityLogEntity).save(manager.getRepository(activity_log_entity_1.ActivityLogEntity).create({
1851
2459
  actorMemberId: task.assigneeMemberId,
1852
2460
  eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.TOKEN_ADVANCED,
@@ -1867,6 +2475,14 @@ let WorkflowEngineService = class WorkflowEngineService {
1867
2475
  const tokenRepository = manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity);
1868
2476
  const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
1869
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
+ ]);
1870
2486
  await this.consumeOpenRuntimeState(manager, instance, returnedAt, workflow_engine_enums_1.TaskStatusEnum.CANCELLED);
1871
2487
  const returnedInstance = await instanceRepository.save({
1872
2488
  ...instance,
@@ -2096,11 +2712,12 @@ exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
2096
2712
  tslib_1.__param(4, (0, typeorm_1.InjectRepository)(task_decision_entity_1.TaskDecisionEntity)),
2097
2713
  tslib_1.__param(5, (0, typeorm_1.InjectRepository)(notification_entity_1.NotificationEntity)),
2098
2714
  tslib_1.__param(6, (0, typeorm_1.InjectRepository)(activity_log_entity_1.ActivityLogEntity)),
2099
- tslib_1.__param(7, (0, typeorm_1.InjectRepository)(approval_template_entity_1.ApprovalTemplateEntity)),
2100
- tslib_1.__param(8, (0, typeorm_1.InjectRepository)(approval_template_version_entity_1.ApprovalTemplateVersionEntity)),
2101
- tslib_1.__param(9, (0, typeorm_1.InjectRepository)(form_definition_version_entity_1.FormDefinitionVersionEntity)),
2102
- tslib_1.__param(15, (0, common_1.Optional)()),
2103
- 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)),
2104
2721
  tslib_1.__metadata("design:paramtypes", [typeorm_2.Repository,
2105
2722
  typeorm_2.Repository,
2106
2723
  typeorm_2.Repository,
@@ -2111,6 +2728,7 @@ exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
2111
2728
  typeorm_2.Repository,
2112
2729
  typeorm_2.Repository,
2113
2730
  typeorm_2.Repository,
2731
+ typeorm_2.Repository,
2114
2732
  attachment_service_1.AttachmentService,
2115
2733
  condition_service_1.ConditionService,
2116
2734
  delegation_service_1.DelegationService,
@@ -2515,6 +3133,152 @@ async function executeWebhookServiceAction(serviceTaskDispatcher, action, payloa
2515
3133
  };
2516
3134
  }
2517
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
+ }
2518
3282
  function readManagerMemberIdFromInitiatorSnapshot(initiatorMetadataSnapshot, levelsUp) {
2519
3283
  const managerChain = initiatorMetadataSnapshot.managerChain;
2520
3284
  if (Array.isArray(managerChain)) {
@@ -2782,4 +3546,16 @@ function readDryRunStepMessage(node, entryConditionMet) {
2782
3546
  }
2783
3547
  return '節點條件通過。';
2784
3548
  }
3549
+ function mapDecisionToResolution(action) {
3550
+ switch (action) {
3551
+ case workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED:
3552
+ return notification_enums_1.NotificationResolutionEnum.APPROVED;
3553
+ case workflow_engine_enums_1.TaskDecisionActionEnum.REJECTED:
3554
+ return notification_enums_1.NotificationResolutionEnum.REJECTED;
3555
+ case workflow_engine_enums_1.TaskDecisionActionEnum.RETURNED:
3556
+ return notification_enums_1.NotificationResolutionEnum.RETURNED;
3557
+ case workflow_engine_enums_1.TaskDecisionActionEnum.TRANSFERRED:
3558
+ return notification_enums_1.NotificationResolutionEnum.TRANSFERRED;
3559
+ }
3560
+ }
2785
3561
  //# sourceMappingURL=workflow-engine.service.js.map