@rytass/bpm-core-nestjs-module 0.15.0 → 0.17.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 +89 -0
- package/README.md +421 -271
- package/package.json +5 -5
- package/src/lib/attachment/attachment-options.d.ts +23 -10
- package/src/lib/attachment/attachment-options.js +17 -31
- package/src/lib/attachment/attachment-options.js.map +1 -1
- package/src/lib/attachment/attachment-request-origin.d.ts +19 -0
- package/src/lib/attachment/attachment-request-origin.js +51 -0
- package/src/lib/attachment/attachment-request-origin.js.map +1 -0
- package/src/lib/attachment/attachment.module.d.ts +1 -2
- package/src/lib/attachment/attachment.module.js.map +1 -1
- package/src/lib/attachment/attachment.queries.d.ts +2 -2
- package/src/lib/attachment/attachment.queries.js +9 -4
- package/src/lib/attachment/attachment.queries.js.map +1 -1
- package/src/lib/attachment/attachment.service.d.ts +6 -1
- package/src/lib/attachment/attachment.service.js +6 -4
- package/src/lib/attachment/attachment.service.js.map +1 -1
- package/src/lib/bpm/bpm-root-options.d.ts +1 -2
- package/src/lib/bpm/bpm-root-options.js.map +1 -1
- package/src/lib/bpm/bpm-root.module.d.ts +1 -2
- package/src/lib/bpm/bpm-root.module.js.map +1 -1
- package/src/lib/bpm-auth/bpm-auth.options.d.ts +1 -2
- package/src/lib/calendar/calendar.module.d.ts +1 -2
- package/src/lib/calendar/calendar.module.js.map +1 -1
- package/src/lib/delegation/delegation-rule.entity.js +3 -1
- package/src/lib/delegation/delegation-rule.entity.js.map +1 -1
- package/src/lib/delegation/delegation.service.d.ts +4 -0
- package/src/lib/delegation/delegation.service.js +19 -3
- package/src/lib/delegation/delegation.service.js.map +1 -1
- package/src/lib/delegation/dto/delegation-rule.input.js +6 -2
- package/src/lib/delegation/dto/delegation-rule.input.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.module.d.ts +1 -2
- package/src/lib/form-data-source/form-data-source.module.js.map +1 -1
- package/src/lib/identity/identity.module.d.ts +1 -2
- package/src/lib/identity/identity.module.js.map +1 -1
- package/src/lib/migrations/0000000000001-enable-postgres-extensions.js +6 -2
- package/src/lib/migrations/0000000000001-enable-postgres-extensions.js.map +1 -1
- package/src/lib/notification/notification-options.d.ts +5 -5
- package/src/lib/notification/notification-options.module.d.ts +1 -2
- package/src/lib/notification/notification-options.module.js.map +1 -1
- package/src/lib/notification/notification.service.js +5 -2
- package/src/lib/notification/notification.service.js.map +1 -1
- package/src/lib/organization/index.d.ts +1 -0
- package/src/lib/organization/index.js +1 -0
- package/src/lib/organization/index.js.map +1 -1
- package/src/lib/organization/org-unit-retirement-impact.object.d.ts +18 -0
- package/src/lib/organization/org-unit-retirement-impact.object.js +34 -0
- package/src/lib/organization/org-unit-retirement-impact.object.js.map +1 -0
- package/src/lib/organization/organization.mutations.d.ts +1 -1
- package/src/lib/organization/organization.mutations.js +7 -4
- package/src/lib/organization/organization.mutations.js.map +1 -1
- package/src/lib/organization/organization.queries.d.ts +2 -0
- package/src/lib/organization/organization.queries.js +13 -0
- package/src/lib/organization/organization.queries.js.map +1 -1
- package/src/lib/organization/organization.service.d.ts +32 -1
- package/src/lib/organization/organization.service.js +157 -16
- package/src/lib/organization/organization.service.js.map +1 -1
- package/src/lib/signature/signature.module.d.ts +1 -2
- package/src/lib/signature/signature.module.js.map +1 -1
- package/src/lib/template/template.service.js +91 -57
- package/src/lib/template/template.service.js.map +1 -1
- package/src/lib/template/workflow-definition.validator.js +11 -0
- package/src/lib/template/workflow-definition.validator.js.map +1 -1
- package/src/lib/workflow-engine/dto/reassign-member-tasks.input.d.ts +7 -0
- package/src/lib/workflow-engine/dto/reassign-member-tasks.input.js +34 -0
- package/src/lib/workflow-engine/dto/reassign-member-tasks.input.js.map +1 -0
- package/src/lib/workflow-engine/dto/reassign-task.input.d.ts +8 -0
- package/src/lib/workflow-engine/dto/reassign-task.input.js +39 -0
- package/src/lib/workflow-engine/dto/reassign-task.input.js.map +1 -0
- package/src/lib/workflow-engine/index.d.ts +3 -0
- package/src/lib/workflow-engine/index.js +3 -0
- package/src/lib/workflow-engine/index.js.map +1 -1
- package/src/lib/workflow-engine/task-reassignment.object.d.ts +26 -0
- package/src/lib/workflow-engine/task-reassignment.object.js +97 -0
- package/src/lib/workflow-engine/task-reassignment.object.js.map +1 -0
- package/src/lib/workflow-engine/workflow-dashboard-summary.object.js +18 -6
- package/src/lib/workflow-engine/workflow-dashboard-summary.object.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.enums.d.ts +29 -0
- package/src/lib/workflow-engine/workflow-engine.enums.js +42 -1
- package/src/lib/workflow-engine/workflow-engine.enums.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.module.d.ts +1 -2
- package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.mutations.d.ts +5 -0
- package/src/lib/workflow-engine/workflow-engine.mutations.js +48 -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 +15 -0
- package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +160 -1
- package/src/lib/workflow-engine/workflow-engine.service.js +749 -53
- package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.tokens.d.ts +2 -1
- package/src/lib/workflow-engine/workflow-engine.tokens.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook-options.module.d.ts +1 -2
- package/src/lib/workflow-webhook/workflow-webhook-options.module.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook.module.d.ts +1 -2
- package/src/lib/workflow-webhook/workflow-webhook.module.js.map +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var WorkflowEngineService_1;
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WorkflowEngineService = exports.MANUAL_TRANSFER_DELEGATION_REASON = exports.MAX_APPROVAL_INSTANCE_BUSINESS_KEY_LENGTH = void 0;
|
|
4
|
+
exports.WorkflowEngineService = exports.ADMIN_REASSIGN_DELEGATION_REASON = exports.MANUAL_TRANSFER_DELEGATION_REASON = exports.MAX_APPROVAL_INSTANCE_BUSINESS_KEY_LENGTH = void 0;
|
|
4
5
|
const tslib_1 = require("tslib");
|
|
5
6
|
const class_validator_1 = require("class-validator");
|
|
6
7
|
const form_1 = require("@rytass/bpm-core-shared/form");
|
|
8
|
+
const workflow_1 = require("@rytass/bpm-core-shared/workflow");
|
|
7
9
|
const workflow_graph_1 = require("@rytass/bpm-core-shared/workflow-graph");
|
|
8
10
|
const workflow_webhook_delivery_service_1 = require("../workflow-webhook/workflow-webhook-delivery.service");
|
|
9
11
|
const common_1 = require("@nestjs/common");
|
|
@@ -57,13 +59,65 @@ const SYSTEM_DRY_RUN_INSTANCE_ID = 'dry-run-instance';
|
|
|
57
59
|
* opposed to {@link SLA_ESCALATION_DELEGATION_REASON}.
|
|
58
60
|
*/
|
|
59
61
|
exports.MANUAL_TRANSFER_DELEGATION_REASON = 'MANUAL_TRANSFER';
|
|
62
|
+
/**
|
|
63
|
+
* Delegation-chain reason for a seat an administrator moved to another member
|
|
64
|
+
* (see {@link WorkflowEngineService.reassignTask}). The step's `actorMemberId`
|
|
65
|
+
* names the administrator and `comment` carries the reason they typed.
|
|
66
|
+
*/
|
|
67
|
+
exports.ADMIN_REASSIGN_DELEGATION_REASON = 'ADMIN_REASSIGN';
|
|
68
|
+
const TASK_REASSIGNMENT_MESSAGES = {
|
|
69
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.INSTANCE_NOT_RUNNING]: '案件已不在進行中',
|
|
70
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.REASON_REQUIRED]: '必須填寫改派原因',
|
|
71
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.SOURCE_NOT_ON_TASK]: '來源成員已不在此任務的待簽名單上',
|
|
72
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_ALREADY_DECIDED]: '接手人已對此任務簽過核',
|
|
73
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_ALREADY_ON_TASK]: '接手人已在此任務的待簽名單上',
|
|
74
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_GRADE_TOO_LOW]: '接手人職等不足,不能取代此簽核位置',
|
|
75
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_IS_INITIATOR]: '此節點不允許申請人自簽,接手人不能是申請人',
|
|
76
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_IS_SUBMITTER]: '此節點不允許代送人簽核,接手人不能是代為送出此單的人',
|
|
77
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_SAME_AS_SOURCE]: '接手人與原簽核人相同',
|
|
78
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TASK_NOT_OPEN]: '任務已不在待簽狀態',
|
|
79
|
+
[workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.UNEXPECTED_ERROR]: '改派時發生非預期錯誤',
|
|
80
|
+
};
|
|
81
|
+
/** Statuses in which a candidate row still holds a seat on the task. */
|
|
82
|
+
const OPEN_SEAT_STATUSES = [
|
|
83
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING,
|
|
84
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.CLAIMED,
|
|
85
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.WAITING,
|
|
86
|
+
];
|
|
87
|
+
/** Closed row states that a member can be moved back into (one row per member per task). */
|
|
88
|
+
const REUSABLE_SEAT_STATUSES = [
|
|
89
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.CANCELLED,
|
|
90
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.SUPERSEDED,
|
|
91
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.TRANSFERRED,
|
|
92
|
+
];
|
|
93
|
+
const OPEN_TASK_STATUSES = [
|
|
94
|
+
workflow_engine_enums_1.TaskStatusEnum.PENDING,
|
|
95
|
+
workflow_engine_enums_1.TaskStatusEnum.IN_PROGRESS,
|
|
96
|
+
];
|
|
97
|
+
function buildTaskReassignmentResult(task, outcome, errorCode) {
|
|
98
|
+
return {
|
|
99
|
+
errorCode,
|
|
100
|
+
instanceId: task.instanceId,
|
|
101
|
+
message: errorCode ? TASK_REASSIGNMENT_MESSAGES[errorCode] : null,
|
|
102
|
+
nodeId: task.nodeId,
|
|
103
|
+
outcome,
|
|
104
|
+
taskId: task.id,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** A refused reassignment: raised while planning, before anything is written. */
|
|
108
|
+
class TaskReassignmentRejectedError extends Error {
|
|
109
|
+
constructor(errorCode) {
|
|
110
|
+
super(TASK_REASSIGNMENT_MESSAGES[errorCode]);
|
|
111
|
+
this.errorCode = errorCode;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
60
114
|
const WORKFLOW_READ_ALL_PERMISSIONS = new Set([
|
|
61
115
|
'bpm:*',
|
|
62
116
|
'bpm.workflow.read_all',
|
|
63
117
|
'bpm:workflow:read_all',
|
|
64
118
|
'workflow.read_all',
|
|
65
119
|
]);
|
|
66
|
-
let WorkflowEngineService = class WorkflowEngineService {
|
|
120
|
+
let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineService {
|
|
67
121
|
constructor(approvalInstanceRepository, workflowTokenRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, notificationRepository, activityLogRepository, adhocDirectiveRepository, approvalTemplateRepository, approvalTemplateVersionRepository, formDefinitionVersionRepository, attachmentService, conditionService, delegationService, notificationService, signatureService, slaScheduleService, injectedServiceTaskDispatcher, formDataSourceValueResolver, rootOptions, webhookDeliveryService) {
|
|
68
122
|
this.approvalInstanceRepository = approvalInstanceRepository;
|
|
69
123
|
this.workflowTokenRepository = workflowTokenRepository;
|
|
@@ -84,6 +138,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
84
138
|
this.slaScheduleService = slaScheduleService;
|
|
85
139
|
this.formDataSourceValueResolver = formDataSourceValueResolver;
|
|
86
140
|
this.webhookDeliveryService = webhookDeliveryService;
|
|
141
|
+
this.logger = new common_1.Logger(WorkflowEngineService_1.name);
|
|
87
142
|
// A binding under the token wins: that is either the host's own provider
|
|
88
143
|
// or `workflowServiceTaskDispatcherProvider`, both of them deliberate. The
|
|
89
144
|
// runtime instance from `BPMRootModule`'s async factory comes next.
|
|
@@ -356,6 +411,24 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
356
411
|
if (!instance) {
|
|
357
412
|
throw new common_1.NotFoundException(`Approval instance ${task.instanceId} was not found`);
|
|
358
413
|
}
|
|
414
|
+
if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.TRANSFERRED &&
|
|
415
|
+
transferToMemberId !== null &&
|
|
416
|
+
isGroupDecisionPolicy(readDecisionPolicySnapshot(task)) &&
|
|
417
|
+
taskCandidates.some((candidate) => candidate.memberId === transferToMemberId &&
|
|
418
|
+
OPEN_SEAT_STATUSES.includes(candidate.status))) {
|
|
419
|
+
throw new common_1.ConflictException('Transfer target already holds an open seat on this task');
|
|
420
|
+
}
|
|
421
|
+
// A member who has decided keeps a COMPLETED row, and the database
|
|
422
|
+
// allows one row per (task, member): moving a seat to them would hit
|
|
423
|
+
// that index instead of answering why. Mirrors TARGET_ALREADY_DECIDED
|
|
424
|
+
// on the administrator reassignment.
|
|
425
|
+
if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.TRANSFERRED &&
|
|
426
|
+
transferToMemberId !== null &&
|
|
427
|
+
isGroupDecisionPolicy(readDecisionPolicySnapshot(task)) &&
|
|
428
|
+
taskCandidates.some((candidate) => candidate.memberId === transferToMemberId &&
|
|
429
|
+
candidate.status === workflow_engine_enums_1.TaskCandidateStatusEnum.COMPLETED)) {
|
|
430
|
+
throw new common_1.ConflictException('Transfer target has already decided this task');
|
|
431
|
+
}
|
|
359
432
|
if (instance.state !== workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING) {
|
|
360
433
|
throw new common_1.ConflictException(`Approval instance ${instance.id} is not running`);
|
|
361
434
|
}
|
|
@@ -449,6 +522,25 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
449
522
|
taskId: task.id,
|
|
450
523
|
transferToMemberId,
|
|
451
524
|
}));
|
|
525
|
+
if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.TRANSFERRED &&
|
|
526
|
+
transferToMemberId !== null &&
|
|
527
|
+
isGroupDecisionPolicy(readDecisionPolicySnapshot(claimedTask))) {
|
|
528
|
+
await this.transferGroupSeat(manager, {
|
|
529
|
+
actorCandidate: claimedCandidate,
|
|
530
|
+
decidedAt,
|
|
531
|
+
decision,
|
|
532
|
+
instance,
|
|
533
|
+
node: taskNode,
|
|
534
|
+
reusableRow: taskCandidates.find((candidate) => candidate.memberId === transferToMemberId &&
|
|
535
|
+
REUSABLE_SEAT_STATUSES.includes(candidate.status)) ?? null,
|
|
536
|
+
signature,
|
|
537
|
+
task: claimedTask,
|
|
538
|
+
taskBeforeClaim: task,
|
|
539
|
+
toMemberId: transferToMemberId,
|
|
540
|
+
transferReason: options.transferReason ?? exports.MANUAL_TRANSFER_DELEGATION_REASON,
|
|
541
|
+
});
|
|
542
|
+
return decision;
|
|
543
|
+
}
|
|
452
544
|
if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.TRANSFERRED) {
|
|
453
545
|
await this.transferTask(manager, instance, claimedTask, decision, signature, transferToMemberId, decisionComment, decidedAt, claimedCandidate, options.transferReason ?? exports.MANUAL_TRANSFER_DELEGATION_REASON);
|
|
454
546
|
await this.notificationService.resolveTaskNotifications({
|
|
@@ -582,21 +674,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
582
674
|
instance.state !== workflow_engine_enums_1.ApprovalInstanceStateEnum.RETURNED) {
|
|
583
675
|
throw new common_1.ConflictException(`Approval instance ${instance.id} is not cancellable`);
|
|
584
676
|
}
|
|
585
|
-
const
|
|
586
|
-
await this.consumeOpenRuntimeState(manager, instance, cancelledAt, workflow_engine_enums_1.TaskStatusEnum.CANCELLED);
|
|
587
|
-
await this.notificationService.supersedeInstanceTaskNotifications({
|
|
588
|
-
instanceId: instance.id,
|
|
589
|
-
manager,
|
|
590
|
-
});
|
|
591
|
-
// Same prototype-preserving reason as `cancelAdhocDirective`: this
|
|
592
|
-
// entity is returned to the resolver and `ApprovalInstance` exposes
|
|
593
|
-
// five getter-backed JSON fields.
|
|
594
|
-
const cancelledInstance = await instanceRepository.save(Object.assign(new approval_instance_entity_1.ApprovalInstanceEntity(), instance, {
|
|
595
|
-
completedAt: cancelledAt,
|
|
596
|
-
state: workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED,
|
|
597
|
-
}));
|
|
598
|
-
await this.dispatchAdhocCompletionNotifications(manager, cancelledInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED);
|
|
599
|
-
await this.cancelPendingAdhocDirectives(manager, instance.id, null);
|
|
677
|
+
const cancelledInstance = await this.closeInstance(manager, instance, workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED);
|
|
600
678
|
await manager.getRepository(activity_log_entity_1.ActivityLogEntity).save(manager.getRepository(activity_log_entity_1.ActivityLogEntity).create({
|
|
601
679
|
actorMemberId: input.cancelledByMemberId,
|
|
602
680
|
eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.INSTANCE_CANCELLED,
|
|
@@ -610,6 +688,67 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
610
688
|
return cancelledInstance;
|
|
611
689
|
});
|
|
612
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* Ends a running or returned instance because a task on it ran past its SLA
|
|
693
|
+
* with `onTimeout: 'TERMINATE_INSTANCE'`. It closes the instance exactly as
|
|
694
|
+
* a cancellation does, but lands in `EXPIRED` and is recorded with no actor:
|
|
695
|
+
* nobody chose to end it. An instance that is already closed — another
|
|
696
|
+
* overdue task of it got here first, or someone decided it meanwhile — is
|
|
697
|
+
* returned unchanged.
|
|
698
|
+
*/
|
|
699
|
+
async expireApprovalInstance(input) {
|
|
700
|
+
return this.approvalInstanceRepository.manager.transaction(async (manager) => {
|
|
701
|
+
await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
|
|
702
|
+
input.instanceId,
|
|
703
|
+
]);
|
|
704
|
+
const instance = await manager
|
|
705
|
+
.getRepository(approval_instance_entity_1.ApprovalInstanceEntity)
|
|
706
|
+
.findOne({ where: { id: input.instanceId } });
|
|
707
|
+
if (!instance) {
|
|
708
|
+
throw new common_1.NotFoundException(`Approval instance ${input.instanceId} was not found`);
|
|
709
|
+
}
|
|
710
|
+
if (instance.state !== workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING &&
|
|
711
|
+
instance.state !== workflow_engine_enums_1.ApprovalInstanceStateEnum.RETURNED) {
|
|
712
|
+
return instance;
|
|
713
|
+
}
|
|
714
|
+
const expiredInstance = await this.closeInstance(manager, instance, workflow_engine_enums_1.ApprovalInstanceStateEnum.EXPIRED);
|
|
715
|
+
await manager.getRepository(activity_log_entity_1.ActivityLogEntity).save(manager.getRepository(activity_log_entity_1.ActivityLogEntity).create({
|
|
716
|
+
actorMemberId: null,
|
|
717
|
+
eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.INSTANCE_EXPIRED,
|
|
718
|
+
instanceId: instance.id,
|
|
719
|
+
nodeId: input.nodeId ?? null,
|
|
720
|
+
payload: {
|
|
721
|
+
comment: input.comment?.trim() || null,
|
|
722
|
+
},
|
|
723
|
+
taskId: input.taskId ?? null,
|
|
724
|
+
}));
|
|
725
|
+
return expiredInstance;
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
// Shared ending of a cancellation and an SLA expiry: everything open on the
|
|
729
|
+
// instance is closed, its task notifications are superseded, ad-hoc
|
|
730
|
+
// completion notices go out with the final state, and what is still pending
|
|
731
|
+
// is dropped. The caller records who (if anyone) ended it.
|
|
732
|
+
async closeInstance(manager, instance, finalState) {
|
|
733
|
+
const closedAt = new Date();
|
|
734
|
+
await this.consumeOpenRuntimeState(manager, instance, closedAt, workflow_engine_enums_1.TaskStatusEnum.CANCELLED);
|
|
735
|
+
await this.notificationService.supersedeInstanceTaskNotifications({
|
|
736
|
+
instanceId: instance.id,
|
|
737
|
+
manager,
|
|
738
|
+
});
|
|
739
|
+
// Same prototype-preserving reason as `cancelAdhocDirective`: this
|
|
740
|
+
// entity is returned to the resolver and `ApprovalInstance` exposes
|
|
741
|
+
// five getter-backed JSON fields.
|
|
742
|
+
const closedInstance = await manager
|
|
743
|
+
.getRepository(approval_instance_entity_1.ApprovalInstanceEntity)
|
|
744
|
+
.save(Object.assign(new approval_instance_entity_1.ApprovalInstanceEntity(), instance, {
|
|
745
|
+
completedAt: closedAt,
|
|
746
|
+
state: finalState,
|
|
747
|
+
}));
|
|
748
|
+
await this.dispatchAdhocCompletionNotifications(manager, closedInstance, finalState);
|
|
749
|
+
await this.cancelPendingAdhocDirectives(manager, instance.id, null);
|
|
750
|
+
return closedInstance;
|
|
751
|
+
}
|
|
613
752
|
async resubmitApprovalInstance(input, authContext) {
|
|
614
753
|
const formData = parseJsonObject(input.formDataJson, 'formDataJson');
|
|
615
754
|
const preparation = this.formDataSourceValueResolver
|
|
@@ -1804,14 +1943,13 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
1804
1943
|
view: workflow_engine_enums_1.ApprovalInstanceListViewEnum.ALL,
|
|
1805
1944
|
})).filter((instance) => isInstanceWithinRange(instance, options));
|
|
1806
1945
|
const instanceIds = instances.map((instance) => instance.id);
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
: [];
|
|
1946
|
+
// The task counters answer "what is waiting on me", not "what is open on
|
|
1947
|
+
// the instances I can see": a member who can read an instance as initiator,
|
|
1948
|
+
// reader or administrator is not thereby the one who has to act on it. So
|
|
1949
|
+
// they count the member's own inbox, narrowed to the instances in range.
|
|
1950
|
+
// Without a scope there is no member to speak for, and the counters keep
|
|
1951
|
+
// describing every open task.
|
|
1952
|
+
const openTasks = await this.readDashboardOpenTasks(scope, instanceIds);
|
|
1815
1953
|
const now = Date.now();
|
|
1816
1954
|
return {
|
|
1817
1955
|
activeInstanceCount: instances.filter((instance) => instance.state === workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING).length,
|
|
@@ -1822,6 +1960,21 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
1822
1960
|
totalInstanceCount: instances.length,
|
|
1823
1961
|
};
|
|
1824
1962
|
}
|
|
1963
|
+
async readDashboardOpenTasks(scope, instanceIds) {
|
|
1964
|
+
if (!instanceIds.length) {
|
|
1965
|
+
return [];
|
|
1966
|
+
}
|
|
1967
|
+
if (scope) {
|
|
1968
|
+
const instanceIdSet = new Set(instanceIds);
|
|
1969
|
+
return (await this.readOpenInboxTasks(scope.memberId)).filter((task) => instanceIdSet.has(task.instanceId));
|
|
1970
|
+
}
|
|
1971
|
+
return this.taskRepository.find({
|
|
1972
|
+
where: {
|
|
1973
|
+
instanceId: (0, typeorm_2.In)(instanceIds),
|
|
1974
|
+
status: (0, typeorm_2.In)([workflow_engine_enums_1.TaskStatusEnum.PENDING, workflow_engine_enums_1.TaskStatusEnum.IN_PROGRESS]),
|
|
1975
|
+
},
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1825
1978
|
async listLaunchableApprovalTemplates(memberId) {
|
|
1826
1979
|
const templates = await this.approvalTemplateRepository.find({
|
|
1827
1980
|
order: { updatedAt: 'DESC' },
|
|
@@ -1869,6 +2022,20 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
1869
2022
|
}));
|
|
1870
2023
|
}
|
|
1871
2024
|
async listInboxTasks(assigneeMemberId) {
|
|
2025
|
+
return this.attachTaskCandidateSummaries(await this.readOpenInboxTasks(assigneeMemberId));
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* The open tasks that are this member's own to act on — the single
|
|
2029
|
+
* definition behind both the inbox list and the dashboard counters, so the
|
|
2030
|
+
* two can never disagree. A task is theirs when it is assigned to them, or
|
|
2031
|
+
* when they hold a PENDING or CLAIMED candidate row on it. A sequential
|
|
2032
|
+
* chain's later candidates sit in WAITING until it is their turn, so a task
|
|
2033
|
+
* that has not reached them is not theirs yet.
|
|
2034
|
+
*/
|
|
2035
|
+
async readOpenInboxTasks(assigneeMemberId, candidateStatuses = [
|
|
2036
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING,
|
|
2037
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.CLAIMED,
|
|
2038
|
+
]) {
|
|
1872
2039
|
const directTasks = await this.taskRepository.find({
|
|
1873
2040
|
order: { createdAt: 'DESC' },
|
|
1874
2041
|
where: [
|
|
@@ -1881,10 +2048,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
1881
2048
|
const candidateRows = await this.taskCandidateRepository.find({
|
|
1882
2049
|
where: {
|
|
1883
2050
|
memberId: assigneeMemberId,
|
|
1884
|
-
status: (0, typeorm_2.In)(
|
|
1885
|
-
workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING,
|
|
1886
|
-
workflow_engine_enums_1.TaskCandidateStatusEnum.CLAIMED,
|
|
1887
|
-
]),
|
|
2051
|
+
status: (0, typeorm_2.In)(candidateStatuses),
|
|
1888
2052
|
},
|
|
1889
2053
|
});
|
|
1890
2054
|
const candidateTasks = candidateRows.length
|
|
@@ -1918,7 +2082,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
1918
2082
|
})
|
|
1919
2083
|
: [];
|
|
1920
2084
|
const settledTaskIds = new Set(settledCandidateRows.map((candidate) => candidate.taskId));
|
|
1921
|
-
return
|
|
2085
|
+
return openTasks.filter((task) => !settledTaskIds.has(task.id));
|
|
1922
2086
|
}
|
|
1923
2087
|
async listApprovalHistoryTasks(assigneeMemberId) {
|
|
1924
2088
|
const directTasks = await this.taskRepository.find({
|
|
@@ -2951,6 +3115,439 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
2951
3115
|
});
|
|
2952
3116
|
}
|
|
2953
3117
|
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Administrator reassignment of one open task, moved in place: the task keeps
|
|
3120
|
+
* its id, decision policy, SLA and every other seat. Only `fromMemberId`'s
|
|
3121
|
+
* seat (and the assignee field, when it is theirs) moves to `toMemberId`.
|
|
3122
|
+
*
|
|
3123
|
+
* This is not a decision by the holder: it writes no TaskDecision and no
|
|
3124
|
+
* signature. It is recorded as a TASK_REASSIGNED activity and an
|
|
3125
|
+
* {@link ADMIN_REASSIGN_DELEGATION_REASON} delegation step.
|
|
3126
|
+
*
|
|
3127
|
+
* Unlike the holder's transfer, the target is NOT run through the delegation
|
|
3128
|
+
* rules (a rule for `toMemberId` does not apply). The initiator and submitter
|
|
3129
|
+
* exclusions and the grade check of ORG_MANAGER_CHAIN nodes still do.
|
|
3130
|
+
*
|
|
3131
|
+
* A refused reassignment is reported as `REJECTED` with an error code. When
|
|
3132
|
+
* `dryRun` is off, a refusal is thrown as a ConflictException whose message
|
|
3133
|
+
* carries that code.
|
|
3134
|
+
*/
|
|
3135
|
+
async reassignTask(taskId, options) {
|
|
3136
|
+
const task = await this.taskRepository.findOne({ where: { id: taskId } });
|
|
3137
|
+
if (!task) {
|
|
3138
|
+
throw new common_1.NotFoundException(`Task ${taskId} was not found`);
|
|
3139
|
+
}
|
|
3140
|
+
const result = await this.executeTaskReassignment(task, options);
|
|
3141
|
+
if (result.outcome === workflow_engine_enums_1.TaskReassignmentOutcomeEnum.REJECTED &&
|
|
3142
|
+
!options.dryRun) {
|
|
3143
|
+
throw new common_1.ConflictException(`[${result.errorCode}] ${result.message ?? ''}`.trim());
|
|
3144
|
+
}
|
|
3145
|
+
return result;
|
|
3146
|
+
}
|
|
3147
|
+
/**
|
|
3148
|
+
* Runs {@link reassignTask} for every open task `fromMemberId` holds, each in
|
|
3149
|
+
* its own transaction, and reports one result per task. A failing task does
|
|
3150
|
+
* not stop the others: a refusal is a REJECTED result, and an unexpected
|
|
3151
|
+
* error is reported as REJECTED with UNEXPECTED_ERROR for that task alone.
|
|
3152
|
+
*/
|
|
3153
|
+
async reassignMemberTasks(options) {
|
|
3154
|
+
const held = await this.readRunningMemberTasks(options.fromMemberId);
|
|
3155
|
+
return held.reduce(async (previous, { task }) => {
|
|
3156
|
+
const results = await previous;
|
|
3157
|
+
const result = await this.executeTaskReassignment(task, options).catch((error) => {
|
|
3158
|
+
this.logger.warn(`Reassigning task ${task.id} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3159
|
+
return buildTaskReassignmentResult(task, workflow_engine_enums_1.TaskReassignmentOutcomeEnum.REJECTED, workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.UNEXPECTED_ERROR);
|
|
3160
|
+
});
|
|
3161
|
+
return [...results, result];
|
|
3162
|
+
}, Promise.resolve([]));
|
|
3163
|
+
}
|
|
3164
|
+
/**
|
|
3165
|
+
* The open tasks a member holds, for the administrator's reassignment screen.
|
|
3166
|
+
* Same definition as the inbox, plus WAITING sequential seats, restricted to
|
|
3167
|
+
* RUNNING instances.
|
|
3168
|
+
*/
|
|
3169
|
+
async listMemberOpenTasks(memberId) {
|
|
3170
|
+
const held = await this.readRunningMemberTasks(memberId);
|
|
3171
|
+
if (!held.length) {
|
|
3172
|
+
return [];
|
|
3173
|
+
}
|
|
3174
|
+
const tasks = await this.attachTaskCandidateSummaries(held.map(({ task }) => task));
|
|
3175
|
+
const openRows = await this.taskCandidateRepository.find({
|
|
3176
|
+
where: {
|
|
3177
|
+
memberId,
|
|
3178
|
+
status: (0, typeorm_2.In)(OPEN_SEAT_STATUSES),
|
|
3179
|
+
taskId: (0, typeorm_2.In)(tasks.map((task) => task.id)),
|
|
3180
|
+
},
|
|
3181
|
+
});
|
|
3182
|
+
// One query for every template, not one per template: the listing opens
|
|
3183
|
+
// each time an administrator opens the reassignment screen.
|
|
3184
|
+
const templates = await this.approvalTemplateRepository.find({
|
|
3185
|
+
where: {
|
|
3186
|
+
id: (0, typeorm_2.In)([...new Set(held.map(({ instance }) => instance.templateId))]),
|
|
3187
|
+
},
|
|
3188
|
+
});
|
|
3189
|
+
const templateNameById = new Map(templates.map((template) => [
|
|
3190
|
+
template.id,
|
|
3191
|
+
template.name,
|
|
3192
|
+
]));
|
|
3193
|
+
const openRowByTaskId = new Map(openRows.map((row) => [
|
|
3194
|
+
row.taskId,
|
|
3195
|
+
row,
|
|
3196
|
+
]));
|
|
3197
|
+
const summaryByTaskId = new Map(tasks.map((summary) => [
|
|
3198
|
+
summary.id,
|
|
3199
|
+
summary,
|
|
3200
|
+
]));
|
|
3201
|
+
return held.map(({ instance, task }) => {
|
|
3202
|
+
const row = openRowByTaskId.get(task.id);
|
|
3203
|
+
const summary = summaryByTaskId.get(task.id);
|
|
3204
|
+
return {
|
|
3205
|
+
candidateStatus: row?.status ?? null,
|
|
3206
|
+
createdAt: task.createdAt,
|
|
3207
|
+
decisionPolicyType: readDecisionPolicySnapshot(task).type,
|
|
3208
|
+
businessKey: instance.businessKey,
|
|
3209
|
+
instanceId: instance.id,
|
|
3210
|
+
instanceTitle: instance.title,
|
|
3211
|
+
memberRole: row
|
|
3212
|
+
? workflow_engine_enums_1.MemberTaskRoleEnum.CANDIDATE
|
|
3213
|
+
: workflow_engine_enums_1.MemberTaskRoleEnum.ASSIGNEE,
|
|
3214
|
+
nodeId: task.nodeId,
|
|
3215
|
+
nodeLabel: summary?.nodeLabel ?? null,
|
|
3216
|
+
slaDueAt: task.slaDueAt,
|
|
3217
|
+
taskId: task.id,
|
|
3218
|
+
templateId: instance.templateId,
|
|
3219
|
+
templateName: templateNameById.get(instance.templateId) ?? null,
|
|
3220
|
+
};
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
/**
|
|
3224
|
+
* The open tasks `memberId` holds on RUNNING instances, with the instance
|
|
3225
|
+
* each belongs to. The instance filter is applied here, not only in the query.
|
|
3226
|
+
*/
|
|
3227
|
+
async readRunningMemberTasks(memberId) {
|
|
3228
|
+
const tasks = await this.readOpenInboxTasks(memberId, OPEN_SEAT_STATUSES);
|
|
3229
|
+
if (!tasks.length) {
|
|
3230
|
+
return [];
|
|
3231
|
+
}
|
|
3232
|
+
const instances = await this.approvalInstanceRepository.find({
|
|
3233
|
+
where: { id: (0, typeorm_2.In)([...new Set(tasks.map((task) => task.instanceId))]) },
|
|
3234
|
+
});
|
|
3235
|
+
const runningById = new Map(instances
|
|
3236
|
+
.filter((instance) => instance.state === workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING)
|
|
3237
|
+
.map((instance) => [
|
|
3238
|
+
instance.id,
|
|
3239
|
+
instance,
|
|
3240
|
+
]));
|
|
3241
|
+
return tasks.flatMap((task) => {
|
|
3242
|
+
const instance = runningById.get(task.instanceId);
|
|
3243
|
+
return instance ? [{ instance, task }] : [];
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
async executeTaskReassignment(preRead, options) {
|
|
3247
|
+
return this.approvalInstanceRepository.manager.transaction(async (manager) => {
|
|
3248
|
+
try {
|
|
3249
|
+
const plan = await this.planTaskReassignment(manager, preRead.id, options);
|
|
3250
|
+
if (options.dryRun) {
|
|
3251
|
+
return buildTaskReassignmentResult(preRead, workflow_engine_enums_1.TaskReassignmentOutcomeEnum.WOULD_REASSIGN, null);
|
|
3252
|
+
}
|
|
3253
|
+
await this.applyTaskReassignment(manager, plan, options);
|
|
3254
|
+
return buildTaskReassignmentResult(preRead, workflow_engine_enums_1.TaskReassignmentOutcomeEnum.REASSIGNED, null);
|
|
3255
|
+
}
|
|
3256
|
+
catch (error) {
|
|
3257
|
+
// Planning writes nothing, so a refusal commits no change.
|
|
3258
|
+
if (error instanceof TaskReassignmentRejectedError) {
|
|
3259
|
+
return buildTaskReassignmentResult(preRead, workflow_engine_enums_1.TaskReassignmentOutcomeEnum.REJECTED, error.errorCode);
|
|
3260
|
+
}
|
|
3261
|
+
throw error;
|
|
3262
|
+
}
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* Checks every rule of an administrator reassignment under the instance lock,
|
|
3267
|
+
* against the rows as they are now. Writes nothing; a refusal throws
|
|
3268
|
+
* {@link TaskReassignmentRejectedError}.
|
|
3269
|
+
*/
|
|
3270
|
+
async planTaskReassignment(manager, taskId, options) {
|
|
3271
|
+
const fromMemberId = options.fromMemberId;
|
|
3272
|
+
const toMemberId = options.toMemberId;
|
|
3273
|
+
if (!options.reason.trim()) {
|
|
3274
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.REASON_REQUIRED);
|
|
3275
|
+
}
|
|
3276
|
+
if (fromMemberId === toMemberId) {
|
|
3277
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_SAME_AS_SOURCE);
|
|
3278
|
+
}
|
|
3279
|
+
const taskRepository = manager.getRepository(task_entity_1.TaskEntity);
|
|
3280
|
+
const unlockedTask = await taskRepository.findOne({
|
|
3281
|
+
where: { id: taskId },
|
|
3282
|
+
});
|
|
3283
|
+
if (!unlockedTask) {
|
|
3284
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TASK_NOT_OPEN);
|
|
3285
|
+
}
|
|
3286
|
+
await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
|
|
3287
|
+
unlockedTask.instanceId,
|
|
3288
|
+
]);
|
|
3289
|
+
// Read again under the lock: the task may have been decided or moved while
|
|
3290
|
+
// this request waited for it.
|
|
3291
|
+
const task = await taskRepository.findOne({ where: { id: taskId } });
|
|
3292
|
+
if (!task || !OPEN_TASK_STATUSES.includes(task.status)) {
|
|
3293
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TASK_NOT_OPEN);
|
|
3294
|
+
}
|
|
3295
|
+
const instance = await manager
|
|
3296
|
+
.getRepository(approval_instance_entity_1.ApprovalInstanceEntity)
|
|
3297
|
+
.findOne({ where: { id: task.instanceId } });
|
|
3298
|
+
if (!instance || instance.state !== workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING) {
|
|
3299
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.INSTANCE_NOT_RUNNING);
|
|
3300
|
+
}
|
|
3301
|
+
const node = readWorkflowNodeOrThrow(instance.workflowSnapshot, task.nodeId);
|
|
3302
|
+
if (node.type !== 'userTask') {
|
|
3303
|
+
throw new common_1.ConflictException(`Task ${task.id} is not bound to a user task node`);
|
|
3304
|
+
}
|
|
3305
|
+
const candidates = await manager
|
|
3306
|
+
.getRepository(task_candidate_entity_1.TaskCandidateEntity)
|
|
3307
|
+
.find({ where: { taskId: task.id } });
|
|
3308
|
+
const sourceRow = candidates.find((candidate) => candidate.memberId === fromMemberId &&
|
|
3309
|
+
OPEN_SEAT_STATUSES.includes(candidate.status)) ?? null;
|
|
3310
|
+
// A holder with no candidate row at all is the assignee of a direct task.
|
|
3311
|
+
// Anyone with a row that is not open (COMPLETED, TRANSFERRED, ...) no
|
|
3312
|
+
// longer has a seat, so they are refused even when they are the assignee.
|
|
3313
|
+
const sourceHasAnyRow = candidates.some((candidate) => candidate.memberId === fromMemberId);
|
|
3314
|
+
if (!sourceRow &&
|
|
3315
|
+
!(task.assigneeMemberId === fromMemberId && !sourceHasAnyRow)) {
|
|
3316
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.SOURCE_NOT_ON_TASK);
|
|
3317
|
+
}
|
|
3318
|
+
const targetRows = candidates.filter((candidate) => candidate.memberId === toMemberId);
|
|
3319
|
+
const targetDecisions = await manager
|
|
3320
|
+
.getRepository(task_decision_entity_1.TaskDecisionEntity)
|
|
3321
|
+
.find({ where: { decidedByMemberId: toMemberId, taskId: task.id } });
|
|
3322
|
+
// Checked before "already on task": a member who has decided here may
|
|
3323
|
+
// still be the assignee field, and the decided reason is the useful one.
|
|
3324
|
+
if (targetDecisions.length > 0 ||
|
|
3325
|
+
targetRows.some((candidate) => candidate.status === workflow_engine_enums_1.TaskCandidateStatusEnum.COMPLETED)) {
|
|
3326
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_ALREADY_DECIDED);
|
|
3327
|
+
}
|
|
3328
|
+
if (task.assigneeMemberId === toMemberId ||
|
|
3329
|
+
targetRows.some((candidate) => OPEN_SEAT_STATUSES.includes(candidate.status))) {
|
|
3330
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_ALREADY_ON_TASK);
|
|
3331
|
+
}
|
|
3332
|
+
if (isInitiatorExcluded(node)) {
|
|
3333
|
+
if (toMemberId === instance.initiatorMemberId) {
|
|
3334
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_IS_INITIATOR);
|
|
3335
|
+
}
|
|
3336
|
+
if (toMemberId === (0, instance_submitter_1.readDistinctSubmitterMemberId)(instance)) {
|
|
3337
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_IS_SUBMITTER);
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
if (!task.isAdhoc && isOrgManagerChainNode(node)) {
|
|
3341
|
+
const requiredGrade = await this.readTransferTargetRequiredGrade(manager, {
|
|
3342
|
+
actorMemberId: fromMemberId,
|
|
3343
|
+
delegationChain: sourceRow?.delegationChain ?? task.delegationChain,
|
|
3344
|
+
targetMemberId: toMemberId,
|
|
3345
|
+
});
|
|
3346
|
+
if (requiredGrade !== null) {
|
|
3347
|
+
throw new TaskReassignmentRejectedError(workflow_engine_enums_1.TaskReassignmentErrorCodeEnum.TARGET_GRADE_TOO_LOW);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
return {
|
|
3351
|
+
instance,
|
|
3352
|
+
node,
|
|
3353
|
+
reusableRow: targetRows.find((candidate) => REUSABLE_SEAT_STATUSES.includes(candidate.status)) ?? null,
|
|
3354
|
+
sourceRow,
|
|
3355
|
+
task,
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
async applyTaskReassignment(manager, plan, options) {
|
|
3359
|
+
const decidedAt = new Date();
|
|
3360
|
+
const reason = options.reason.trim();
|
|
3361
|
+
const step = {
|
|
3362
|
+
actorMemberId: options.actorMemberId,
|
|
3363
|
+
comment: reason,
|
|
3364
|
+
from: options.fromMemberId,
|
|
3365
|
+
reason: exports.ADMIN_REASSIGN_DELEGATION_REASON,
|
|
3366
|
+
ruleId: null,
|
|
3367
|
+
to: options.toMemberId,
|
|
3368
|
+
};
|
|
3369
|
+
const seat = await this.replaceOpenSeat(manager, {
|
|
3370
|
+
decidedAt,
|
|
3371
|
+
fromMemberId: options.fromMemberId,
|
|
3372
|
+
reusableRow: plan.reusableRow,
|
|
3373
|
+
sourceRow: plan.sourceRow,
|
|
3374
|
+
step,
|
|
3375
|
+
task: plan.task,
|
|
3376
|
+
toMemberId: options.toMemberId,
|
|
3377
|
+
});
|
|
3378
|
+
const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
|
|
3379
|
+
await activityRepository.save(activityRepository.create({
|
|
3380
|
+
actorMemberId: options.actorMemberId,
|
|
3381
|
+
eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.TASK_REASSIGNED,
|
|
3382
|
+
instanceId: plan.instance.id,
|
|
3383
|
+
nodeId: plan.task.nodeId,
|
|
3384
|
+
payload: {
|
|
3385
|
+
actorMemberId: options.actorMemberId,
|
|
3386
|
+
assigneeChanged: seat.assigneeMoved,
|
|
3387
|
+
candidateStatus: plan.sourceRow?.status ?? null,
|
|
3388
|
+
fromMemberId: options.fromMemberId,
|
|
3389
|
+
reason,
|
|
3390
|
+
sequence: plan.sourceRow?.sequence ?? null,
|
|
3391
|
+
toMemberId: options.toMemberId,
|
|
3392
|
+
},
|
|
3393
|
+
taskId: plan.task.id,
|
|
3394
|
+
}));
|
|
3395
|
+
await this.notifySeatReplaced(manager, {
|
|
3396
|
+
fromMemberId: options.fromMemberId,
|
|
3397
|
+
instance: plan.instance,
|
|
3398
|
+
node: plan.node,
|
|
3399
|
+
seat,
|
|
3400
|
+
toMemberId: options.toMemberId,
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
/**
|
|
3404
|
+
* A holder's transfer of their seat on a group stage (PARALLEL_ALL,
|
|
3405
|
+
* PARALLEL_ANY, QUORUM). The stage stays open: the transferring seat moves
|
|
3406
|
+
* to the target in place, the other seats keep their state, and the task
|
|
3407
|
+
* keeps its id. Unlike transferTask, no replacement SINGLE task is created
|
|
3408
|
+
* and the other voters are not superseded. The transferring member's
|
|
3409
|
+
* decision and signature are kept on the task.
|
|
3410
|
+
*/
|
|
3411
|
+
async transferGroupSeat(manager, { actorCandidate, decidedAt, decision, instance, node, reusableRow, signature, task, taskBeforeClaim, toMemberId, transferReason, }) {
|
|
3412
|
+
const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
|
|
3413
|
+
const fromMemberId = decision.decidedByMemberId;
|
|
3414
|
+
// decideTask claims the task for whoever decides, writing them into the
|
|
3415
|
+
// task-level assignee. A seat transfer is not a claim of the whole stage:
|
|
3416
|
+
// put the task-level holder back first, so the seat replacement only moves
|
|
3417
|
+
// the assignee, the delegation chain and openedAt when the transferring
|
|
3418
|
+
// member really held the task.
|
|
3419
|
+
const unclaimedTask = await manager.getRepository(task_entity_1.TaskEntity).save(Object.assign(new task_entity_1.TaskEntity(), task, {
|
|
3420
|
+
assigneeMemberId: taskBeforeClaim.assigneeMemberId,
|
|
3421
|
+
openedAt: taskBeforeClaim.openedAt,
|
|
3422
|
+
originalAssigneeMemberId: taskBeforeClaim.originalAssigneeMemberId,
|
|
3423
|
+
}));
|
|
3424
|
+
const seat = await this.replaceOpenSeat(manager, {
|
|
3425
|
+
decidedAt,
|
|
3426
|
+
fromMemberId,
|
|
3427
|
+
reusableRow,
|
|
3428
|
+
sourceRow: actorCandidate,
|
|
3429
|
+
step: {
|
|
3430
|
+
from: fromMemberId,
|
|
3431
|
+
reason: transferReason,
|
|
3432
|
+
ruleId: null,
|
|
3433
|
+
to: toMemberId,
|
|
3434
|
+
},
|
|
3435
|
+
task: unclaimedTask,
|
|
3436
|
+
toMemberId,
|
|
3437
|
+
});
|
|
3438
|
+
await activityRepository.save(activityRepository.create({
|
|
3439
|
+
actorMemberId: fromMemberId,
|
|
3440
|
+
eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.TASK_DECIDED,
|
|
3441
|
+
instanceId: instance.id,
|
|
3442
|
+
nodeId: task.nodeId,
|
|
3443
|
+
payload: {
|
|
3444
|
+
action: workflow_engine_enums_1.TaskDecisionActionEnum.TRANSFERRED,
|
|
3445
|
+
comment: decision.comment,
|
|
3446
|
+
decisionId: decision.id,
|
|
3447
|
+
signatureId: signature.id,
|
|
3448
|
+
signedPayloadHash: signature.signedPayloadHash,
|
|
3449
|
+
transferToMemberId: toMemberId,
|
|
3450
|
+
},
|
|
3451
|
+
taskId: task.id,
|
|
3452
|
+
}));
|
|
3453
|
+
await this.notifySeatReplaced(manager, {
|
|
3454
|
+
fromMemberId,
|
|
3455
|
+
instance,
|
|
3456
|
+
node,
|
|
3457
|
+
seat,
|
|
3458
|
+
toMemberId,
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
/**
|
|
3462
|
+
* Moves one open seat from `fromMemberId` to `toMemberId` on the same task.
|
|
3463
|
+
*
|
|
3464
|
+
* With a source candidate row: that row is marked TRANSFERRED, and the target
|
|
3465
|
+
* takes a row with the same sequence, source type and original member. A
|
|
3466
|
+
* WAITING seat stays WAITING; anything else becomes PENDING. A closed row the
|
|
3467
|
+
* target already has (CANCELLED, SUPERSEDED, TRANSFERRED) is reused, because
|
|
3468
|
+
* the database allows one row per (task, member).
|
|
3469
|
+
*
|
|
3470
|
+
* Without one (a direct assignee with no candidate row) only the assignee
|
|
3471
|
+
* field moves. When the source is the assignee, the assignee moves too and
|
|
3472
|
+
* the step is appended to the task's chain.
|
|
3473
|
+
*/
|
|
3474
|
+
async replaceOpenSeat(manager, { decidedAt, fromMemberId, reusableRow, sourceRow, step, task, toMemberId, }) {
|
|
3475
|
+
const taskRepository = manager.getRepository(task_entity_1.TaskEntity);
|
|
3476
|
+
const candidateRepository = manager.getRepository(task_candidate_entity_1.TaskCandidateEntity);
|
|
3477
|
+
const assigneeMoved = task.assigneeMemberId === fromMemberId;
|
|
3478
|
+
if (sourceRow) {
|
|
3479
|
+
await candidateRepository.save(Object.assign(new task_candidate_entity_1.TaskCandidateEntity(), sourceRow, {
|
|
3480
|
+
decidedAt,
|
|
3481
|
+
status: workflow_engine_enums_1.TaskCandidateStatusEnum.TRANSFERRED,
|
|
3482
|
+
}));
|
|
3483
|
+
}
|
|
3484
|
+
const seatStatus = sourceRow
|
|
3485
|
+
? sourceRow.status === workflow_engine_enums_1.TaskCandidateStatusEnum.WAITING
|
|
3486
|
+
? workflow_engine_enums_1.TaskCandidateStatusEnum.WAITING
|
|
3487
|
+
: workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING
|
|
3488
|
+
: null;
|
|
3489
|
+
if (sourceRow) {
|
|
3490
|
+
const seat = {
|
|
3491
|
+
claimedAt: null,
|
|
3492
|
+
decidedAt: null,
|
|
3493
|
+
delegationChain: [
|
|
3494
|
+
...readDelegationSteps(sourceRow.delegationChain),
|
|
3495
|
+
step,
|
|
3496
|
+
],
|
|
3497
|
+
memberId: toMemberId,
|
|
3498
|
+
originalMemberId: sourceRow.originalMemberId,
|
|
3499
|
+
sequence: sourceRow.sequence,
|
|
3500
|
+
sourceType: sourceRow.sourceType,
|
|
3501
|
+
status: seatStatus ?? workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING,
|
|
3502
|
+
};
|
|
3503
|
+
if (reusableRow) {
|
|
3504
|
+
await candidateRepository.save(Object.assign(new task_candidate_entity_1.TaskCandidateEntity(), reusableRow, seat));
|
|
3505
|
+
}
|
|
3506
|
+
else {
|
|
3507
|
+
await candidateRepository.save(candidateRepository.create({ ...seat, taskId: task.id }));
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
const nextTask = assigneeMoved
|
|
3511
|
+
? await taskRepository.save(Object.assign(new task_entity_1.TaskEntity(), task, {
|
|
3512
|
+
assigneeMemberId: toMemberId,
|
|
3513
|
+
delegationChain: [
|
|
3514
|
+
...readDelegationSteps(task.delegationChain),
|
|
3515
|
+
step,
|
|
3516
|
+
],
|
|
3517
|
+
openedAt: null,
|
|
3518
|
+
}))
|
|
3519
|
+
: task;
|
|
3520
|
+
return { assigneeMoved, seatStatus, task: nextTask };
|
|
3521
|
+
}
|
|
3522
|
+
/**
|
|
3523
|
+
* Notifications for a replaced seat: the member who gave it up is resolved as
|
|
3524
|
+
* TRANSFERRED (their notifications on this task only), and the member who
|
|
3525
|
+
* takes it is notified now if the seat is active. A WAITING seat notifies
|
|
3526
|
+
* when its turn comes, through the normal sequential activation.
|
|
3527
|
+
*/
|
|
3528
|
+
async notifySeatReplaced(manager, { fromMemberId, instance, node, seat, toMemberId, }) {
|
|
3529
|
+
await this.notificationService.resolveTaskNotifications({
|
|
3530
|
+
actingMemberId: fromMemberId,
|
|
3531
|
+
manager,
|
|
3532
|
+
resolution: notification_enums_1.NotificationResolutionEnum.TRANSFERRED,
|
|
3533
|
+
supersedeOthers: false,
|
|
3534
|
+
taskId: seat.task.id,
|
|
3535
|
+
});
|
|
3536
|
+
const isActive = seat.seatStatus === null ||
|
|
3537
|
+
seat.seatStatus === workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING;
|
|
3538
|
+
if (!isActive) {
|
|
3539
|
+
return;
|
|
3540
|
+
}
|
|
3541
|
+
await this.notificationService.createTaskAssignedNotification({
|
|
3542
|
+
instance,
|
|
3543
|
+
manager,
|
|
3544
|
+
node,
|
|
3545
|
+
task: Object.assign(new task_entity_1.TaskEntity(), seat.task, {
|
|
3546
|
+
assigneeMemberId: toMemberId,
|
|
3547
|
+
}),
|
|
3548
|
+
transferred: true,
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
2954
3551
|
/**
|
|
2955
3552
|
* Resolves who gets the node's task. `null` means the node asked to be
|
|
2956
3553
|
* skipped (`excludeInitiator.whenEmpty = SKIP_NODE`) because the initiator
|
|
@@ -3060,17 +3657,29 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
3060
3657
|
* delegated, otherwise the actor). No grade on the principal means no
|
|
3061
3658
|
* constraint.
|
|
3062
3659
|
*/
|
|
3063
|
-
async assertTransferTargetGrade(manager,
|
|
3660
|
+
async assertTransferTargetGrade(manager, args) {
|
|
3661
|
+
const requiredLevel = await this.readTransferTargetRequiredGrade(manager, args);
|
|
3662
|
+
if (requiredLevel !== null) {
|
|
3663
|
+
throw new common_1.ForbiddenException(`簽核節點「${args.label}」需由職等 ${requiredLevel} 以上的成員簽核,不能轉簽給職等不足的成員`);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* The grade a transfer target must reach, or `null` when the target passes.
|
|
3668
|
+
* Seniority is measured from the member being replaced: the first delegator
|
|
3669
|
+
* on their chain, or that member themselves. Shared by the user transfer
|
|
3670
|
+
* (which throws) and the administrator reassignment (which reports).
|
|
3671
|
+
*/
|
|
3672
|
+
async readTransferTargetRequiredGrade(manager, { actorMemberId, delegationChain, targetMemberId, }) {
|
|
3064
3673
|
const principalMemberId = readDelegationSteps(delegationChain)[0]?.from ?? actorMemberId;
|
|
3065
3674
|
const date = this.readOrganizationToday();
|
|
3066
3675
|
const requiredLevel = await this.readMemberPositionLevel(manager, principalMemberId, date);
|
|
3067
3676
|
if (requiredLevel === null) {
|
|
3068
|
-
return;
|
|
3677
|
+
return null;
|
|
3069
3678
|
}
|
|
3070
3679
|
const targetLevel = await this.readMemberPositionLevel(manager, targetMemberId, date);
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3680
|
+
return targetLevel === null || targetLevel < requiredLevel
|
|
3681
|
+
? requiredLevel
|
|
3682
|
+
: null;
|
|
3074
3683
|
}
|
|
3075
3684
|
async resolveApproverResolver(manager, instance, resolver, label, subject = '簽核人', skippedChainMemberIds = []) {
|
|
3076
3685
|
if (resolver.type === 'DIRECT') {
|
|
@@ -3100,6 +3709,18 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
3100
3709
|
return this.resolveInitiatorManagerCandidates(manager, instance, resolver.levelsUp, resolver.fallback, label, resolver.preferClosestOrgUnit === true, subject).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
|
|
3101
3710
|
}
|
|
3102
3711
|
async resolvePositionCandidates(manager, positionId, label, subject) {
|
|
3712
|
+
const memberIds = await this.readActivePositionMemberIds(manager, positionId);
|
|
3713
|
+
if (!memberIds.length) {
|
|
3714
|
+
throw new common_1.ConflictException(`${label} 找不到${subject}:職位「${await this.readPositionName(manager, positionId)}」目前沒有任何有效成員`);
|
|
3715
|
+
}
|
|
3716
|
+
return memberIds;
|
|
3717
|
+
}
|
|
3718
|
+
/**
|
|
3719
|
+
* The members who hold the position today (organisation time zone) within
|
|
3720
|
+
* their effective period. Primary memberships come first, then the most
|
|
3721
|
+
* recently started. Empty when nobody holds it; never throws.
|
|
3722
|
+
*/
|
|
3723
|
+
async readActivePositionMemberIds(manager, positionId) {
|
|
3103
3724
|
const today = this.readOrganizationToday();
|
|
3104
3725
|
const memberships = await manager.getRepository(membership_entity_1.MembershipEntity).find({
|
|
3105
3726
|
where: { positionId },
|
|
@@ -3113,9 +3734,6 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
3113
3734
|
}
|
|
3114
3735
|
return second.effectiveFrom.localeCompare(first.effectiveFrom);
|
|
3115
3736
|
});
|
|
3116
|
-
if (!activeMemberships.length) {
|
|
3117
|
-
throw new common_1.ConflictException(`${label} 找不到${subject}:職位「${await this.readPositionName(manager, positionId)}」目前沒有任何有效成員`);
|
|
3118
|
-
}
|
|
3119
3737
|
return uniqueTexts(activeMemberships.map((membership) => membership.memberId));
|
|
3120
3738
|
}
|
|
3121
3739
|
async resolveInitiatorManagerCandidates(manager, instance, levelsUp, fallback, label, preferClosestOrgUnit, subject) {
|
|
@@ -3124,9 +3742,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
3124
3742
|
if (resolvedManagerMemberIds.length) {
|
|
3125
3743
|
return resolvedManagerMemberIds;
|
|
3126
3744
|
}
|
|
3127
|
-
return
|
|
3128
|
-
this.resolveFallbackAssignee(instance, fallback, label, subject, `找不到發起人的第 ${normalizedLevelsUp} 層主管`),
|
|
3129
|
-
];
|
|
3745
|
+
return this.resolveFallbackAssignees(manager, instance, fallback, label, subject, `找不到發起人的第 ${normalizedLevelsUp} 層主管`);
|
|
3130
3746
|
}
|
|
3131
3747
|
/**
|
|
3132
3748
|
* Walks up the initiator's manager chain, nearest manager first, until a
|
|
@@ -3177,10 +3793,10 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
3177
3793
|
return chain;
|
|
3178
3794
|
}
|
|
3179
3795
|
}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3796
|
+
// A position fallback can name several holders. They all join the last
|
|
3797
|
+
// tier, so they all have to sign, the same as every other chain member.
|
|
3798
|
+
const fallbackMemberIds = await this.resolveFallbackAssignees(manager, instance, fallback, label, subject, `沿主管鏈找不到職等達 ${minPositionLevel} 的主管`);
|
|
3799
|
+
return uniqueTexts([...chain, ...fallbackMemberIds]);
|
|
3184
3800
|
}
|
|
3185
3801
|
/** Highest position level over the member's active memberships, or `null`. */
|
|
3186
3802
|
async readMemberPositionLevel(manager, memberId, date) {
|
|
@@ -3215,11 +3831,46 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
3215
3831
|
return [memberId, levels.length === 0 ? null : Math.max(...levels)];
|
|
3216
3832
|
}));
|
|
3217
3833
|
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Every member a manager resolver's fallback yields. A `DIRECT` or `NONE`
|
|
3836
|
+
* fallback yields at most one; a `POSITION` fallback yields the position's
|
|
3837
|
+
* current holders, minus the initiator unless the fallback allows self
|
|
3838
|
+
* approval. Failure throws, as the single-member path does.
|
|
3839
|
+
*/
|
|
3840
|
+
async resolveFallbackAssignees(manager, instance, fallback, label, subject, failureReason) {
|
|
3841
|
+
if (fallback?.type !== 'POSITION') {
|
|
3842
|
+
return [
|
|
3843
|
+
this.resolveFallbackAssignee(instance, fallback, label, subject, failureReason),
|
|
3844
|
+
];
|
|
3845
|
+
}
|
|
3846
|
+
const failure = subject === '通知對象' ? '無法建立通知' : '無法建立待簽任務';
|
|
3847
|
+
if (!fallback.positionId.trim()) {
|
|
3848
|
+
throw new common_1.ConflictException(`${label} ${failure}:改派職位未設定。`);
|
|
3849
|
+
}
|
|
3850
|
+
const holderMemberIds = await this.readActivePositionMemberIds(manager, fallback.positionId);
|
|
3851
|
+
if (holderMemberIds.length === 0) {
|
|
3852
|
+
throw new common_1.ConflictException(`${label} ${failure}:${failureReason},且改派職位「${await this.readPositionName(manager, fallback.positionId)}」目前沒有任何有效成員。請聯絡流程管理員調整主管規則或模板 fallback。`);
|
|
3853
|
+
}
|
|
3854
|
+
const assignees = fallback.allowInitiatorSelfApproval
|
|
3855
|
+
? holderMemberIds
|
|
3856
|
+
: holderMemberIds.filter((memberId) => memberId !== instance.initiatorMemberId);
|
|
3857
|
+
if (assignees.length === 0) {
|
|
3858
|
+
throw new common_1.ConflictException(`${label} ${failure}:${failureReason},且改派職位的有效成員只有申請人本人,此節點未允許申請人自簽。`);
|
|
3859
|
+
}
|
|
3860
|
+
return assignees;
|
|
3861
|
+
}
|
|
3218
3862
|
resolveFallbackAssignee(instance, fallback, label, subject, failureReason) {
|
|
3219
3863
|
const failure = subject === '通知對象' ? '無法建立通知' : '無法建立待簽任務';
|
|
3220
3864
|
if (!fallback || fallback.type === 'NONE') {
|
|
3221
3865
|
throw new common_1.ConflictException(`${label} ${failure}:${failureReason},且未設定改派固定人。請聯絡流程管理員調整主管規則或模板 fallback。`);
|
|
3222
3866
|
}
|
|
3867
|
+
// A template saved by a newer engine can carry a fallback type this
|
|
3868
|
+
// engine does not know. Refuse it by name instead of reading `memberId`
|
|
3869
|
+
// off a shape it does not have, which surfaced as an opaque TypeError.
|
|
3870
|
+
const unsupportedType = (0, workflow_1.readUnsupportedApproverFallbackType)(fallback);
|
|
3871
|
+
if (unsupportedType !== null) {
|
|
3872
|
+
throw new common_1.ConflictException(`${label} ${failure}:${failureReason},且此引擎版本不支援改派類型「${unsupportedType}」。請升級 BPMCore 後再發佈或執行此流程。`);
|
|
3873
|
+
}
|
|
3223
3874
|
if (!fallback.memberId.trim()) {
|
|
3224
3875
|
throw new common_1.ConflictException(`${label} ${failure}:改派固定人未設定。`);
|
|
3225
3876
|
}
|
|
@@ -3271,9 +3922,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
3271
3922
|
scopeType: organization_enums_1.ManagerResolutionScopeTypeEnum.ORG_UNIT,
|
|
3272
3923
|
})), date, readOrgUnitDepthMap(orgUnits), preferClosestOrgUnit);
|
|
3273
3924
|
if (!managerMemberIds.length) {
|
|
3274
|
-
return
|
|
3275
|
-
this.resolveFallbackAssignee(instance, fallback, label, subject, `找不到指定組織的主管規則`),
|
|
3276
|
-
];
|
|
3925
|
+
return this.resolveFallbackAssignees(manager, instance, fallback, label, subject, `找不到指定組織的主管規則`);
|
|
3277
3926
|
}
|
|
3278
3927
|
return managerMemberIds;
|
|
3279
3928
|
}
|
|
@@ -3782,7 +4431,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
3782
4431
|
}
|
|
3783
4432
|
};
|
|
3784
4433
|
exports.WorkflowEngineService = WorkflowEngineService;
|
|
3785
|
-
exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
|
|
4434
|
+
exports.WorkflowEngineService = WorkflowEngineService = WorkflowEngineService_1 = tslib_1.__decorate([
|
|
3786
4435
|
(0, common_1.Injectable)(),
|
|
3787
4436
|
tslib_1.__param(0, (0, typeorm_1.InjectRepository)(approval_instance_entity_1.ApprovalInstanceEntity)),
|
|
3788
4437
|
tslib_1.__param(1, (0, typeorm_1.InjectRepository)(workflow_token_entity_1.WorkflowTokenEntity)),
|
|
@@ -3991,8 +4640,12 @@ function readDelegationSteps(value) {
|
|
|
3991
4640
|
if (!from || !to || !reason) {
|
|
3992
4641
|
return null;
|
|
3993
4642
|
}
|
|
4643
|
+
const actorMemberId = readStringValue(step.actorMemberId);
|
|
4644
|
+
const comment = readStringValue(step.comment);
|
|
3994
4645
|
return {
|
|
3995
4646
|
from,
|
|
4647
|
+
...(actorMemberId ? { actorMemberId } : {}),
|
|
4648
|
+
...(comment ? { comment } : {}),
|
|
3996
4649
|
reason,
|
|
3997
4650
|
ruleId: readStringValue(step.ruleId),
|
|
3998
4651
|
to,
|
|
@@ -4372,6 +5025,22 @@ function readDecisionPolicySnapshot(task) {
|
|
|
4372
5025
|
}
|
|
4373
5026
|
return { type: 'SINGLE' };
|
|
4374
5027
|
}
|
|
5028
|
+
/**
|
|
5029
|
+
* Stages where several seats decide together. A holder's transfer on one of
|
|
5030
|
+
* them moves that seat in place; SINGLE and SEQUENTIAL keep the replacement
|
|
5031
|
+
* task path.
|
|
5032
|
+
*/
|
|
5033
|
+
function isGroupDecisionPolicy(policy) {
|
|
5034
|
+
return (policy.type === 'PARALLEL_ALL' ||
|
|
5035
|
+
policy.type === 'PARALLEL_ANY' ||
|
|
5036
|
+
policy.type === 'QUORUM');
|
|
5037
|
+
}
|
|
5038
|
+
/** Candidate states in which the seat is no longer part of the task. */
|
|
5039
|
+
const SEAT_LEFT_STATUSES = [
|
|
5040
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.CANCELLED,
|
|
5041
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.SUPERSEDED,
|
|
5042
|
+
workflow_engine_enums_1.TaskCandidateStatusEnum.TRANSFERRED,
|
|
5043
|
+
];
|
|
4375
5044
|
function shouldCompleteTaskAfterDecision({ action, candidates, decisionPolicy, }) {
|
|
4376
5045
|
if (action !== workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED) {
|
|
4377
5046
|
return true;
|
|
@@ -4381,7 +5050,11 @@ function shouldCompleteTaskAfterDecision({ action, candidates, decisionPolicy, }
|
|
|
4381
5050
|
return true;
|
|
4382
5051
|
}
|
|
4383
5052
|
const completedCount = candidates.filter((candidate) => candidate.status === workflow_engine_enums_1.TaskCandidateStatusEnum.COMPLETED).length;
|
|
4384
|
-
|
|
5053
|
+
// A seat that has left the task (transferred away, cancelled, superseded)
|
|
5054
|
+
// no longer belongs to the stage. Counting it kept a PARALLEL_ALL or
|
|
5055
|
+
// SEQUENTIAL stage from ever reaching "every seat decided" once a seat had
|
|
5056
|
+
// been reassigned.
|
|
5057
|
+
const totalCount = Math.max(candidates.filter((candidate) => !SEAT_LEFT_STATUSES.includes(candidate.status)).length, 1);
|
|
4385
5058
|
if (decisionPolicy.type === 'PARALLEL_ALL' ||
|
|
4386
5059
|
decisionPolicy.type === 'SEQUENTIAL') {
|
|
4387
5060
|
return completedCount >= totalCount;
|
|
@@ -4612,6 +5285,9 @@ function readInstanceFinalStateLabel(finalState) {
|
|
|
4612
5285
|
if (finalState === workflow_engine_enums_1.ApprovalInstanceStateEnum.REJECTED) {
|
|
4613
5286
|
return '拒絕';
|
|
4614
5287
|
}
|
|
5288
|
+
if (finalState === workflow_engine_enums_1.ApprovalInstanceStateEnum.EXPIRED) {
|
|
5289
|
+
return '逾期終止';
|
|
5290
|
+
}
|
|
4615
5291
|
return '取消';
|
|
4616
5292
|
}
|
|
4617
5293
|
function readManagerMemberIdFromInitiatorSnapshot(initiatorMetadataSnapshot, levelsUp) {
|
|
@@ -5013,6 +5689,14 @@ function endSentence(message) {
|
|
|
5013
5689
|
// has no manager, or `null` when it can: like the runtime, it must exist, name
|
|
5014
5690
|
// a member, and not be the initiator unless the node allows self-approval.
|
|
5015
5691
|
function readDryRunFallbackProblem(initiatorMemberId, fallback) {
|
|
5692
|
+
if (fallback && (0, workflow_1.readUnsupportedApproverFallbackType)(fallback) !== null) {
|
|
5693
|
+
return 'UNSUPPORTED';
|
|
5694
|
+
}
|
|
5695
|
+
if (fallback?.type === 'POSITION') {
|
|
5696
|
+
// The holders are only known at run time, so the dry run cannot tell
|
|
5697
|
+
// whether the position is empty; it only rules out a blank position.
|
|
5698
|
+
return fallback.positionId.trim() === '' ? 'BLANK_POSITION' : null;
|
|
5699
|
+
}
|
|
5016
5700
|
if (!fallback || fallback.type !== 'DIRECT') {
|
|
5017
5701
|
return 'NOT_SET';
|
|
5018
5702
|
}
|
|
@@ -5025,9 +5709,15 @@ function readDryRunFallbackProblem(initiatorMemberId, fallback) {
|
|
|
5025
5709
|
: null;
|
|
5026
5710
|
}
|
|
5027
5711
|
function readDryRunFallbackMemberIds(input, fallback) {
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5712
|
+
if (readDryRunFallbackProblem(input.initiatorMemberId, fallback) !== null) {
|
|
5713
|
+
return [];
|
|
5714
|
+
}
|
|
5715
|
+
if (fallback?.type === 'DIRECT') {
|
|
5716
|
+
return [fallback.memberId.trim()];
|
|
5717
|
+
}
|
|
5718
|
+
// Same placeholder a POSITION node gets in a dry run; the label names it.
|
|
5719
|
+
return fallback?.type === 'POSITION'
|
|
5720
|
+
? [`position:${fallback.positionId.trim()}`]
|
|
5031
5721
|
: [];
|
|
5032
5722
|
}
|
|
5033
5723
|
function readDryRunNodeName(node) {
|
|
@@ -5071,6 +5761,12 @@ function readDryRunUnresolvedApproverMessage(resolver, nodeName, initiatorMember
|
|
|
5071
5761
|
if (fallbackProblem === 'IS_INITIATOR') {
|
|
5072
5762
|
return `簽核節點「${nodeName}」找不到主管,且改派固定人是申請人本人,此節點未允許申請人自簽`;
|
|
5073
5763
|
}
|
|
5764
|
+
if (fallbackProblem === 'BLANK_POSITION') {
|
|
5765
|
+
return `簽核節點「${nodeName}」找不到主管,且改派職位未設定`;
|
|
5766
|
+
}
|
|
5767
|
+
if (fallbackProblem === 'UNSUPPORTED') {
|
|
5768
|
+
return `簽核節點「${nodeName}」找不到主管,且此引擎版本不支援該改派設定,請升級 BPMCore 後再試`;
|
|
5769
|
+
}
|
|
5074
5770
|
return `簽核節點「${nodeName}」依申請人的組織找不到主管,請確認試跑時的申請人資料`;
|
|
5075
5771
|
}
|
|
5076
5772
|
function createDryRunStep(input) {
|