diff --git a/database/mssql/scripts/sampledata/dbo.ORBC_FEATURE_FLAG.Table.sql b/database/mssql/scripts/sampledata/dbo.ORBC_FEATURE_FLAG.Table.sql index e66c2e2d9..bbe9a4f38 100644 --- a/database/mssql/scripts/sampledata/dbo.ORBC_FEATURE_FLAG.Table.sql +++ b/database/mssql/scripts/sampledata/dbo.ORBC_FEATURE_FLAG.Table.sql @@ -63,8 +63,8 @@ INSERT INTO VALUES ( '3', - 'POLICY-CONFIG', - 'ENABLED', + 'VALIDATE-WITH-POLICY-ENGINE', + 'DISABLED', NULL, N'dbo', GETUTCDATE(), @@ -301,7 +301,6 @@ VALUES N'dbo', GETUTCDATE() ); - SET IDENTITY_INSERT [dbo].[ORBC_FEATURE_FLAG] OFF GO \ No newline at end of file diff --git a/vehicles/package-lock.json b/vehicles/package-lock.json index e26b59eed..20015ebae 100644 --- a/vehicles/package-lock.json +++ b/vehicles/package-lock.json @@ -39,6 +39,7 @@ "nest-winston": "^1.10.0", "nestjs-cls": "^4.5.0", "nestjs-typeorm-paginate": "^4.0.4", + "onroute-policy-engine": "^1.5.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "response-time": "^2.3.3", @@ -1912,6 +1913,28 @@ "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.1.tgz", "integrity": "sha512-Xla/d7ZMMR6+zRd6lTio0wRZECfcfFJP7GGe9A9L4tDOlD5CX4YcZ4YZle9w58bBYzssojVapI84RraKWDQZRg==" }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@ljharb/through": { "version": "2.3.13", "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", @@ -5614,6 +5637,11 @@ "node": ">=6" } }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -6513,6 +6541,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash-it": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hash-it/-/hash-it-6.0.0.tgz", + "integrity": "sha512-KHzmSFx1KwyMPw0kXeeUD752q/Kfbzhy6dAZrjXV9kAIXGqzGvv8vhkUqj+2MGZldTo0IBpw6v7iWE7uxsvH0w==" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7898,6 +7931,14 @@ "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -7921,6 +7962,28 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, + "node_modules/json-rules-engine": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/json-rules-engine/-/json-rules-engine-7.3.0.tgz", + "integrity": "sha512-Ng8Nq9sXID2h92gk3gTCB6bYK6GvQOPgxHLOIl6dEL+PE4+jvTltSOKtfYkVScTR2wL/+ts5gaQqoBFl0zK4/g==", + "dependencies": { + "clone": "^2.1.2", + "eventemitter2": "^6.4.4", + "hash-it": "^6.0.0", + "jsonpath-plus": "^10.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/json-rules-engine/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -7959,6 +8022,23 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonpath-plus": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.2.0.tgz", + "integrity": "sha512-T9V+8iNYKFL2n2rF+w02LBOT2JjDnTjioaNFrxRy0Bv1y/hNsqR/EBK7Ojy2ythRHwmz2cRIls+9JitQGZC/sw==", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -9226,6 +9306,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/onroute-policy-engine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/onroute-policy-engine/-/onroute-policy-engine-1.5.0.tgz", + "integrity": "sha512-MAbbwtyJUGssrSOepZT0XpyfIKDaCDcNH3QxeybfvnGdd4Ly0sc+WawZoxraZJyo0dVDqt/5tFs8JAdGQQCdYw==", + "dependencies": { + "dayjs": "^1.11.13", + "json-rules-engine": "^7.2.1", + "semver": "^7.6.3" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", diff --git a/vehicles/package.json b/vehicles/package.json index 17542cb4c..e37d32d51 100644 --- a/vehicles/package.json +++ b/vehicles/package.json @@ -71,6 +71,7 @@ "nest-winston": "^1.10.0", "nestjs-cls": "^4.5.0", "nestjs-typeorm-paginate": "^4.0.4", + "onroute-policy-engine": "^1.5.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "response-time": "^2.3.3", diff --git a/vehicles/src/common/enum/cache-key.enum.ts b/vehicles/src/common/enum/cache-key.enum.ts index 82100c8cc..fd58b6416 100644 --- a/vehicles/src/common/enum/cache-key.enum.ts +++ b/vehicles/src/common/enum/cache-key.enum.ts @@ -20,4 +20,6 @@ export enum CacheKey { FEATURE_FLAG_TYPE = 'FEATURE_FLAG_TYPE', PERMIT_APPLICATION_ORIGIN = 'PERMIT_APPLICATION_ORIGIN', PERMIT_APPROVAL_SOURCE = 'PERMIT_APPROVAL_SOURCE', + POLICY_CONFIGURATIONS = 'POLICY_CONFIGURATIONS', + ORBC_SERVICE_ACCOUNT_ACCESS_TOKEN = 'ORBC_SERVICE_ACCOUNT_ACCESS_TOKEN', } diff --git a/vehicles/src/common/enum/gov-common-services.enum.ts b/vehicles/src/common/enum/gov-common-services.enum.ts index 6a44a473f..acc92f93d 100644 --- a/vehicles/src/common/enum/gov-common-services.enum.ts +++ b/vehicles/src/common/enum/gov-common-services.enum.ts @@ -2,4 +2,5 @@ export enum GovCommonServices { COMMON_HOSTED_EMAIL_SERVICE = 'CHES', COMMON_DOCUMENT_GENERATION_SERVICE = 'CDOGS', CREDIT_ACCOUNT_SERVICE = 'CREDIT_ACCOUNT', + ORBC_SERVICE_ACCOUNT = 'SA', } diff --git a/vehicles/src/common/helper/gov-common-services.helper.ts b/vehicles/src/common/helper/gov-common-services.helper.ts index be81e942b..d453be518 100644 --- a/vehicles/src/common/helper/gov-common-services.helper.ts +++ b/vehicles/src/common/helper/gov-common-services.helper.ts @@ -124,7 +124,12 @@ function getTokenCredentials(govCommonServices: GovCommonServices): { username = process.env.CFS_CREDIT_ACCOUNT_CLIENT_ID; password = process.env.CFS_CREDIT_ACCOUNT_CLIENT_SECRET; break; - + case GovCommonServices.ORBC_SERVICE_ACCOUNT: + tokenCacheKey = CacheKey.ORBC_SERVICE_ACCOUNT_ACCESS_TOKEN; + tokenUrl = process.env.ORBC_SERVICE_ACCOUNT_TOKEN_URL; + username = process.env.ORBC_SERVICE_ACCOUNT_CLIENT_ID; + password = process.env.ORBC_SERVICE_ACCOUNT_CLIENT_SECRET; + break; default: break; } diff --git a/vehicles/src/common/helper/policy-engine.helper.ts b/vehicles/src/common/helper/policy-engine.helper.ts index fcf3fd3ba..8a65e1cf9 100644 --- a/vehicles/src/common/helper/policy-engine.helper.ts +++ b/vehicles/src/common/helper/policy-engine.helper.ts @@ -1,6 +1,12 @@ +import { HttpService } from '@nestjs/axios'; +import { AxiosResponse } from 'axios'; +import { Cache } from 'cache-manager'; import { Permit } from 'src/modules/permit-application-payment/permit/entities/permit.entity'; -import { PolicyApplication } from '../interface/policy-application.interface'; +import { ReadPolicyConfigDto } from '../../modules/policy/dto/response/read-policy-config.dto'; +import { GovCommonServices } from '../enum/gov-common-services.enum'; import { PermitData } from '../interface/permit.template.interface'; +import { PolicyApplication } from '../interface/policy-application.interface'; +import { getAccessToken } from './gov-common-services.helper'; export const convertToPolicyApplication = ( application: Permit, @@ -10,3 +16,32 @@ export const convertToPolicyApplication = ( permitData: JSON.parse(application.permitData.permitData) as PermitData, }; }; + +export const getActivePolicyDefinitions = async ( + httpService: HttpService, + cacheManager: Cache, +) => { + const token = await getAccessToken( + GovCommonServices.ORBC_SERVICE_ACCOUNT, + httpService, + cacheManager, + ); + const response = await httpService.axiosRef.get< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + AxiosResponse, + Request + >(process.env.ORBC_POLICY_URL + '/policy-configurations', { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + const policyConfigArray = + (await response.data.json()) as ReadPolicyConfigDto[]; + if (!policyConfigArray.length) { + return null; + } + return policyConfigArray[0]; +}; diff --git a/vehicles/src/common/logger/logger.config.ts b/vehicles/src/common/logger/logger.config.ts index b4f2604bd..6a0a933c5 100644 --- a/vehicles/src/common/logger/logger.config.ts +++ b/vehicles/src/common/logger/logger.config.ts @@ -7,7 +7,7 @@ const correlationIdFormat = winston.format((info) => { const cls = ClsServiceManager.getClsService(); const correlationId = cls.getId(); if (correlationId) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string info.message = `[${correlationId}] ${info.message}`; } return info; diff --git a/vehicles/src/modules/common/common.service.ts b/vehicles/src/modules/common/common.service.ts index 0185d60a5..0a90c5745 100644 --- a/vehicles/src/modules/common/common.service.ts +++ b/vehicles/src/modules/common/common.service.ts @@ -1,9 +1,22 @@ -import { Injectable } from '@nestjs/common'; +import { + Inject, + Injectable, + InternalServerErrorException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Country } from './entities/country.entity'; import { Province } from './entities/province.entity'; import { LogAsyncMethodExecution } from '../../common/decorator/log-async-method-execution.decorator'; +import { Permit } from '../permit-application-payment/permit/entities/permit.entity'; +import { Policy, ValidationResults } from 'onroute-policy-engine'; +import { ReadPolicyConfigDto } from '../policy/dto/response/read-policy-config.dto'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; +import { HttpService } from '@nestjs/axios'; +import { CacheKey } from '../../common/enum/cache-key.enum'; +import { getActivePolicyDefinitions } from '../../common/helper/policy-engine.helper'; +import { addToCache } from '../../common/helper/cache.helper'; @Injectable() export class CommonService { @@ -12,6 +25,9 @@ export class CommonService { private countryRepository: Repository, @InjectRepository(Province) private provinceRepository: Repository, + private readonly httpService: HttpService, + @Inject(CACHE_MANAGER) + private readonly cacheManager: Cache, ) {} @LogAsyncMethodExecution() @@ -37,4 +53,52 @@ export class CommonService { async findAllProvinces(): Promise { return await this.provinceRepository.find({}); } + + /** + * Validates a permit application using the policy engine. + * + * This method retrieves the active policy definitions to validate + * the given permit application and returns the validation results. + * + * The policy definitions are fetched from the cache if available; + * otherwise, it retrieves them from the policy service and stores + * them in the cache for future requests. + * If the policy engine is unavailable, an InternalServerErrorException + * is thrown. + * + * @param {Permit} permitApplication - The permit application to be validated. + * @returns {Promise} - The results of the validation process. + * @throws {InternalServerErrorException} - If the policy engine is not available. + */ + @LogAsyncMethodExecution() + async validateWithPolicyEngine( + permitApplication: Permit, + ): Promise { + const policyDefinitions: string = await this.cacheManager.get( + CacheKey.POLICY_CONFIGURATIONS, + ); + if (!policyDefinitions) { + const policyDefinitions = await getActivePolicyDefinitions( + this.httpService, + this.cacheManager, + ); + if (!policyDefinitions) { + throw new InternalServerErrorException( + 'Policy engine is not available', + ); + } + await addToCache( + this.cacheManager, + CacheKey.POLICY_CONFIGURATIONS, + JSON.stringify(policyDefinitions), + ); + } + const activePolicyDefintion = JSON.parse( + policyDefinitions, + ) as ReadPolicyConfigDto; + const policy = new Policy(activePolicyDefintion.policy); + const validationResults: ValidationResults = + await policy.validate(permitApplication); + return validationResults; + } } diff --git a/vehicles/src/modules/permit-application-payment/payment/payment.service.ts b/vehicles/src/modules/permit-application-payment/payment/payment.service.ts index 89d1f7700..55fc905f8 100644 --- a/vehicles/src/modules/permit-application-payment/payment/payment.service.ts +++ b/vehicles/src/modules/permit-application-payment/payment/payment.service.ts @@ -71,6 +71,7 @@ import { PermitData } from 'src/common/interface/permit.template.interface'; import { isValidLoa } from 'src/common/helper/validate-loa.helper'; import { PermitHistoryDto } from '../permit/dto/response/permit-history.dto'; import { SpecialAuthService } from 'src/modules/special-auth/special-auth.service'; +import { CommonService } from '../../common/common.service'; @Injectable() export class PaymentService { @@ -91,6 +92,7 @@ export class PaymentService { @InjectMapper() private readonly classMapper: Mapper, @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, + private readonly commonService: CommonService, ) {} private generateHashExpiry = (currDate?: Date) => { @@ -282,6 +284,10 @@ export class PaymentService { createTransactionDto.transactionTypeId == TransactionType.REFUND || createTransactionDto.paymentMethodTypeCode === PaymentMethodTypeEnum.NO_PAYMENT; + const isPolicyEngineEnabled = + featureFlags?.['VALIDATE-WITH-POLICY-ENGINE'] && + (featureFlags['VALIDATE-WITH-POLICY-ENGINE'] as FeatureFlagValue) === + FeatureFlagValue.ENABLED; // If the user is a staff user, // transacation is NOT a refund or no payment and STAFF-CAN-PAY is disabled, @@ -364,6 +370,15 @@ export class PaymentService { if (permitData.loas) { await isValidLoa(application, queryRunner, this.classMapper); } + if (isPolicyEngineEnabled) { + const validationResults = + await this.commonService.validateWithPolicyEngine(application); + if (validationResults?.violations?.length > 0) { + throw new BadRequestException( + 'Application data does not meet policy engine requirements.', + ); + } + } } const totalTransactionAmount = await this.validateApplicationAndPayment( createTransactionDto, diff --git a/vehicles/src/modules/policy/dto/response/read-policy-config.dto.ts b/vehicles/src/modules/policy/dto/response/read-policy-config.dto.ts new file mode 100644 index 000000000..21a3bf049 --- /dev/null +++ b/vehicles/src/modules/policy/dto/response/read-policy-config.dto.ts @@ -0,0 +1,54 @@ +import { AutoMap } from '@automapper/classes'; +import { ApiProperty } from '@nestjs/swagger'; +import { PolicyDefinition } from 'onroute-policy-engine/dist/types'; + +export class ReadPolicyConfigDto { + /** + * Unique identifier for the policy configuration. + */ + @AutoMap() + @ApiProperty({ + example: '1', + description: 'Unique identifier for the policy configuration.', + }) + policyConfigId: number; + + /** + * JSON data representing the policy configuration. + */ + @AutoMap() + @ApiProperty({ + description: 'Policy configuration in JSON format.', + }) + policy: PolicyDefinition; + + /** + * Configuration effective date. + */ + @AutoMap() + @ApiProperty({ + example: '2023-07-13T17:31:17.470Z', + description: 'Policy Configuration effective date.', + }) + effectiveDate: string; + + /** + * Indicates if the configuration is currently a draft version. + */ + @AutoMap() + @ApiProperty({ + example: true, + description: 'Indicates if the configuration is currently a draft.', + }) + isDraft: boolean; + + /** + * Description of changes made in the configuration. + */ + @AutoMap() + @ApiProperty({ + example: 'Initial release of policy configuration with updated rules', + description: 'Description of changes made in the configuration.', + }) + changeDescription: string; +} diff --git a/vehicles/tsconfig.json b/vehicles/tsconfig.json index fd39c3271..c153b4495 100644 --- a/vehicles/tsconfig.json +++ b/vehicles/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "commonjs", + "module": "CommonJS", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true,