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 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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "av6-core",
3
- "version": "1.7.15",
3
+ "version": "1.7.16",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",