@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.
- package/CHANGELOG.md +34 -0
- package/README.md +43 -0
- package/package.json +3 -2
- package/src/lib/migrations/0000000017000-adhoc-directives.d.ts +6 -0
- package/src/lib/migrations/0000000017000-adhoc-directives.js +53 -0
- package/src/lib/migrations/0000000017000-adhoc-directives.js.map +1 -0
- package/src/lib/migrations/index.d.ts +1 -0
- package/src/lib/migrations/index.js +3 -0
- package/src/lib/migrations/index.js.map +1 -1
- package/src/lib/notification/notification.service.d.ts +15 -1
- package/src/lib/notification/notification.service.js +30 -0
- package/src/lib/notification/notification.service.js.map +1 -1
- package/src/lib/workflow-engine/adhoc-directive.entity.d.ts +31 -0
- package/src/lib/workflow-engine/adhoc-directive.entity.js +105 -0
- package/src/lib/workflow-engine/adhoc-directive.entity.js.map +1 -0
- package/src/lib/workflow-engine/adhoc.enums.d.ts +21 -0
- package/src/lib/workflow-engine/adhoc.enums.js +42 -0
- package/src/lib/workflow-engine/adhoc.enums.js.map +1 -0
- package/src/lib/workflow-engine/dto/adhoc-notification.input.d.ts +6 -0
- package/src/lib/workflow-engine/dto/adhoc-notification.input.js +28 -0
- package/src/lib/workflow-engine/dto/adhoc-notification.input.js.map +1 -0
- package/src/lib/workflow-engine/dto/adhoc-target.input.d.ts +16 -0
- package/src/lib/workflow-engine/dto/adhoc-target.input.js +61 -0
- package/src/lib/workflow-engine/dto/adhoc-target.input.js.map +1 -0
- package/src/lib/workflow-engine/index.d.ts +4 -0
- package/src/lib/workflow-engine/index.js +4 -0
- package/src/lib/workflow-engine/index.js.map +1 -1
- package/src/lib/workflow-engine/task.entity.d.ts +5 -0
- package/src/lib/workflow-engine/task.entity.js +21 -0
- package/src/lib/workflow-engine/task.entity.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.enums.d.ts +3 -0
- package/src/lib/workflow-engine/workflow-engine.enums.js +3 -0
- package/src/lib/workflow-engine/workflow-engine.enums.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.module.js +2 -0
- package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.mutations.d.ts +10 -0
- package/src/lib/workflow-engine/workflow-engine.mutations.js +112 -0
- package/src/lib/workflow-engine/workflow-engine.mutations.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.queries.d.ts +2 -0
- package/src/lib/workflow-engine/workflow-engine.queries.js +12 -0
- package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +44 -1
- package/src/lib/workflow-engine/workflow-engine.service.js +780 -41
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1857
|
-
|
|
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
|
|
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)(
|
|
2125
|
-
tslib_1.__param(8, (0, typeorm_1.InjectRepository)(
|
|
2126
|
-
tslib_1.__param(9, (0, typeorm_1.InjectRepository)(
|
|
2127
|
-
tslib_1.__param(
|
|
2128
|
-
tslib_1.__param(
|
|
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)) {
|