av6-core 1.7.15 → 1.7.16
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/dist/index.d.mts +55 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +291 -0
- package/dist/index.mjs +290 -0
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -6,6 +6,7 @@ import { Readable } from 'stream';
|
|
|
6
6
|
import winston from 'winston';
|
|
7
7
|
import * as PrismaNamespace from '@prisma/client';
|
|
8
8
|
import { PrismaClient, Prisma } from '@prisma/client';
|
|
9
|
+
import EventEmitter from 'events';
|
|
9
10
|
|
|
10
11
|
declare enum ErrorMessageType {
|
|
11
12
|
INVALID_ID = "Invalid id: %1 Numeric value expected.",
|
|
@@ -527,6 +528,22 @@ declare enum ApprovalStatus {
|
|
|
527
528
|
REJECTED = 3,
|
|
528
529
|
CANCELLED = 4
|
|
529
530
|
}
|
|
531
|
+
type StepType = "MIN_MAX" | "NORMAL";
|
|
532
|
+
type ApprovalStep = {
|
|
533
|
+
id?: number;
|
|
534
|
+
flowId: number;
|
|
535
|
+
level: number;
|
|
536
|
+
minAmount?: Decimal | DecimalJsLike | number | string | null;
|
|
537
|
+
maxAmount?: Decimal | DecimalJsLike | number | string | null;
|
|
538
|
+
stepType?: StepType;
|
|
539
|
+
config?: InputJsonValue;
|
|
540
|
+
childConfig?: InputJsonValue;
|
|
541
|
+
isActive?: boolean;
|
|
542
|
+
createdBy?: number | null;
|
|
543
|
+
updatedBy?: number | null;
|
|
544
|
+
createdAt?: Date | string;
|
|
545
|
+
updatedAt?: Date | string;
|
|
546
|
+
};
|
|
530
547
|
type ApprovalInstance = {
|
|
531
548
|
id?: number;
|
|
532
549
|
flowId: number;
|
|
@@ -566,6 +583,43 @@ declare global {
|
|
|
566
583
|
"approval:REJECTED": (i: ApprovalInstance) => void;
|
|
567
584
|
}
|
|
568
585
|
}
|
|
586
|
+
interface ActInput {
|
|
587
|
+
instanceId: number;
|
|
588
|
+
approverId: number;
|
|
589
|
+
action: "APPROVE" | "REJECT";
|
|
590
|
+
ccId: number;
|
|
591
|
+
comment?: string;
|
|
592
|
+
}
|
|
593
|
+
interface StartFlowReq {
|
|
594
|
+
service: string;
|
|
595
|
+
subjectType: string;
|
|
596
|
+
subjectId: number;
|
|
597
|
+
netTotal: number;
|
|
598
|
+
ccId: number;
|
|
599
|
+
refNo: string;
|
|
600
|
+
level?: number;
|
|
601
|
+
extra?: Record<string, string | number | boolean | null>;
|
|
602
|
+
}
|
|
603
|
+
interface ApprovalDeps {
|
|
604
|
+
helpers: Helpers;
|
|
605
|
+
logger: winston.Logger;
|
|
606
|
+
requestStorage: AsyncLocalStorage<Store>;
|
|
607
|
+
prisma: PrismaClient;
|
|
608
|
+
eventBus: EventEmitter;
|
|
609
|
+
}
|
|
610
|
+
type PrismaTransactionClient = Omit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends">;
|
|
611
|
+
|
|
612
|
+
declare class ApprovalService {
|
|
613
|
+
private deps;
|
|
614
|
+
private approvalRepo;
|
|
615
|
+
constructor(deps: ApprovalDeps);
|
|
616
|
+
startFlow(tx: PrismaClient | PrismaTransactionClient, { service, subjectType, subjectId, netTotal, ccId, refNo, level, extra }: StartFlowReq): Promise<void>;
|
|
617
|
+
lastLevel(steps: ApprovalStep[]): Promise<number>;
|
|
618
|
+
/** Approver clicks “Approve” or “Reject”. */
|
|
619
|
+
act({ instanceId, approverId, action, ccId, comment }: ActInput): Promise<any>;
|
|
620
|
+
private emitEvents;
|
|
621
|
+
private assertPermission;
|
|
622
|
+
}
|
|
569
623
|
|
|
570
624
|
declare function customOmit<T extends object, K extends keyof T>(obj: T, keys: K[]): {
|
|
571
625
|
rest: Omit<T, K>;
|
|
@@ -760,4 +814,4 @@ declare class AuditProxy<Module extends string = "OPD" | "PROCEDURE" | "GENERAL_
|
|
|
760
814
|
createAuditedService<T extends object>(serviceName: string, service: T): T;
|
|
761
815
|
}
|
|
762
816
|
|
|
763
|
-
export { type AuditContext, type AuditContextProvider, AuditCore, type AuditLogPayload, AuditLogger, AuditProxy, type BulkAtomicResult, type BulkConfig, type BulkConflictConfig, type BulkOnConflict, type CacheAdapter, type CalculationRes, type ColValue, type CommonCreateRequestRepository, type CommonExcelRequest, type CommonFilterRequest, type CommonFilterWithDate, type CommonServiceResponse, type CommonUpdateRequestRepository, type Config, type Context, type CreateUINConfigRequest, type CrudContext, type CrudDelegate, type DataType, type DeepMerge, type DeleteParams, type DeleteRequestRepository, type Deps, type DropdownRequest, type DropdownRequestService, type DtoFromMapping, type DtoNullOnMissing, type DynamicCreateInput, type DynamicCrudConfig, type DynamicShortCode, type DynamicUpdateInput, type EmitPayload, type EmployeeCache, type ExcelConfig, type ExportExcel, type ExportExcelRequestService, type FetchRequest, type FetchRequestRepository, type FieldConfig, type FieldRules, type FieldType, type FixedMap, type FixedSearchRequest, type FixedSearchRequestService, type Helpers, type ImportExcel, type ImportExcelRequestService, type LockUnlockParams, type LockUnlockRequestRepository, type LogicNode, type Mapper, type MergeAll, type NewFixedSearchRequest, type NewFixedSearchRequestService, type NewSearchRequest, NotificationEmitter, type Op, type PaginatedResponse, type PathToSelectWithSelect, type PathValue, type PathsToSelectWithSelect, type Presence, type Recipient, type RelationConfig, type RelationStrategy, type RelationWriteConfig, type SearchRequest, type SearchRequestService, type ServiceCacheAdapter, type SingleValidationMapping, type SourcePath, type Store, type ToggleActive, type TxClient, type UINConfigDTO, type UINPreviewRequest, type UINSegment, type UINSegmentType, type UIN_RESET_POLICY, type UinDeps, type UnionToIntersection, type UniqueConfig, type UpdateConfigByCodeInput, type UpdateStatusRequestRepository, type UpdateUINConfigRequest, type ValidationErrorItem, commonService, convertArrayPatternToEachBlocksGeneric, customOmit, findDifferences, formatDatesDeep, fromTimestampToSqlDatetime, getDynamicValue, getNestedValue, getNestedValueV2, getPattern, interpolate, objectTo2DArray, renderEmailTemplate, renderTemplate, toNumberOrNull, toUINConfigDTO, uinConfigService, type updateStatusParams };
|
|
817
|
+
export { ApprovalService, type AuditContext, type AuditContextProvider, AuditCore, type AuditLogPayload, AuditLogger, AuditProxy, type BulkAtomicResult, type BulkConfig, type BulkConflictConfig, type BulkOnConflict, type CacheAdapter, type CalculationRes, type ColValue, type CommonCreateRequestRepository, type CommonExcelRequest, type CommonFilterRequest, type CommonFilterWithDate, type CommonServiceResponse, type CommonUpdateRequestRepository, type Config, type Context, type CreateUINConfigRequest, type CrudContext, type CrudDelegate, type DataType, type DeepMerge, type DeleteParams, type DeleteRequestRepository, type Deps, type DropdownRequest, type DropdownRequestService, type DtoFromMapping, type DtoNullOnMissing, type DynamicCreateInput, type DynamicCrudConfig, type DynamicShortCode, type DynamicUpdateInput, type EmitPayload, type EmployeeCache, type ExcelConfig, type ExportExcel, type ExportExcelRequestService, type FetchRequest, type FetchRequestRepository, type FieldConfig, type FieldRules, type FieldType, type FixedMap, type FixedSearchRequest, type FixedSearchRequestService, type Helpers, type ImportExcel, type ImportExcelRequestService, type LockUnlockParams, type LockUnlockRequestRepository, type LogicNode, type Mapper, type MergeAll, type NewFixedSearchRequest, type NewFixedSearchRequestService, type NewSearchRequest, NotificationEmitter, type Op, type PaginatedResponse, type PathToSelectWithSelect, type PathValue, type PathsToSelectWithSelect, type Presence, type Recipient, type RelationConfig, type RelationStrategy, type RelationWriteConfig, type SearchRequest, type SearchRequestService, type ServiceCacheAdapter, type SingleValidationMapping, type SourcePath, type Store, type ToggleActive, type TxClient, type UINConfigDTO, type UINPreviewRequest, type UINSegment, type UINSegmentType, type UIN_RESET_POLICY, type UinDeps, type UnionToIntersection, type UniqueConfig, type UpdateConfigByCodeInput, type UpdateStatusRequestRepository, type UpdateUINConfigRequest, type ValidationErrorItem, commonService, convertArrayPatternToEachBlocksGeneric, customOmit, findDifferences, formatDatesDeep, fromTimestampToSqlDatetime, getDynamicValue, getNestedValue, getNestedValueV2, getPattern, interpolate, objectTo2DArray, renderEmailTemplate, renderTemplate, toNumberOrNull, toUINConfigDTO, uinConfigService, type updateStatusParams };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Readable } from 'stream';
|
|
|
6
6
|
import winston from 'winston';
|
|
7
7
|
import * as PrismaNamespace from '@prisma/client';
|
|
8
8
|
import { PrismaClient, Prisma } from '@prisma/client';
|
|
9
|
+
import EventEmitter from 'events';
|
|
9
10
|
|
|
10
11
|
declare enum ErrorMessageType {
|
|
11
12
|
INVALID_ID = "Invalid id: %1 Numeric value expected.",
|
|
@@ -527,6 +528,22 @@ declare enum ApprovalStatus {
|
|
|
527
528
|
REJECTED = 3,
|
|
528
529
|
CANCELLED = 4
|
|
529
530
|
}
|
|
531
|
+
type StepType = "MIN_MAX" | "NORMAL";
|
|
532
|
+
type ApprovalStep = {
|
|
533
|
+
id?: number;
|
|
534
|
+
flowId: number;
|
|
535
|
+
level: number;
|
|
536
|
+
minAmount?: Decimal | DecimalJsLike | number | string | null;
|
|
537
|
+
maxAmount?: Decimal | DecimalJsLike | number | string | null;
|
|
538
|
+
stepType?: StepType;
|
|
539
|
+
config?: InputJsonValue;
|
|
540
|
+
childConfig?: InputJsonValue;
|
|
541
|
+
isActive?: boolean;
|
|
542
|
+
createdBy?: number | null;
|
|
543
|
+
updatedBy?: number | null;
|
|
544
|
+
createdAt?: Date | string;
|
|
545
|
+
updatedAt?: Date | string;
|
|
546
|
+
};
|
|
530
547
|
type ApprovalInstance = {
|
|
531
548
|
id?: number;
|
|
532
549
|
flowId: number;
|
|
@@ -566,6 +583,43 @@ declare global {
|
|
|
566
583
|
"approval:REJECTED": (i: ApprovalInstance) => void;
|
|
567
584
|
}
|
|
568
585
|
}
|
|
586
|
+
interface ActInput {
|
|
587
|
+
instanceId: number;
|
|
588
|
+
approverId: number;
|
|
589
|
+
action: "APPROVE" | "REJECT";
|
|
590
|
+
ccId: number;
|
|
591
|
+
comment?: string;
|
|
592
|
+
}
|
|
593
|
+
interface StartFlowReq {
|
|
594
|
+
service: string;
|
|
595
|
+
subjectType: string;
|
|
596
|
+
subjectId: number;
|
|
597
|
+
netTotal: number;
|
|
598
|
+
ccId: number;
|
|
599
|
+
refNo: string;
|
|
600
|
+
level?: number;
|
|
601
|
+
extra?: Record<string, string | number | boolean | null>;
|
|
602
|
+
}
|
|
603
|
+
interface ApprovalDeps {
|
|
604
|
+
helpers: Helpers;
|
|
605
|
+
logger: winston.Logger;
|
|
606
|
+
requestStorage: AsyncLocalStorage<Store>;
|
|
607
|
+
prisma: PrismaClient;
|
|
608
|
+
eventBus: EventEmitter;
|
|
609
|
+
}
|
|
610
|
+
type PrismaTransactionClient = Omit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends">;
|
|
611
|
+
|
|
612
|
+
declare class ApprovalService {
|
|
613
|
+
private deps;
|
|
614
|
+
private approvalRepo;
|
|
615
|
+
constructor(deps: ApprovalDeps);
|
|
616
|
+
startFlow(tx: PrismaClient | PrismaTransactionClient, { service, subjectType, subjectId, netTotal, ccId, refNo, level, extra }: StartFlowReq): Promise<void>;
|
|
617
|
+
lastLevel(steps: ApprovalStep[]): Promise<number>;
|
|
618
|
+
/** Approver clicks “Approve” or “Reject”. */
|
|
619
|
+
act({ instanceId, approverId, action, ccId, comment }: ActInput): Promise<any>;
|
|
620
|
+
private emitEvents;
|
|
621
|
+
private assertPermission;
|
|
622
|
+
}
|
|
569
623
|
|
|
570
624
|
declare function customOmit<T extends object, K extends keyof T>(obj: T, keys: K[]): {
|
|
571
625
|
rest: Omit<T, K>;
|
|
@@ -760,4 +814,4 @@ declare class AuditProxy<Module extends string = "OPD" | "PROCEDURE" | "GENERAL_
|
|
|
760
814
|
createAuditedService<T extends object>(serviceName: string, service: T): T;
|
|
761
815
|
}
|
|
762
816
|
|
|
763
|
-
export { type AuditContext, type AuditContextProvider, AuditCore, type AuditLogPayload, AuditLogger, AuditProxy, type BulkAtomicResult, type BulkConfig, type BulkConflictConfig, type BulkOnConflict, type CacheAdapter, type CalculationRes, type ColValue, type CommonCreateRequestRepository, type CommonExcelRequest, type CommonFilterRequest, type CommonFilterWithDate, type CommonServiceResponse, type CommonUpdateRequestRepository, type Config, type Context, type CreateUINConfigRequest, type CrudContext, type CrudDelegate, type DataType, type DeepMerge, type DeleteParams, type DeleteRequestRepository, type Deps, type DropdownRequest, type DropdownRequestService, type DtoFromMapping, type DtoNullOnMissing, type DynamicCreateInput, type DynamicCrudConfig, type DynamicShortCode, type DynamicUpdateInput, type EmitPayload, type EmployeeCache, type ExcelConfig, type ExportExcel, type ExportExcelRequestService, type FetchRequest, type FetchRequestRepository, type FieldConfig, type FieldRules, type FieldType, type FixedMap, type FixedSearchRequest, type FixedSearchRequestService, type Helpers, type ImportExcel, type ImportExcelRequestService, type LockUnlockParams, type LockUnlockRequestRepository, type LogicNode, type Mapper, type MergeAll, type NewFixedSearchRequest, type NewFixedSearchRequestService, type NewSearchRequest, NotificationEmitter, type Op, type PaginatedResponse, type PathToSelectWithSelect, type PathValue, type PathsToSelectWithSelect, type Presence, type Recipient, type RelationConfig, type RelationStrategy, type RelationWriteConfig, type SearchRequest, type SearchRequestService, type ServiceCacheAdapter, type SingleValidationMapping, type SourcePath, type Store, type ToggleActive, type TxClient, type UINConfigDTO, type UINPreviewRequest, type UINSegment, type UINSegmentType, type UIN_RESET_POLICY, type UinDeps, type UnionToIntersection, type UniqueConfig, type UpdateConfigByCodeInput, type UpdateStatusRequestRepository, type UpdateUINConfigRequest, type ValidationErrorItem, commonService, convertArrayPatternToEachBlocksGeneric, customOmit, findDifferences, formatDatesDeep, fromTimestampToSqlDatetime, getDynamicValue, getNestedValue, getNestedValueV2, getPattern, interpolate, objectTo2DArray, renderEmailTemplate, renderTemplate, toNumberOrNull, toUINConfigDTO, uinConfigService, type updateStatusParams };
|
|
817
|
+
export { ApprovalService, type AuditContext, type AuditContextProvider, AuditCore, type AuditLogPayload, AuditLogger, AuditProxy, type BulkAtomicResult, type BulkConfig, type BulkConflictConfig, type BulkOnConflict, type CacheAdapter, type CalculationRes, type ColValue, type CommonCreateRequestRepository, type CommonExcelRequest, type CommonFilterRequest, type CommonFilterWithDate, type CommonServiceResponse, type CommonUpdateRequestRepository, type Config, type Context, type CreateUINConfigRequest, type CrudContext, type CrudDelegate, type DataType, type DeepMerge, type DeleteParams, type DeleteRequestRepository, type Deps, type DropdownRequest, type DropdownRequestService, type DtoFromMapping, type DtoNullOnMissing, type DynamicCreateInput, type DynamicCrudConfig, type DynamicShortCode, type DynamicUpdateInput, type EmitPayload, type EmployeeCache, type ExcelConfig, type ExportExcel, type ExportExcelRequestService, type FetchRequest, type FetchRequestRepository, type FieldConfig, type FieldRules, type FieldType, type FixedMap, type FixedSearchRequest, type FixedSearchRequestService, type Helpers, type ImportExcel, type ImportExcelRequestService, type LockUnlockParams, type LockUnlockRequestRepository, type LogicNode, type Mapper, type MergeAll, type NewFixedSearchRequest, type NewFixedSearchRequestService, type NewSearchRequest, NotificationEmitter, type Op, type PaginatedResponse, type PathToSelectWithSelect, type PathValue, type PathsToSelectWithSelect, type Presence, type Recipient, type RelationConfig, type RelationStrategy, type RelationWriteConfig, type SearchRequest, type SearchRequestService, type ServiceCacheAdapter, type SingleValidationMapping, type SourcePath, type Store, type ToggleActive, type TxClient, type UINConfigDTO, type UINPreviewRequest, type UINSegment, type UINSegmentType, type UIN_RESET_POLICY, type UinDeps, type UnionToIntersection, type UniqueConfig, type UpdateConfigByCodeInput, type UpdateStatusRequestRepository, type UpdateUINConfigRequest, type ValidationErrorItem, commonService, convertArrayPatternToEachBlocksGeneric, customOmit, findDifferences, formatDatesDeep, fromTimestampToSqlDatetime, getDynamicValue, getNestedValue, getNestedValueV2, getPattern, interpolate, objectTo2DArray, renderEmailTemplate, renderTemplate, toNumberOrNull, toUINConfigDTO, uinConfigService, type updateStatusParams };
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
ApprovalService: () => ApprovalService,
|
|
33
34
|
AuditCore: () => AuditCore,
|
|
34
35
|
AuditLogger: () => AuditLogger,
|
|
35
36
|
AuditProxy: () => AuditProxy,
|
|
@@ -2300,6 +2301,295 @@ var commonService = (serviceDeps) => {
|
|
|
2300
2301
|
};
|
|
2301
2302
|
};
|
|
2302
2303
|
|
|
2304
|
+
// src/repository/approval.repository.ts
|
|
2305
|
+
var approvalRepository = (helpers) => {
|
|
2306
|
+
return {
|
|
2307
|
+
async findMatchingFlow(tx, type, service, ccId, netTotal, level = 1) {
|
|
2308
|
+
const result = await tx.$queryRaw(`
|
|
2309
|
+
SELECT af.id AS flowId,
|
|
2310
|
+
s.id AS stepId,
|
|
2311
|
+
s.level,
|
|
2312
|
+
s.min_amount AS minAmount,
|
|
2313
|
+
s.max_amount AS maxAmount,
|
|
2314
|
+
s.step_type AS stepType,
|
|
2315
|
+
af.service
|
|
2316
|
+
FROM core_approval_flow AS af
|
|
2317
|
+
JOIN core_approval_step AS s ON s.flow_id = af.id AND s.level = ${level}
|
|
2318
|
+
WHERE af.subject_type = ${type}
|
|
2319
|
+
AND af.service = ${service}
|
|
2320
|
+
AND af.is_active = TRUE
|
|
2321
|
+
AND s.is_active = TRUE
|
|
2322
|
+
AND ( (s.step_type = 'MIN_MAX'
|
|
2323
|
+
AND s.min_amount <= ${netTotal}
|
|
2324
|
+
AND s.max_amount >= ${netTotal})
|
|
2325
|
+
OR (s.step_type = 'NORMAL') )
|
|
2326
|
+
LIMIT 1; -- we expect exactly one matching step
|
|
2327
|
+
`);
|
|
2328
|
+
if (result.length === 0) {
|
|
2329
|
+
throw new helpers.ErrorHandler(400, "No matching flow found.");
|
|
2330
|
+
}
|
|
2331
|
+
return result[0];
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
};
|
|
2335
|
+
|
|
2336
|
+
// src/services/approval.service.ts
|
|
2337
|
+
var ApprovalService = class {
|
|
2338
|
+
constructor(deps) {
|
|
2339
|
+
this.deps = deps;
|
|
2340
|
+
this.approvalRepo = approvalRepository(deps.helpers);
|
|
2341
|
+
}
|
|
2342
|
+
deps;
|
|
2343
|
+
approvalRepo;
|
|
2344
|
+
async startFlow(tx, { service, subjectType, subjectId, netTotal, ccId, refNo, level = 1, extra }) {
|
|
2345
|
+
const store = this.deps.requestStorage.getStore();
|
|
2346
|
+
const currentUser = store?.user?.id;
|
|
2347
|
+
const flow = await this.approvalRepo.findMatchingFlow(tx, subjectType, service, ccId, netTotal, level);
|
|
2348
|
+
if (!flow) throw new Error("No approval flow configured");
|
|
2349
|
+
await tx.approvalInstance.updateMany({
|
|
2350
|
+
where: {
|
|
2351
|
+
service: flow.service,
|
|
2352
|
+
subjectType,
|
|
2353
|
+
subjectId
|
|
2354
|
+
},
|
|
2355
|
+
data: {
|
|
2356
|
+
isActive: false,
|
|
2357
|
+
updatedBy: currentUser
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
const inst = await tx.approvalInstance.create({
|
|
2361
|
+
data: {
|
|
2362
|
+
flowId: flow.flowId,
|
|
2363
|
+
service: flow.service,
|
|
2364
|
+
subjectType,
|
|
2365
|
+
subjectId,
|
|
2366
|
+
currentStep: flow.stepId,
|
|
2367
|
+
netTotal,
|
|
2368
|
+
refNo,
|
|
2369
|
+
extra,
|
|
2370
|
+
createdBy: currentUser
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2373
|
+
const approvers = await tx.approverMapping.findMany({
|
|
2374
|
+
where: { stepId: flow.stepId, ccId, isActive: true }
|
|
2375
|
+
});
|
|
2376
|
+
this.deps.eventBus.emit("approval:LEVEL_READY", {
|
|
2377
|
+
instanceId: inst.id,
|
|
2378
|
+
subjectType: inst.subjectType,
|
|
2379
|
+
service: inst.service,
|
|
2380
|
+
subjectId: inst.subjectId,
|
|
2381
|
+
level: 1,
|
|
2382
|
+
approvers,
|
|
2383
|
+
ccId
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
async lastLevel(steps) {
|
|
2387
|
+
if (steps.length === 0) throw new Error("No steps defined in the approval flow");
|
|
2388
|
+
return Math.max(...steps.map((s) => s.level));
|
|
2389
|
+
}
|
|
2390
|
+
/** Approver clicks “Approve” or “Reject”. */
|
|
2391
|
+
async act({ instanceId, approverId, action, ccId, comment }) {
|
|
2392
|
+
return this.deps.prisma.$transaction(async (tx) => {
|
|
2393
|
+
const inst = await tx.approvalInstance.findUnique({
|
|
2394
|
+
where: { id: instanceId },
|
|
2395
|
+
include: { flow: { where: { isActive: true }, include: { steps: { where: { isActive: true } } } } }
|
|
2396
|
+
});
|
|
2397
|
+
if (!inst) throw new this.deps.helpers.ErrorHandler(400, "Approval instance not found");
|
|
2398
|
+
if (!inst.flow) throw new this.deps.helpers.ErrorHandler(400, "Approval flow not found");
|
|
2399
|
+
const step = inst.flow.steps.find((s) => s.id === inst.currentStep);
|
|
2400
|
+
if (!step) throw new this.deps.helpers.ErrorHandler(400, "Current step not found in the flow");
|
|
2401
|
+
await this.assertPermission(step, approverId, instanceId, ccId, tx);
|
|
2402
|
+
inst.flow.steps = inst.flow.steps.filter((s) => {
|
|
2403
|
+
return s.stepType === "NORMAL" || s.stepType === "MIN_MAX" && Number(inst.netTotal) >= Number(s.minAmount) && Number(inst.netTotal) <= Number(s.maxAmount);
|
|
2404
|
+
});
|
|
2405
|
+
const lastLevel = await this.lastLevel(inst.flow.steps);
|
|
2406
|
+
const newStatus = action === "REJECT" ? 3 /* REJECTED */ : step.level === lastLevel ? 2 /* APPROVED */ : 1 /* PARTIALLY_APPROVED */;
|
|
2407
|
+
await tx.approvalAction.create({
|
|
2408
|
+
data: {
|
|
2409
|
+
instanceId,
|
|
2410
|
+
level: step.level,
|
|
2411
|
+
actedBy: approverId,
|
|
2412
|
+
comment,
|
|
2413
|
+
statusAfter: newStatus
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
let newFlow = null;
|
|
2417
|
+
if (newStatus === 1 /* PARTIALLY_APPROVED */) {
|
|
2418
|
+
newFlow = await this.approvalRepo.findMatchingFlow(
|
|
2419
|
+
tx,
|
|
2420
|
+
inst.flow.subjectType,
|
|
2421
|
+
inst.flow.service,
|
|
2422
|
+
ccId,
|
|
2423
|
+
Number(inst.netTotal || 0),
|
|
2424
|
+
step.level + 1
|
|
2425
|
+
);
|
|
2426
|
+
if (!newFlow) {
|
|
2427
|
+
throw new this.deps.helpers.ErrorHandler(400, "No next step found for the approval flow");
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const updated = await tx.approvalInstance.update({
|
|
2431
|
+
where: { id: instanceId },
|
|
2432
|
+
data: {
|
|
2433
|
+
currentStep: newStatus === 1 /* PARTIALLY_APPROVED */ ? newFlow?.stepId : step.id,
|
|
2434
|
+
status: newStatus
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2437
|
+
setImmediate(() => this.emitEvents(updated, inst.flow?.flowType, approverId, step, comment));
|
|
2438
|
+
this.deps.eventBus.emit("approval:LEVEL_DONE", {
|
|
2439
|
+
instanceId: inst.id,
|
|
2440
|
+
level: step.level,
|
|
2441
|
+
actedBy: approverId,
|
|
2442
|
+
action,
|
|
2443
|
+
comment
|
|
2444
|
+
});
|
|
2445
|
+
if (newStatus === 1 /* PARTIALLY_APPROVED */) {
|
|
2446
|
+
const nextLevel = step.level + 1;
|
|
2447
|
+
const approvers = await tx.approverMapping.findMany({
|
|
2448
|
+
where: { stepId: updated.currentStep, ccId, isActive: true }
|
|
2449
|
+
});
|
|
2450
|
+
this.deps.eventBus.emit("approval:LEVEL_READY", {
|
|
2451
|
+
instanceId: inst.id,
|
|
2452
|
+
subjectType: inst.subjectType,
|
|
2453
|
+
service: inst.service,
|
|
2454
|
+
subjectId: inst.subjectId,
|
|
2455
|
+
level: nextLevel,
|
|
2456
|
+
approvers,
|
|
2457
|
+
ccId
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
return updated;
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
/* ------------ private helpers ------------ */
|
|
2464
|
+
// private async advance(id: number, ccId: number, stepId: number, tx: PrismaClient | PrismaTransactionClient = this.deps.prisma) {
|
|
2465
|
+
// console.log(`Advancing approval instance ${id}`);
|
|
2466
|
+
// // // Fetch the approval instance by its id, including associated flow steps
|
|
2467
|
+
// const inst = await tx.approvalInstance.findUniqueOrThrow({
|
|
2468
|
+
// where: { id },
|
|
2469
|
+
// include: { flow: { include: { steps: true } } },
|
|
2470
|
+
// });
|
|
2471
|
+
// if (!inst) throw new this.deps.helpers.ErrorHandler(400, "Approval instance not found");
|
|
2472
|
+
// if (!inst.flow) throw new this.deps.helpers.ErrorHandler(400, "Approval flow not found");
|
|
2473
|
+
// // Get the current step for the instance
|
|
2474
|
+
// let nextLevel: number;
|
|
2475
|
+
// if (inst.currentLevel === 0) {
|
|
2476
|
+
// nextLevel = 1;
|
|
2477
|
+
// } else {
|
|
2478
|
+
// const currentStep = inst.flow.steps.find((s) => s.level === inst.currentLevel);
|
|
2479
|
+
// if (!currentStep) throw new Error(`Invalid level ${inst.currentLevel} for the instance`);
|
|
2480
|
+
// // If this step is done (e.g., approved or rejected), move on to the next level
|
|
2481
|
+
// nextLevel = inst.currentLevel + 1;
|
|
2482
|
+
// }
|
|
2483
|
+
// // Check if there is a next level defined
|
|
2484
|
+
// const nextStep = inst.flow.steps.find((s) => s.level === nextLevel);
|
|
2485
|
+
// if (nextStep) {
|
|
2486
|
+
// // Update the instance to move to the next level
|
|
2487
|
+
// await tx.approvalInstance.update({
|
|
2488
|
+
// where: { id },
|
|
2489
|
+
// data: {
|
|
2490
|
+
// currentLevel: nextLevel,
|
|
2491
|
+
// status: "PENDING", // Reset to "PENDING" as we are progressing the approval to the next level
|
|
2492
|
+
// },
|
|
2493
|
+
// });
|
|
2494
|
+
// const approvers = await tx.approverMapping.findMany({
|
|
2495
|
+
// where: { stepId: nextStep.id, ccId, isActive: true },
|
|
2496
|
+
// });
|
|
2497
|
+
// // Emit event for the next level approvers
|
|
2498
|
+
// this.deps.eventBus.emit("approval:LEVEL_READY", {
|
|
2499
|
+
// instanceId: inst.id,
|
|
2500
|
+
// subjectType: inst.subjectType,
|
|
2501
|
+
// subjectId: inst.subjectId,
|
|
2502
|
+
// level: nextLevel,
|
|
2503
|
+
// approvers: approvers,
|
|
2504
|
+
// ccId,
|
|
2505
|
+
// });
|
|
2506
|
+
// } else {
|
|
2507
|
+
// // If no next step, mark the instance as fully approved (completed)
|
|
2508
|
+
// await tx.approvalInstance.update({
|
|
2509
|
+
// where: { id },
|
|
2510
|
+
// data: { status: "APPROVED" }, // or REJECTED if the final level is not approved
|
|
2511
|
+
// });
|
|
2512
|
+
// // Emit event for final approval
|
|
2513
|
+
// this.deps.eventBus.emit("approval:APPROVED", {
|
|
2514
|
+
// instanceId: inst.id,
|
|
2515
|
+
// subjectType: inst.subjectType,
|
|
2516
|
+
// subjectId: inst.subjectId,
|
|
2517
|
+
// });
|
|
2518
|
+
// }
|
|
2519
|
+
// }
|
|
2520
|
+
emitEvents(instance, flowType, approverId, step, comment) {
|
|
2521
|
+
this.deps.eventBus.emit(`approval:${instance.status}`, {
|
|
2522
|
+
instanceId: instance.id,
|
|
2523
|
+
flowType,
|
|
2524
|
+
subjectId: instance.subjectId,
|
|
2525
|
+
approverId,
|
|
2526
|
+
step,
|
|
2527
|
+
comment,
|
|
2528
|
+
subjectType: instance.subjectType,
|
|
2529
|
+
service: instance.service
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
async assertPermission(step, approverId, instanceId, ccId, tx) {
|
|
2533
|
+
const result = await tx.$queryRaw(`
|
|
2534
|
+
SELECT COUNT(*) AS count
|
|
2535
|
+
FROM (
|
|
2536
|
+
SELECT am.staff_id AS staff_id
|
|
2537
|
+
FROM core_approver_mapping am
|
|
2538
|
+
WHERE am.step_id = ${step.id}
|
|
2539
|
+
AND am.is_active = TRUE
|
|
2540
|
+
AND am.cc_id = ${ccId}
|
|
2541
|
+
AND am.staff_id = ${approverId}
|
|
2542
|
+
|
|
2543
|
+
UNION
|
|
2544
|
+
|
|
2545
|
+
SELECT scc.staff_id AS staff_id
|
|
2546
|
+
FROM core_approver_mapping am
|
|
2547
|
+
LEFT JOIN staff_roles sr
|
|
2548
|
+
ON sr.role_id = am.role_id
|
|
2549
|
+
LEFT JOIN staff_collection_center scc
|
|
2550
|
+
ON scc.collection_center_id = am.cc_id
|
|
2551
|
+
AND scc.staff_id = sr.staff_id
|
|
2552
|
+
WHERE am.step_id = ${step.id}
|
|
2553
|
+
AND am.is_active = TRUE
|
|
2554
|
+
AND am.cc_id = ${ccId}
|
|
2555
|
+
AND scc.staff_id = ${approverId}
|
|
2556
|
+
) AS staff_union;
|
|
2557
|
+
`);
|
|
2558
|
+
if (Number(result[0].count) === 0) {
|
|
2559
|
+
throw new this.deps.helpers.ErrorHandler(403, "You are not allowed to act on this approval step");
|
|
2560
|
+
}
|
|
2561
|
+
const existingActsQuery = `
|
|
2562
|
+
SELECT COUNT(*) AS count
|
|
2563
|
+
FROM core_approval_action a
|
|
2564
|
+
JOIN core_approval_instance ai ON ai.id = a.instance_id
|
|
2565
|
+
WHERE ai.id = ?
|
|
2566
|
+
AND a.level = ?
|
|
2567
|
+
AND a.acted_by = ?
|
|
2568
|
+
AND a.is_active = true
|
|
2569
|
+
AND ai.is_active = true
|
|
2570
|
+
`;
|
|
2571
|
+
const actionsResult = await tx.$queryRawUnsafe(existingActsQuery, instanceId, step.level, approverId);
|
|
2572
|
+
if (Number(actionsResult[0].count) > 0) {
|
|
2573
|
+
throw new this.deps.helpers.ErrorHandler(409, "You have already submitted a decision for this level");
|
|
2574
|
+
}
|
|
2575
|
+
const prevApproversQuery = `
|
|
2576
|
+
SELECT COUNT(*) AS count
|
|
2577
|
+
FROM core_approval_action a
|
|
2578
|
+
JOIN core_approval_instance ai ON ai.id = a.instance_id
|
|
2579
|
+
WHERE ai.id = ?
|
|
2580
|
+
AND a.level < ?
|
|
2581
|
+
AND a.is_active = true
|
|
2582
|
+
AND ai.is_active = true
|
|
2583
|
+
AND a.acted_by IS NOT NULL
|
|
2584
|
+
`;
|
|
2585
|
+
const prevActionsResult = await tx.$queryRawUnsafe(prevApproversQuery, instanceId, step.level);
|
|
2586
|
+
if (Number(prevActionsResult[0].count) !== step.level - 1) {
|
|
2587
|
+
throw new this.deps.helpers.ErrorHandler(403, "You must wait for previous approvers to act first");
|
|
2588
|
+
}
|
|
2589
|
+
return step;
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
|
|
2303
2593
|
// src/utils/audit.utils.ts
|
|
2304
2594
|
function isValidDate(value) {
|
|
2305
2595
|
if (value instanceof Date) {
|
|
@@ -4294,6 +4584,7 @@ var AuditProxy = class {
|
|
|
4294
4584
|
};
|
|
4295
4585
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4296
4586
|
0 && (module.exports = {
|
|
4587
|
+
ApprovalService,
|
|
4297
4588
|
AuditCore,
|
|
4298
4589
|
AuditLogger,
|
|
4299
4590
|
AuditProxy,
|
package/dist/index.mjs
CHANGED
|
@@ -2244,6 +2244,295 @@ var commonService = (serviceDeps) => {
|
|
|
2244
2244
|
};
|
|
2245
2245
|
};
|
|
2246
2246
|
|
|
2247
|
+
// src/repository/approval.repository.ts
|
|
2248
|
+
var approvalRepository = (helpers) => {
|
|
2249
|
+
return {
|
|
2250
|
+
async findMatchingFlow(tx, type, service, ccId, netTotal, level = 1) {
|
|
2251
|
+
const result = await tx.$queryRaw(`
|
|
2252
|
+
SELECT af.id AS flowId,
|
|
2253
|
+
s.id AS stepId,
|
|
2254
|
+
s.level,
|
|
2255
|
+
s.min_amount AS minAmount,
|
|
2256
|
+
s.max_amount AS maxAmount,
|
|
2257
|
+
s.step_type AS stepType,
|
|
2258
|
+
af.service
|
|
2259
|
+
FROM core_approval_flow AS af
|
|
2260
|
+
JOIN core_approval_step AS s ON s.flow_id = af.id AND s.level = ${level}
|
|
2261
|
+
WHERE af.subject_type = ${type}
|
|
2262
|
+
AND af.service = ${service}
|
|
2263
|
+
AND af.is_active = TRUE
|
|
2264
|
+
AND s.is_active = TRUE
|
|
2265
|
+
AND ( (s.step_type = 'MIN_MAX'
|
|
2266
|
+
AND s.min_amount <= ${netTotal}
|
|
2267
|
+
AND s.max_amount >= ${netTotal})
|
|
2268
|
+
OR (s.step_type = 'NORMAL') )
|
|
2269
|
+
LIMIT 1; -- we expect exactly one matching step
|
|
2270
|
+
`);
|
|
2271
|
+
if (result.length === 0) {
|
|
2272
|
+
throw new helpers.ErrorHandler(400, "No matching flow found.");
|
|
2273
|
+
}
|
|
2274
|
+
return result[0];
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
};
|
|
2278
|
+
|
|
2279
|
+
// src/services/approval.service.ts
|
|
2280
|
+
var ApprovalService = class {
|
|
2281
|
+
constructor(deps) {
|
|
2282
|
+
this.deps = deps;
|
|
2283
|
+
this.approvalRepo = approvalRepository(deps.helpers);
|
|
2284
|
+
}
|
|
2285
|
+
deps;
|
|
2286
|
+
approvalRepo;
|
|
2287
|
+
async startFlow(tx, { service, subjectType, subjectId, netTotal, ccId, refNo, level = 1, extra }) {
|
|
2288
|
+
const store = this.deps.requestStorage.getStore();
|
|
2289
|
+
const currentUser = store?.user?.id;
|
|
2290
|
+
const flow = await this.approvalRepo.findMatchingFlow(tx, subjectType, service, ccId, netTotal, level);
|
|
2291
|
+
if (!flow) throw new Error("No approval flow configured");
|
|
2292
|
+
await tx.approvalInstance.updateMany({
|
|
2293
|
+
where: {
|
|
2294
|
+
service: flow.service,
|
|
2295
|
+
subjectType,
|
|
2296
|
+
subjectId
|
|
2297
|
+
},
|
|
2298
|
+
data: {
|
|
2299
|
+
isActive: false,
|
|
2300
|
+
updatedBy: currentUser
|
|
2301
|
+
}
|
|
2302
|
+
});
|
|
2303
|
+
const inst = await tx.approvalInstance.create({
|
|
2304
|
+
data: {
|
|
2305
|
+
flowId: flow.flowId,
|
|
2306
|
+
service: flow.service,
|
|
2307
|
+
subjectType,
|
|
2308
|
+
subjectId,
|
|
2309
|
+
currentStep: flow.stepId,
|
|
2310
|
+
netTotal,
|
|
2311
|
+
refNo,
|
|
2312
|
+
extra,
|
|
2313
|
+
createdBy: currentUser
|
|
2314
|
+
}
|
|
2315
|
+
});
|
|
2316
|
+
const approvers = await tx.approverMapping.findMany({
|
|
2317
|
+
where: { stepId: flow.stepId, ccId, isActive: true }
|
|
2318
|
+
});
|
|
2319
|
+
this.deps.eventBus.emit("approval:LEVEL_READY", {
|
|
2320
|
+
instanceId: inst.id,
|
|
2321
|
+
subjectType: inst.subjectType,
|
|
2322
|
+
service: inst.service,
|
|
2323
|
+
subjectId: inst.subjectId,
|
|
2324
|
+
level: 1,
|
|
2325
|
+
approvers,
|
|
2326
|
+
ccId
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
async lastLevel(steps) {
|
|
2330
|
+
if (steps.length === 0) throw new Error("No steps defined in the approval flow");
|
|
2331
|
+
return Math.max(...steps.map((s) => s.level));
|
|
2332
|
+
}
|
|
2333
|
+
/** Approver clicks “Approve” or “Reject”. */
|
|
2334
|
+
async act({ instanceId, approverId, action, ccId, comment }) {
|
|
2335
|
+
return this.deps.prisma.$transaction(async (tx) => {
|
|
2336
|
+
const inst = await tx.approvalInstance.findUnique({
|
|
2337
|
+
where: { id: instanceId },
|
|
2338
|
+
include: { flow: { where: { isActive: true }, include: { steps: { where: { isActive: true } } } } }
|
|
2339
|
+
});
|
|
2340
|
+
if (!inst) throw new this.deps.helpers.ErrorHandler(400, "Approval instance not found");
|
|
2341
|
+
if (!inst.flow) throw new this.deps.helpers.ErrorHandler(400, "Approval flow not found");
|
|
2342
|
+
const step = inst.flow.steps.find((s) => s.id === inst.currentStep);
|
|
2343
|
+
if (!step) throw new this.deps.helpers.ErrorHandler(400, "Current step not found in the flow");
|
|
2344
|
+
await this.assertPermission(step, approverId, instanceId, ccId, tx);
|
|
2345
|
+
inst.flow.steps = inst.flow.steps.filter((s) => {
|
|
2346
|
+
return s.stepType === "NORMAL" || s.stepType === "MIN_MAX" && Number(inst.netTotal) >= Number(s.minAmount) && Number(inst.netTotal) <= Number(s.maxAmount);
|
|
2347
|
+
});
|
|
2348
|
+
const lastLevel = await this.lastLevel(inst.flow.steps);
|
|
2349
|
+
const newStatus = action === "REJECT" ? 3 /* REJECTED */ : step.level === lastLevel ? 2 /* APPROVED */ : 1 /* PARTIALLY_APPROVED */;
|
|
2350
|
+
await tx.approvalAction.create({
|
|
2351
|
+
data: {
|
|
2352
|
+
instanceId,
|
|
2353
|
+
level: step.level,
|
|
2354
|
+
actedBy: approverId,
|
|
2355
|
+
comment,
|
|
2356
|
+
statusAfter: newStatus
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
let newFlow = null;
|
|
2360
|
+
if (newStatus === 1 /* PARTIALLY_APPROVED */) {
|
|
2361
|
+
newFlow = await this.approvalRepo.findMatchingFlow(
|
|
2362
|
+
tx,
|
|
2363
|
+
inst.flow.subjectType,
|
|
2364
|
+
inst.flow.service,
|
|
2365
|
+
ccId,
|
|
2366
|
+
Number(inst.netTotal || 0),
|
|
2367
|
+
step.level + 1
|
|
2368
|
+
);
|
|
2369
|
+
if (!newFlow) {
|
|
2370
|
+
throw new this.deps.helpers.ErrorHandler(400, "No next step found for the approval flow");
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
const updated = await tx.approvalInstance.update({
|
|
2374
|
+
where: { id: instanceId },
|
|
2375
|
+
data: {
|
|
2376
|
+
currentStep: newStatus === 1 /* PARTIALLY_APPROVED */ ? newFlow?.stepId : step.id,
|
|
2377
|
+
status: newStatus
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
setImmediate(() => this.emitEvents(updated, inst.flow?.flowType, approverId, step, comment));
|
|
2381
|
+
this.deps.eventBus.emit("approval:LEVEL_DONE", {
|
|
2382
|
+
instanceId: inst.id,
|
|
2383
|
+
level: step.level,
|
|
2384
|
+
actedBy: approverId,
|
|
2385
|
+
action,
|
|
2386
|
+
comment
|
|
2387
|
+
});
|
|
2388
|
+
if (newStatus === 1 /* PARTIALLY_APPROVED */) {
|
|
2389
|
+
const nextLevel = step.level + 1;
|
|
2390
|
+
const approvers = await tx.approverMapping.findMany({
|
|
2391
|
+
where: { stepId: updated.currentStep, ccId, isActive: true }
|
|
2392
|
+
});
|
|
2393
|
+
this.deps.eventBus.emit("approval:LEVEL_READY", {
|
|
2394
|
+
instanceId: inst.id,
|
|
2395
|
+
subjectType: inst.subjectType,
|
|
2396
|
+
service: inst.service,
|
|
2397
|
+
subjectId: inst.subjectId,
|
|
2398
|
+
level: nextLevel,
|
|
2399
|
+
approvers,
|
|
2400
|
+
ccId
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
return updated;
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
/* ------------ private helpers ------------ */
|
|
2407
|
+
// private async advance(id: number, ccId: number, stepId: number, tx: PrismaClient | PrismaTransactionClient = this.deps.prisma) {
|
|
2408
|
+
// console.log(`Advancing approval instance ${id}`);
|
|
2409
|
+
// // // Fetch the approval instance by its id, including associated flow steps
|
|
2410
|
+
// const inst = await tx.approvalInstance.findUniqueOrThrow({
|
|
2411
|
+
// where: { id },
|
|
2412
|
+
// include: { flow: { include: { steps: true } } },
|
|
2413
|
+
// });
|
|
2414
|
+
// if (!inst) throw new this.deps.helpers.ErrorHandler(400, "Approval instance not found");
|
|
2415
|
+
// if (!inst.flow) throw new this.deps.helpers.ErrorHandler(400, "Approval flow not found");
|
|
2416
|
+
// // Get the current step for the instance
|
|
2417
|
+
// let nextLevel: number;
|
|
2418
|
+
// if (inst.currentLevel === 0) {
|
|
2419
|
+
// nextLevel = 1;
|
|
2420
|
+
// } else {
|
|
2421
|
+
// const currentStep = inst.flow.steps.find((s) => s.level === inst.currentLevel);
|
|
2422
|
+
// if (!currentStep) throw new Error(`Invalid level ${inst.currentLevel} for the instance`);
|
|
2423
|
+
// // If this step is done (e.g., approved or rejected), move on to the next level
|
|
2424
|
+
// nextLevel = inst.currentLevel + 1;
|
|
2425
|
+
// }
|
|
2426
|
+
// // Check if there is a next level defined
|
|
2427
|
+
// const nextStep = inst.flow.steps.find((s) => s.level === nextLevel);
|
|
2428
|
+
// if (nextStep) {
|
|
2429
|
+
// // Update the instance to move to the next level
|
|
2430
|
+
// await tx.approvalInstance.update({
|
|
2431
|
+
// where: { id },
|
|
2432
|
+
// data: {
|
|
2433
|
+
// currentLevel: nextLevel,
|
|
2434
|
+
// status: "PENDING", // Reset to "PENDING" as we are progressing the approval to the next level
|
|
2435
|
+
// },
|
|
2436
|
+
// });
|
|
2437
|
+
// const approvers = await tx.approverMapping.findMany({
|
|
2438
|
+
// where: { stepId: nextStep.id, ccId, isActive: true },
|
|
2439
|
+
// });
|
|
2440
|
+
// // Emit event for the next level approvers
|
|
2441
|
+
// this.deps.eventBus.emit("approval:LEVEL_READY", {
|
|
2442
|
+
// instanceId: inst.id,
|
|
2443
|
+
// subjectType: inst.subjectType,
|
|
2444
|
+
// subjectId: inst.subjectId,
|
|
2445
|
+
// level: nextLevel,
|
|
2446
|
+
// approvers: approvers,
|
|
2447
|
+
// ccId,
|
|
2448
|
+
// });
|
|
2449
|
+
// } else {
|
|
2450
|
+
// // If no next step, mark the instance as fully approved (completed)
|
|
2451
|
+
// await tx.approvalInstance.update({
|
|
2452
|
+
// where: { id },
|
|
2453
|
+
// data: { status: "APPROVED" }, // or REJECTED if the final level is not approved
|
|
2454
|
+
// });
|
|
2455
|
+
// // Emit event for final approval
|
|
2456
|
+
// this.deps.eventBus.emit("approval:APPROVED", {
|
|
2457
|
+
// instanceId: inst.id,
|
|
2458
|
+
// subjectType: inst.subjectType,
|
|
2459
|
+
// subjectId: inst.subjectId,
|
|
2460
|
+
// });
|
|
2461
|
+
// }
|
|
2462
|
+
// }
|
|
2463
|
+
emitEvents(instance, flowType, approverId, step, comment) {
|
|
2464
|
+
this.deps.eventBus.emit(`approval:${instance.status}`, {
|
|
2465
|
+
instanceId: instance.id,
|
|
2466
|
+
flowType,
|
|
2467
|
+
subjectId: instance.subjectId,
|
|
2468
|
+
approverId,
|
|
2469
|
+
step,
|
|
2470
|
+
comment,
|
|
2471
|
+
subjectType: instance.subjectType,
|
|
2472
|
+
service: instance.service
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
async assertPermission(step, approverId, instanceId, ccId, tx) {
|
|
2476
|
+
const result = await tx.$queryRaw(`
|
|
2477
|
+
SELECT COUNT(*) AS count
|
|
2478
|
+
FROM (
|
|
2479
|
+
SELECT am.staff_id AS staff_id
|
|
2480
|
+
FROM core_approver_mapping am
|
|
2481
|
+
WHERE am.step_id = ${step.id}
|
|
2482
|
+
AND am.is_active = TRUE
|
|
2483
|
+
AND am.cc_id = ${ccId}
|
|
2484
|
+
AND am.staff_id = ${approverId}
|
|
2485
|
+
|
|
2486
|
+
UNION
|
|
2487
|
+
|
|
2488
|
+
SELECT scc.staff_id AS staff_id
|
|
2489
|
+
FROM core_approver_mapping am
|
|
2490
|
+
LEFT JOIN staff_roles sr
|
|
2491
|
+
ON sr.role_id = am.role_id
|
|
2492
|
+
LEFT JOIN staff_collection_center scc
|
|
2493
|
+
ON scc.collection_center_id = am.cc_id
|
|
2494
|
+
AND scc.staff_id = sr.staff_id
|
|
2495
|
+
WHERE am.step_id = ${step.id}
|
|
2496
|
+
AND am.is_active = TRUE
|
|
2497
|
+
AND am.cc_id = ${ccId}
|
|
2498
|
+
AND scc.staff_id = ${approverId}
|
|
2499
|
+
) AS staff_union;
|
|
2500
|
+
`);
|
|
2501
|
+
if (Number(result[0].count) === 0) {
|
|
2502
|
+
throw new this.deps.helpers.ErrorHandler(403, "You are not allowed to act on this approval step");
|
|
2503
|
+
}
|
|
2504
|
+
const existingActsQuery = `
|
|
2505
|
+
SELECT COUNT(*) AS count
|
|
2506
|
+
FROM core_approval_action a
|
|
2507
|
+
JOIN core_approval_instance ai ON ai.id = a.instance_id
|
|
2508
|
+
WHERE ai.id = ?
|
|
2509
|
+
AND a.level = ?
|
|
2510
|
+
AND a.acted_by = ?
|
|
2511
|
+
AND a.is_active = true
|
|
2512
|
+
AND ai.is_active = true
|
|
2513
|
+
`;
|
|
2514
|
+
const actionsResult = await tx.$queryRawUnsafe(existingActsQuery, instanceId, step.level, approverId);
|
|
2515
|
+
if (Number(actionsResult[0].count) > 0) {
|
|
2516
|
+
throw new this.deps.helpers.ErrorHandler(409, "You have already submitted a decision for this level");
|
|
2517
|
+
}
|
|
2518
|
+
const prevApproversQuery = `
|
|
2519
|
+
SELECT COUNT(*) AS count
|
|
2520
|
+
FROM core_approval_action a
|
|
2521
|
+
JOIN core_approval_instance ai ON ai.id = a.instance_id
|
|
2522
|
+
WHERE ai.id = ?
|
|
2523
|
+
AND a.level < ?
|
|
2524
|
+
AND a.is_active = true
|
|
2525
|
+
AND ai.is_active = true
|
|
2526
|
+
AND a.acted_by IS NOT NULL
|
|
2527
|
+
`;
|
|
2528
|
+
const prevActionsResult = await tx.$queryRawUnsafe(prevApproversQuery, instanceId, step.level);
|
|
2529
|
+
if (Number(prevActionsResult[0].count) !== step.level - 1) {
|
|
2530
|
+
throw new this.deps.helpers.ErrorHandler(403, "You must wait for previous approvers to act first");
|
|
2531
|
+
}
|
|
2532
|
+
return step;
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
|
|
2247
2536
|
// src/utils/audit.utils.ts
|
|
2248
2537
|
function isValidDate(value) {
|
|
2249
2538
|
if (value instanceof Date) {
|
|
@@ -4237,6 +4526,7 @@ var AuditProxy = class {
|
|
|
4237
4526
|
}
|
|
4238
4527
|
};
|
|
4239
4528
|
export {
|
|
4529
|
+
ApprovalService,
|
|
4240
4530
|
AuditCore,
|
|
4241
4531
|
AuditLogger,
|
|
4242
4532
|
AuditProxy,
|