diff --git a/policy-engine/.npmignore b/policy-engine/.npmignore index 801be9579..246d8794d 100644 --- a/policy-engine/.npmignore +++ b/policy-engine/.npmignore @@ -5,6 +5,7 @@ coverage/ .eslintignore .eslintrc.cjs dist/_test/ +dist/_examples/ dist/tsconfig.tsbuildinfo tsconfig.tsbuildinfo .prettierrc.json diff --git a/policy-engine/package-lock.json b/policy-engine/package-lock.json index 187c76790..4bf90bb69 100644 --- a/policy-engine/package-lock.json +++ b/policy-engine/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "onroute-policy-engine", "version": "0.1.0", + "license": "Apache-2.0", "dependencies": { "dayjs": "^1.11.10", "flattie": "^1.1.1", @@ -598,6 +599,32 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1248,6 +1275,38 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1715,6 +1774,20 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1783,6 +1856,14 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2168,6 +2249,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2242,6 +2331,17 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -5069,6 +5169,51 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5160,6 +5305,14 @@ "punycode": "^2.1.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -5294,6 +5447,17 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/policy-engine/src/_examples/calculate-cost.ts b/policy-engine/src/_examples/calculate-cost.ts new file mode 100644 index 000000000..809395471 --- /dev/null +++ b/policy-engine/src/_examples/calculate-cost.ts @@ -0,0 +1,24 @@ +import { Policy } from 'onroute-policy-engine'; +import { PermitAppInfo } from 'onroute-policy-engine/enum'; +import { masterPolicyConfig } from '../_test/policy-config/master.sample'; +import { validTros30Day } from '../_test/permit-app/valid-tros-30day'; +import dayjs from 'dayjs'; + +async function start() { + const policy: Policy = new Policy(masterPolicyConfig); + const today = dayjs(); + + // Set startDate to today + validTros30Day.permitData.startDate = today.format( + PermitAppInfo.PermitDateFormat.toString(), + ); + // Set duration to full year (365 or 366 depending on leap year) + const oneYearDuration: number = today.add(1, 'year').diff(today, 'day'); + console.log('Setting TROS permit duration to ' + oneYearDuration); + validTros30Day.permitData.permitDuration = oneYearDuration; + + const validationResult2 = await policy.validate(validTros30Day); + console.log(JSON.stringify(validationResult2, null, ' ')); +} + +start(); diff --git a/policy-engine/src/_examples/get-policy-details.ts b/policy-engine/src/_examples/get-policy-details.ts new file mode 100644 index 000000000..176352f56 --- /dev/null +++ b/policy-engine/src/_examples/get-policy-details.ts @@ -0,0 +1,72 @@ +import { Policy } from 'onroute-policy-engine'; +import { completePolicyConfig } from '../_test/policy-config/complete-in-progress.sample'; + +function start() { + const policy: Policy = new Policy(completePolicyConfig); + + console.log('***ALL COMMODITIES***'); + const allCommodities = policy.getCommodities(); + console.log( + JSON.stringify(Array.from(allCommodities.entries()), null, ' '), + ); + + console.log('***COMMODITIES FOR STOS***'); + const stosCommodities = policy.getCommodities('STOS'); + console.log( + JSON.stringify(Array.from(stosCommodities.entries()), null, ' '), + ); + + console.log('***POWER UNITS PERMITTABLE FOR STOS AND EMPTY COMMODITY***'); + const puTypesEmpty = policy.getPermittablePowerUnitTypes('STOS', 'EMPTYXX'); + console.log(JSON.stringify(Array.from(puTypesEmpty.entries()), null, ' ')); + + console.log( + '***POWER UNITS PERMITTABLE FOR STOS AND BRIDGE BEAMS COMMODITY***', + ); + const puTypesBridgeBeams = policy.getPermittablePowerUnitTypes( + 'STOS', + 'BRGBEAM', + ); + console.log( + JSON.stringify(Array.from(puTypesBridgeBeams.entries()), null, ' '), + ); + + console.log( + '***PERMITTABLE NEXT VEHICLES WITH EMPTY CONFIGURATION, STOS AND EMPTY***', + ); + const vehicleTypes1 = policy.getNextPermittableVehicles( + 'STOS', + 'EMPTYXX', + [], + ); + console.log(JSON.stringify(Array.from(vehicleTypes1.entries()), null, ' ')); + + console.log( + '***PERMITTABLE NEXT VEHICLES WITH TRUCK TRACTOR AND JEEP, STOS AND EMPTY***', + ); + const vehicleTypes2 = policy.getNextPermittableVehicles('STOS', 'EMPTYXX', [ + 'TRKTRAC', + 'JEEPSRG', + ]); + console.log(JSON.stringify(Array.from(vehicleTypes2.entries()), null, ' ')); + + console.log( + '***MAX SIZE FOR TRUCK TRACTOR, JEEP, HIBOEXP, STOS AND EMPTYXX***', + ); + const sizeDimension = policy.getSizeDimension('STOS', 'EMPTYXX', ['TRKTRAC', 'JEEPSRG', 'HIBOEXP']); + console.log(JSON.stringify(sizeDimension, null, ' ')); + + console.log( + '***MAX SIZE FOR TRUCK TRACTOR, JEEP, HIBOEXP, STOS AND EMPTYXX IN PEACE***', + ); + const sizeDimensionPeace = policy.getSizeDimension('STOS', 'EMPTYXX', ['TRKTRAC', 'JEEPSRG', 'HIBOEXP'], ['PCE']); + console.log(JSON.stringify(sizeDimensionPeace, null, ' ')); + + console.log( + '***MAX SIZE FOR TRUCK TRACTOR, JEEP, HIBOEXP, STOS AND EMPTYXX IN PEACE,BC DEFAULT***', + ); + const sizeDimensionPeaceBC = policy.getSizeDimension('STOS', 'EMPTYXX', ['TRKTRAC', 'JEEPSRG', 'HIBOEXP'], ['PCE','BCD']); + console.log(JSON.stringify(sizeDimensionPeaceBC, null, ' ')); +} + +start(); diff --git a/policy-engine/src/_examples/validate-stos.ts b/policy-engine/src/_examples/validate-stos.ts new file mode 100644 index 000000000..d948dd2dd --- /dev/null +++ b/policy-engine/src/_examples/validate-stos.ts @@ -0,0 +1,19 @@ +import { Policy } from 'onroute-policy-engine'; +import { PermitAppInfo } from 'onroute-policy-engine/enum'; +import { completePolicyConfig } from '../_test/policy-config/complete-in-progress.sample'; +import { testStos } from '../_test/permit-app/test-stos'; +import dayjs from 'dayjs'; + +async function start() { + const policy: Policy = new Policy(completePolicyConfig); + + // Set startDate to today + testStos.permitData.startDate = dayjs().format( + PermitAppInfo.PermitDateFormat.toString(), + ); + + const validationResult2 = await policy.validate(testStos); + console.log(JSON.stringify(validationResult2, null, ' ')); +} + +start(); diff --git a/policy-engine/src/_examples/validate-tros.ts b/policy-engine/src/_examples/validate-tros.ts new file mode 100644 index 000000000..6dd4df833 --- /dev/null +++ b/policy-engine/src/_examples/validate-tros.ts @@ -0,0 +1,19 @@ +import { Policy } from 'onroute-policy-engine'; +import { PermitAppInfo } from 'onroute-policy-engine/enum'; +import { masterPolicyConfig } from '../_test/policy-config/master.sample'; +import { validTros30Day } from '../_test/permit-app/valid-tros-30day'; +import dayjs from 'dayjs'; + +async function start() { + const policy: Policy = new Policy(masterPolicyConfig); + + // Set startDate to today + validTros30Day.permitData.startDate = dayjs().format( + PermitAppInfo.PermitDateFormat.toString(), + ); + + const validationResult2 = await policy.validate(validTros30Day); + console.log(JSON.stringify(validationResult2, null, ' ')); +} + +start(); diff --git a/policy-engine/src/_test/permit-app/test-stos.ts b/policy-engine/src/_test/permit-app/test-stos.ts new file mode 100644 index 000000000..37d92e9f0 --- /dev/null +++ b/policy-engine/src/_test/permit-app/test-stos.ts @@ -0,0 +1,63 @@ +import PermitApplication from './permit-application.type'; + +export const testStos: PermitApplication = { + permitType: 'STOS', + permitData: { + companyName: 'Sonic Delivery Services', + clientNumber: 'B3-000102-466', + permitDuration: 7, + commodities: [ + { + description: 'General Permit Conditions', + condition: 'CVSE-1000', + conditionLink: + 'https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1251', + checked: true, + disabled: true, + }, + { + description: 'Permit Scope and Limitation', + condition: 'CVSE-1070', + conditionLink: + 'https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1261', + checked: true, + disabled: true, + }, + ], + contactDetails: { + firstName: 'Chief', + lastName: 'Baker', + phone1: '(250) 555-1234', + phone1Extension: null, + phone2: '(250) 555-4321', + phone2Extension: null, + email: 'chief.baker@gov.bc.ca', + additionalEmail: 'baker.chief@gov.bc.ca', + fax: null, + }, + mailingAddress: { + addressLine1: '940 Blanshard', + addressLine2: null, + city: 'Victoria', + provinceCode: 'BC', + countryCode: 'CA', + postalCode: 'V8B1A2', + }, + vehicleDetails: { + vehicleId: '101', + unitNumber: '321', + vin: '654321', + plate: 'D654321', + make: 'Custom', + year: 2010, + countryCode: 'CA', + provinceCode: 'BC', + vehicleType: 'powerUnit', + vehicleSubType: 'TRKTRAC', + saveVehicle: false, + }, + feeSummary: '30', + startDate: '2024-04-18', + expiryDate: '2024-05-17', + }, +}; diff --git a/policy-engine/src/_test/policy-config/complete-in-progress.sample.ts b/policy-engine/src/_test/policy-config/complete-in-progress.sample.ts new file mode 100644 index 000000000..f98254256 --- /dev/null +++ b/policy-engine/src/_test/policy-config/complete-in-progress.sample.ts @@ -0,0 +1,1896 @@ +import { PolicyDefinition } from '../../types/policy-definition'; + +export const completePolicyConfig: PolicyDefinition = { + version: '2024.03.18.001', + geographicRegions: [ + { + id: 'LMN', + name: 'Lower Mainland', + }, + { + id: 'KTN', + name: 'Kootenay', + }, + { + id: 'PCE', + name: 'Peace', + }, + { + id: 'BCD', + name: 'BC Default', + }, + ], + rangeMatrices: [ + { + id: 'annualFeeCV', + name: 'Annual licensing fee for commercial vehicle', + matrix: [ + { min: 0, max: 500, value: 42 }, + { min: 501, max: 1000, value: 49 }, + { min: 1001, max: 1500, value: 60 }, + { min: 1501, max: 2000, value: 74 }, + { min: 2001, max: 2500, value: 85 }, + { min: 2501, max: 3000, value: 97 }, + { min: 3001, max: 3500, value: 108 }, + { min: 3501, max: 4000, value: 127 }, + { min: 4001, max: 4500, value: 147 }, + { min: 4501, max: 5000, value: 173 }, + { min: 5001, max: 5500, value: 193 }, + { min: 5501, max: 6000, value: 213 }, + { min: 6001, max: 6500, value: 230 }, + { min: 6501, max: 7000, value: 250 }, + { min: 7001, max: 7500, value: 266 }, + { min: 7501, max: 8000, value: 292 }, + { min: 8001, max: 8500, value: 320 }, + { min: 8501, max: 9000, value: 347 }, + { min: 9001, max: 9500, value: 376 }, + { min: 9501, max: 10000, value: 395 }, + { min: 10001, max: 10500, value: 416 }, + { min: 10501, max: 11000, value: 437 }, + { min: 11001, max: 11500, value: 450 }, + { min: 11501, max: 12000, value: 469 }, + { min: 12001, max: 12500, value: 488 }, + { min: 12501, max: 13000, value: 502 }, + { min: 13001, max: 13500, value: 526 }, + { min: 13501, max: 14000, value: 553 }, + { min: 14001, max: 14500, value: 580 }, + { min: 14501, max: 15000, value: 607 }, + { min: 15001, max: 15500, value: 638 }, + { min: 15501, max: 16000, value: 680 }, + { min: 16001, max: 16500, value: 721 }, + { min: 16501, max: 17000, value: 761 }, + { min: 17001, max: 17500, value: 806 }, + { min: 17501, max: 18000, value: 837 }, + { min: 18001, max: 18500, value: 861 }, + { min: 18501, max: 19000, value: 890 }, + { min: 19001, max: 19500, value: 917 }, + { min: 19501, max: 20000, value: 944 }, + { min: 20001, max: 20500, value: 977 }, + { min: 20501, max: 21000, value: 1003 }, + { min: 21001, max: 21500, value: 1030 }, + { min: 21501, max: 22000, value: 1057 }, + { min: 22001, max: 22500, value: 1084 }, + { min: 22501, max: 23000, value: 1111 }, + { min: 23001, max: 23500, value: 1140 }, + { min: 23501, max: 24000, value: 1170 }, + { min: 24001, max: 24500, value: 1199 }, + { min: 24501, max: 25000, value: 1239 }, + { min: 25001, max: 25500, value: 1285 }, + { min: 25501, max: 26000, value: 1326 }, + { min: 26001, max: 26500, value: 1367 }, + { min: 26501, max: 27000, value: 1395 }, + { min: 27001, max: 27500, value: 1424 }, + { min: 27501, max: 28000, value: 1450 }, + { min: 28001, max: 28500, value: 1479 }, + { min: 28501, max: 29000, value: 1505 }, + { min: 29001, max: 29500, value: 1534 }, + { min: 29501, max: 30000, value: 1565 }, + { min: 30001, max: 31000, value: 1591 }, + { min: 31001, max: 32000, value: 1644 }, + { min: 32001, max: 33000, value: 1696 }, + { min: 33001, max: 34000, value: 1751 }, + { min: 34001, max: 35000, value: 1805 }, + { min: 35001, max: 36000, value: 1890 }, + { min: 36001, max: 37000, value: 2018 }, + { min: 37001, max: 38000, value: 2088 }, + { min: 38001, max: 39000, value: 2159 }, + { min: 39001, max: 40000, value: 2229 }, + { min: 40001, max: 41000, value: 2300 }, + { min: 41001, max: 42000, value: 2373 }, + { min: 42001, max: 43000, value: 2445 }, + { min: 43001, max: 44000, value: 2514 }, + { min: 44001, max: 45000, value: 2585 }, + { min: 45001, max: 46000, value: 2690 }, + { min: 46001, max: 47000, value: 2799 }, + { min: 47001, max: 48000, value: 2871 }, + { min: 48001, max: 49000, value: 2940 }, + { min: 49001, max: 50000, value: 3012 }, + { min: 50001, max: 51000, value: 3061 }, + { min: 51001, max: 52000, value: 3127 }, + { min: 52001, max: 53000, value: 3192 }, + { min: 53001, max: 54000, value: 3257 }, + { min: 54001, max: 55000, value: 3322 }, + { min: 55001, max: 56000, value: 3387 }, + { min: 56001, max: 57000, value: 3452 }, + { min: 57001, max: 58000, value: 3516 }, + { min: 58001, max: 59000, value: 3581 }, + { min: 59001, max: 60000, value: 3647 }, + { min: 60001, max: 61000, value: 3710 }, + { min: 61001, max: 62000, value: 3775 }, + { min: 62001, max: 63000, value: 3840 }, + { min: 63001, max: 63500, value: 3905 }, + ], + }, + { + id: 'annualFeePassenger', + name: 'Annual licensing fee for commercial passenger vehicle', + matrix: [ + { min: 0, max: 500, value: 40 }, + { min: 501, max: 1000, value: 47 }, + { min: 1001, max: 1500, value: 57 }, + { min: 1501, max: 2000, value: 70 }, + { min: 2001, max: 2500, value: 81 }, + { min: 2501, max: 3000, value: 92 }, + { min: 3001, max: 3500, value: 103 }, + { min: 3501, max: 4000, value: 121 }, + { min: 4001, max: 4500, value: 140 }, + { min: 4501, max: 5000, value: 165 }, + { min: 5001, max: 5500, value: 184 }, + { min: 5501, max: 6000, value: 203 }, + { min: 6001, max: 6500, value: 219 }, + { min: 6501, max: 7000, value: 238 }, + { min: 7001, max: 7500, value: 253 }, + { min: 7501, max: 8000, value: 278 }, + { min: 8001, max: 8500, value: 305 }, + { min: 8501, max: 9000, value: 330 }, + { min: 9001, max: 9500, value: 358 }, + { min: 9501, max: 10000, value: 376 }, + { min: 10001, max: 10500, value: 396 }, + { min: 10501, max: 11000, value: 416 }, + { min: 11001, max: 11500, value: 429 }, + { min: 11501, max: 12000, value: 447 }, + { min: 12001, max: 12500, value: 465 }, + { min: 12501, max: 13000, value: 478 }, + { min: 13001, max: 13500, value: 501 }, + { min: 13501, max: 14000, value: 527 }, + { min: 14001, max: 14500, value: 552 }, + { min: 14501, max: 15000, value: 578 }, + { min: 15001, max: 15500, value: 608 }, + { min: 15501, max: 16000, value: 648 }, + { min: 16001, max: 16500, value: 687 }, + { min: 16501, max: 17000, value: 725 }, + { min: 17001, max: 17500, value: 768 }, + { min: 17501, max: 18000, value: 797 }, + { min: 18001, max: 18500, value: 820 }, + { min: 18501, max: 19000, value: 848 }, + { min: 19001, max: 19500, value: 873 }, + { min: 19501, max: 20000, value: 899 }, + { min: 20001, max: 20500, value: 930 }, + { min: 20501, max: 21000, value: 955 }, + { min: 21001, max: 21500, value: 981 }, + { min: 21501, max: 22000, value: 1007 }, + { min: 22001, max: 22500, value: 1032 }, + { min: 22501, max: 23000, value: 1058 }, + { min: 23001, max: 23500, value: 1086 }, + { min: 23501, max: 24000, value: 1114 }, + { min: 24001, max: 24500, value: 1142 }, + { min: 24501, max: 25000, value: 1180 }, + { min: 25001, max: 25500, value: 1224 }, + { min: 25501, max: 26000, value: 1263 }, + { min: 26001, max: 26500, value: 1302 }, + { min: 26501, max: 27000, value: 1329 }, + { min: 27001, max: 27500, value: 1356 }, + { min: 27501, max: 28000, value: 1381 }, + { min: 28001, max: 28500, value: 1409 }, + { min: 28501, max: 29000, value: 1433 }, + { min: 29001, max: 29500, value: 1461 }, + { min: 29501, max: 30000, value: 1490 }, + { min: 30001, max: 31000, value: 1516 }, + { min: 31001, max: 32000, value: 1569 }, + { min: 32001, max: 33000, value: 1621 }, + { min: 33001, max: 34000, value: 1676 }, + { min: 34001, max: 35000, value: 1730 }, + { min: 35001, max: 36000, value: 1815 }, + { min: 36001, max: 37000, value: 1943 }, + { min: 37001, max: 38000, value: 2013 }, + { min: 38001, max: 39000, value: 2084 }, + { min: 39001, max: 40000, value: 2154 }, + { min: 40001, max: 41000, value: 2225 }, + { min: 41001, max: 42000, value: 2298 }, + { min: 42001, max: 43000, value: 2370 }, + { min: 43001, max: 44000, value: 2439 }, + { min: 44001, max: 45000, value: 2510 }, + { min: 45001, max: 46000, value: 2615 }, + { min: 46001, max: 47000, value: 2724 }, + { min: 47001, max: 48000, value: 2796 }, + { min: 48001, max: 49000, value: 2865 }, + { min: 49001, max: 50000, value: 2937 }, + { min: 50001, max: 51000, value: 2986 }, + { min: 51001, max: 52000, value: 3052 }, + { min: 52001, max: 53000, value: 3117 }, + { min: 53001, max: 54000, value: 3182 }, + { min: 54001, max: 55000, value: 3247 }, + { min: 55001, max: 56000, value: 3312 }, + { min: 56001, max: 57000, value: 3377 }, + { min: 57001, max: 58000, value: 3441 }, + { min: 58001, max: 59000, value: 3506 }, + { min: 59001, max: 60000, value: 3572 }, + { min: 60001, max: 61000, value: 3635 }, + { min: 61001, max: 62000, value: 3700 }, + { min: 62001, max: 63000, value: 3765 }, + { min: 63001, max: 63500, value: 3830 }, + ], + }, + { + id: 'annualFeeIndustrial', + name: 'Annual licensing fee for an industrial machine', + matrix: [ + { min: 0, max: 2000, value: 45 }, + { min: 2001, max: 5000, value: 69 }, + { min: 5001, max: 7000, value: 110 }, + { min: 7001, max: 9000, value: 164 }, + { min: 9001, max: 11000, value: 216 }, + { min: 11001, value: 260 }, + ], + }, + { + id: 'annualFeeFarm', + name: 'Annual licensing fee for farm vehicle', + matrix: [ + { min: 0, max: 500, value: 30 }, + { min: 501, max: 1000, value: 40 }, + { min: 1001, max: 1500, value: 47 }, + { min: 1501, max: 2000, value: 55 }, + { min: 2001, max: 2500, value: 77 }, + { min: 2501, max: 3000, value: 101 }, + { min: 3001, max: 3500, value: 142 }, + { min: 3501, max: 4000, value: 181 }, + { min: 4001, max: 4500, value: 207 }, + { min: 4501, max: 5000, value: 243 }, + { min: 5001, max: 5500, value: 278 }, + { min: 5501, max: 6000, value: 322 }, + { min: 6001, max: 6500, value: 355 }, + { min: 6501, max: 7000, value: 396 }, + { min: 7001, max: 7500, value: 427 }, + { min: 7501, max: 8000, value: 473 }, + { min: 8001, max: 8500, value: 524 }, + { min: 8501, max: 9000, value: 558 }, + { min: 9001, max: 9500, value: 596 }, + { min: 9501, max: 10000, value: 633 }, + { min: 10001, max: 10500, value: 669 }, + { min: 10501, max: 11000, value: 711 }, + { min: 11001, max: 11500, value: 744 }, + { min: 11501, max: 12000, value: 784 }, + { min: 12001, max: 12500, value: 824 }, + { min: 12501, max: 13000, value: 863 }, + { min: 13001, max: 13500, value: 883 }, + { min: 13501, max: 14000, value: 899 }, + { min: 14001, max: 14500, value: 919 }, + { min: 14501, max: 15000, value: 940 }, + { min: 15001, max: 15500, value: 960 }, + { min: 15501, max: 16000, value: 979 }, + { min: 16001, max: 16500, value: 997 }, + { min: 16501, max: 17000, value: 1017 }, + { min: 17001, max: 17500, value: 1036 }, + { min: 17501, max: 18000, value: 1056 }, + { min: 18001, max: 18500, value: 1076 }, + { min: 18501, max: 19000, value: 1096 }, + { min: 19001, max: 19500, value: 1114 }, + { min: 19501, max: 20000, value: 1134 }, + { min: 20001, max: 20500, value: 1154 }, + { min: 20501, max: 21000, value: 1174 }, + { min: 21001, max: 21500, value: 1192 }, + { min: 21501, max: 22000, value: 1211 }, + { min: 22001, max: 22500, value: 1231 }, + { min: 22501, max: 23000, value: 1251 }, + { min: 23001, max: 23500, value: 1270 }, + { min: 23501, max: 24000, value: 1289 }, + { min: 24001, max: 24400, value: 1309 }, + ], + }, + ], + commonRules: [ + { + conditions: { + not: { + fact: 'permitData.companyName', + operator: 'stringMinimumLength', + value: 1, + }, + }, + event: { + type: 'violation', + params: { + message: 'Company is required', + code: 'field-validation-error', + fieldReference: 'permitData.companyName', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.contactDetails.firstName', + operator: 'stringMinimumLength', + value: 1, + }, + }, + event: { + type: 'violation', + params: { + message: 'Contact first name is required', + code: 'field-validation-error', + fieldReference: 'permitData.contactDetails.firstName', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.contactDetails.lastName', + operator: 'stringMinimumLength', + value: 1, + }, + }, + event: { + type: 'violation', + params: { + message: 'Contact last name is required', + code: 'field-validation-error', + fieldReference: 'permitData.contactDetails.lastName', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.contactDetails.phone1', + operator: 'stringMinimumLength', + value: 1, + }, + }, + event: { + type: 'violation', + params: { + message: 'Contact phone number is required', + code: 'field-validation-error', + fieldReference: 'permitData.contactDetails.phone1', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.contactDetails.email', + operator: 'stringMinimumLength', + value: 1, + }, + }, + event: { + type: 'violation', + params: { + message: 'Company contact email is required', + code: 'field-validation-error', + fieldReference: 'permitData.contactDetails.email', + }, + }, + }, + { + conditions: { + any: [ + { + fact: 'permitData.startDate', + operator: 'dateLessThan', + value: { + fact: 'validationDate', + }, + }, + ], + }, + event: { + type: 'violation', + params: { + message: 'Permit start date cannot be in the past', + code: 'field-validation-error', + fieldReference: 'permitData.startDate', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.vehicleDetails.vin', + operator: 'regex', + value: '^[a-zA-Z0-9]{6}$', + }, + }, + event: { + type: 'violation', + params: { + message: + 'Vehicle Identification Number (vin) must be 6 alphanumeric characters', + code: 'field-validation-error', + fieldReference: 'permitData.vehicleDetails.vin', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.vehicleDetails.plate', + operator: 'stringMinimumLength', + value: 1, + }, + }, + event: { + type: 'violation', + params: { + message: 'Vehicle plate is required', + code: 'field-validation-error', + fieldReference: 'permitData.vehicleDetails.plate', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.vehicleDetails.make', + operator: 'stringMinimumLength', + value: 1, + }, + }, + event: { + type: 'violation', + params: { + message: 'Vehicle make is required', + code: 'field-validation-error', + fieldReference: 'permitData.vehicleDetails.make', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.vehicleDetails.year', + operator: 'greaterThan', + value: 1900, + }, + }, + event: { + type: 'violation', + params: { + message: 'Vehicle year is required', + code: 'field-validation-error', + fieldReference: 'permitData.vehicleDetails.year', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.vehicleDetails.countryCode', + operator: 'stringMinimumLength', + value: 1, + }, + }, + event: { + type: 'violation', + params: { + message: 'Vehicle country of registration is required', + code: 'field-validation-error', + fieldReference: 'permitData.vehicleDetails.countryCode', + }, + }, + }, + ], + permitTypes: [ + { + id: 'TROS', + name: 'Term Oversize', + routingRequired: false, + weightDimensionRequired: false, + sizeDimensionRequired: false, + commodityRequired: false, + allowedVehicles: [ + 'BOOSTER', + 'DOLLIES', + 'EXPANDO', + 'FEBGHSE', + 'FECVYER', + 'FEDRMMX', + 'FEPNYTR', + 'FESEMTR', + 'FEWHELR', + 'FLOATTR', + 'FULLLTL', + 'HIBOEXP', + 'HIBOFLT', + 'JEEPSRG', + 'LOGDGLG', + 'LOGFULL', + 'LOGNTAC', + 'LOGOWBK', + 'LOGSMEM', + 'LOGTNDM', + 'LOGTRIX', + 'ODTRLEX', + 'OGOSFDT', + 'PLATFRM', + 'POLETRL', + 'PONYTRL', + 'REDIMIX', + 'SEMITRL', + 'STBTRAN', + 'STCHIPS', + 'STCRANE', + 'STINGAT', + 'STLOGNG', + 'STNTSHC', + 'STREEFR', + 'STSDBDK', + 'STSTNGR', + 'STWHELR', + 'STWIDWH', + 'BUSTRLR', + 'CONCRET', + 'DDCKBUS', + 'GRADERS', + 'LOGGING', + 'LOGOFFH', + 'LWBTRCT', + 'OGBEDTK', + 'OGOILSW', + 'PICKRTT', + 'PLOWBLD', + 'REGTRCK', + 'STINGER', + 'TOWVEHC', + 'TRKTRAC', + ], + rules: [ + { + conditions: { + all: [ + { + not: { + fact: 'permitData.permitDuration', + operator: 'in', + value: [30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330], + }, + }, + { + not: { + fact: 'permitData.permitDuration', + operator: 'equal', + value: { + fact: 'daysInPermitYear', + }, + }, + }, + ], + }, + event: { + type: 'violation', + params: { + message: 'Duration must be in 30 day increments or a full year', + code: 'field-validation-error', + fieldReference: 'permitData.permitDuration', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.vehicleDetails.vehicleSubType', + operator: 'in', + value: { + fact: 'allowedVehicles', + }, + }, + }, + event: { + type: 'violation', + params: { + message: 'Vehicle type not permittable for this permit type', + code: 'field-validation-error', + fieldReference: 'permitData.vehicleDetails.vehicleSubType', + }, + }, + }, + ], + costRules: [ + { + fact: 'costPerMonth', + params: { + cost: 30, + }, + }, + ], + }, + { + id: 'TROW', + name: 'Term Overweight', + routingRequired: false, + weightDimensionRequired: false, + sizeDimensionRequired: false, + commodityRequired: false, + allowedVehicles: [ + 'DOLLIES', + 'FEBGHSE', + 'FECVYER', + 'FEDRMMX', + 'FEPNYTR', + 'FESEMTR', + 'FEWHELR', + 'REDIMIX', + 'CONCRET', + 'CRAFTAT', + 'CRAFTMB', + 'GRADERS', + 'MUNFITR', + 'OGOILSW', + 'OGSERVC', + 'OGSRRAH', + 'PICKRTT', + 'TOWVEHC', + ], + rules: [ + { + conditions: { + all: [ + { + not: { + fact: 'permitData.permitDuration', + operator: 'in', + value: [30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330], + }, + }, + { + not: { + fact: 'permitData.permitDuration', + operator: 'equal', + value: { + fact: 'daysInPermitYear', + }, + }, + }, + ], + }, + event: { + type: 'violation', + params: { + message: 'Duration must be in 30 day increments or a full year', + code: 'field-validation-error', + fieldReference: 'permitData.permitDuration', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.vehicleDetails.vehicleSubType', + operator: 'in', + value: { + fact: 'allowedVehicles', + }, + }, + }, + event: { + type: 'violation', + params: { + message: 'Vehicle type not permittable for this permit type', + code: 'field-validation-error', + fieldReference: 'permitData.vehicleDetails.vehicleSubType', + }, + }, + }, + ], + costRules: [ + { + fact: 'costPerMonth', + params: { + cost: 100, + }, + }, + ], + }, + { + id: 'STOS', + name: 'Single Trip Oversize', + routingRequired: true, + weightDimensionRequired: false, + sizeDimensionRequired: true, + commodityRequired: true, + allowedCommodities: ['EMPTYXX', 'BRGBEAM', 'AUTOCRR', 'BRSHCUT'], + allowedVehicles: [ + 'DOLLIES', + 'FEBGHSE', + 'FECVYER', + 'FEDRMMX', + 'FEPNYTR', + 'FESEMTR', + 'FEWHELR', + 'REDIMIX', + 'CONCRET', + 'CRAFTAT', + 'CRAFTMB', + 'GRADERS', + 'MUNFITR', + 'OGOILSW', + 'OGSERVC', + 'OGSRRAH', + 'PICKRTT', + 'TOWVEHC', + ], + rules: [ + { + conditions: { + any: [ + { + not: { + fact: 'permitData.permitDuration', + operator: 'lessThanInclusive', + value: 7, + }, + }, + { + not: { + fact: 'permitData.permitDuration', + operator: 'greaterThan', + value: 0, + }, + }, + ], + }, + event: { + type: 'violation', + params: { + message: 'Duration must be 7 days or less', + code: 'field-validation-error', + fieldReference: 'permitData.permitDuration', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.vehicleDetails.vehicleSubType', + operator: 'in', + value: { + fact: 'allowedVehicles', + }, + }, + }, + event: { + type: 'violation', + params: { + message: 'Vehicle type not permittable for this permit type', + code: 'field-validation-error', + fieldReference: 'permitData.vehicleDetails.vehicleSubType', + }, + }, + }, + ], + costRules: [ + { + fact: 'fixedCost', + params: { + cost: 15, + }, + }, + ], + }, + ], + globalWeightDefaults: { + powerUnits: [], + trailers: [], + }, + vehicleCategories: { + trailerCategories: [ + { + id: 'trailer', + name: 'Default trailer category', + }, + { + id: 'accessory', + name: 'Accessory trailer such as jeep or booster, to be used alongside other trailers. Not permittable on its own as a trailer in a combination.', + }, + { + id: 'pseudo', + name: 'Placeholder for a trailer in a combination with no trailer (such as when a brushcutter is permitted with no trailer).', + }, + ], + powerUnitCategories: [ + { + id: 'powerunit', + name: 'Default power unit category', + }, + ], + }, + vehicleTypes: { + powerUnitTypes: [ + { + id: 'BUSCRUM', + name: 'Buses/Crummies', + category: 'powerunit', + }, + { + id: 'BUSTRLR', + name: 'Intercity Buses (Pulling Pony Trailers)', + category: 'powerunit', + }, + { + id: 'CONCRET', + name: 'Concrete Pumper Trucks', + category: 'powerunit', + }, + { + id: 'CRAFTAT', + name: 'Cranes, Rubber-Tired Loaders, Firetrucks - All Terrain', + category: 'powerunit', + }, + { + id: 'CRAFTMB', + name: 'Cranes, Rubber-Tired Loaders, Firetrucks - Mobile', + category: 'powerunit', + }, + { + id: 'DDCKBUS', + name: 'Double Decker Buses', + category: 'powerunit', + }, + { + id: 'FARMVEH', + name: 'Farm Vehicles', + category: 'powerunit', + }, + { + id: 'GRADERS', + name: 'Fixed Equipment - Trucks/Graders etc.', + category: 'powerunit', + }, + { + id: 'LCVRMDB', + name: 'Long Combination Vehicles (LCV) - Rocky Mountain Doubles', + category: 'powerunit', + }, + { + id: 'LCVTPDB', + name: 'Long Combination Vehicles (LCV) - Turnpike Doubles', + category: 'powerunit', + }, + { + id: 'LOGGING', + name: 'Logging Trucks', + category: 'powerunit', + }, + { + id: 'LOGOFFH', + name: 'Logging Trucks - Off-Highway', + category: 'powerunit', + }, + { + id: 'LWBTRCT', + name: 'Long Wheelbase Truck Tractors Exceeding 6.2 m up to 7.25 m', + category: 'powerunit', + }, + { + id: 'MUNFITR', + name: 'Municipal Fire Trucks', + category: 'powerunit', + }, + { + id: 'OGBEDTK', + name: 'Oil and Gas - Bed Trucks', + category: 'powerunit', + }, + { + id: 'OGOILSW', + name: 'Oil and Gas - Oilfield Sows', + category: 'powerunit', + }, + { + id: 'OGSERVC', + name: 'Oil and Gas - Service Rigs', + category: 'powerunit', + }, + { + id: 'OGSRRAH', + name: 'Oil and Gas - Service Rigs and Rathole Augers Only Equipped with Heavy Front Projected Crane (must exceed 14,000 kg tare weight)', + category: 'powerunit', + }, + { + id: 'PICKRTT', + name: 'Picker Truck Tractors', + category: 'powerunit', + }, + { + id: 'PLOWBLD', + name: 'Trucks Equipped with Front or Underbody Plow Blades', + category: 'powerunit', + }, + { + id: 'PUTAXIS', + name: 'Taxis', + category: 'powerunit', + }, + { + id: 'REGTRCK', + name: 'Trucks', + category: 'powerunit', + }, + { + id: 'SCRAPER', + name: 'Scrapers', + category: 'powerunit', + }, + { + id: 'SPAUTHV', + name: 'Specially Authorized Vehicles', + category: 'powerunit', + }, + { + id: 'STINGER', + name: 'Truck Tractors - Stinger Steered', + category: 'powerunit', + }, + { + id: 'TOWVEHC', + name: 'Tow Vehicles', + category: 'powerunit', + }, + { + id: 'TRKTRAC', + name: 'Truck Tractors', + category: 'powerunit', + }, + ], + trailerTypes: [ + { + id: 'BOOSTER', + name: 'Boosters', + category: 'accessory', + }, + { + id: 'DBTRBTR', + name: 'Tandem/Tridem Drive B-Train (Super B-Train)', + category: 'trailer', + }, + { + id: 'DOLLIES', + name: 'Dollies', + category: 'trailer', + }, + { + id: 'EXPANDO', + name: 'Expando Semi-Trailers', + category: 'trailer', + }, + { + id: 'FEBGHSE', + name: 'Fixed Equipment - Portable Asphalt Baghouses', + category: 'trailer', + }, + { + id: 'FECVYER', + name: 'Fixed Equipment - Conveyors', + category: 'trailer', + }, + { + id: 'FEDRMMX', + name: 'Fixed Equipment - Counter Flow Asphalt Drum Mixers', + category: 'trailer', + }, + { + id: 'FEPNYTR', + name: 'Fixed Equipment - Pony Trailers', + category: 'trailer', + }, + { + id: 'FESEMTR', + name: 'Fixed Equipment - Semi-Trailers', + category: 'trailer', + }, + { + id: 'FEWHELR', + name: 'Fixed Equipment - Wheeler Semi-Trailers', + category: 'wheeler', + }, + { + id: 'FLOATTR', + name: 'Float Trailers', + category: 'wheeler', + }, + { + id: 'FULLLTL', + name: 'Full Trailers', + category: 'trailer', + }, + { + id: 'HIBOEXP', + name: 'Semi-Trailers - Hiboys/Expandos', + category: 'trailer', + }, + { + id: 'HIBOFLT', + name: 'Semi-Trailers - Hiboys/Flat Decks', + category: 'trailer', + }, + { + id: 'JEEPSRG', + name: 'Jeeps', + category: 'accessory', + }, + { + id: 'LOGDGLG', + name: 'Legacy Logging Trailer Combinations - Tandem Pole Trailers, Dogloggers', + category: 'trailer', + }, + { + id: 'LOGFULL', + name: 'Logging Trailers - Full Trailers, Tri Axle, Quad Axle', + category: 'trailer', + }, + { + id: 'LOGNTAC', + name: 'Legacy Logging Trailer Combinations - Non-TAC B-Trains', + category: 'trailer', + }, + { + id: 'LOGOWBK', + name: 'Logging Trailers - Overwidth Bunks', + category: 'trailer', + }, + { + id: 'LOGSMEM', + name: 'Logging Semi-Trailer - Empty, 3.2 m Bunks', + category: 'trailer', + }, + { + id: 'LOGTNDM', + name: 'Legacy Logging Trailer Combinations - Single Axle Jeeps, Tandem Axle Pole Trailers, Dogloggers', + category: 'trailer', + }, + { + id: 'LOGTRIX', + name: 'Legacy Logging Trailer Combinations - Single Axle Jeeps, Tri Axle Trailers', + category: 'trailer', + }, + { + id: 'MHMBSHG', + name: 'Manufactured Homes, Modular Buildings, Structures and Houseboats (> 5.0 m OAW) with Attached Axles', + category: 'trailer', + }, + { + id: 'MHMBSHL', + name: 'Manufactured Homes, Modular Buildings, Structures and Houseboats (<= 5.0 m OAW) with Attached Axles', + category: 'trailer', + }, + { + id: 'ODTRLEX', + name: 'Overdimensional Trailers and Semi-Trailers (For Export)', + category: 'trailer', + }, + { + id: 'OGOSFDT', + name: 'Oil and Gas - Oversize Oilfield Flat Deck Semi-Trailers', + category: 'trailer', + }, + { + id: 'PLATFRM', + name: 'Platform Trailers', + category: 'trailer', + }, + { + id: 'PMHWAAX', + name: 'Park Model Homes with Attached Axles', + category: 'trailer', + }, + { + id: 'POLETRL', + name: 'Pole Trailers', + category: 'trailer', + }, + { + id: 'PONYTRL', + name: 'Pony Trailers', + category: 'trailer', + }, + { + id: 'REDIMIX', + name: 'Ready Mix Concrete Pump Semi-Trailers', + category: 'trailer', + }, + { + id: 'SEMITRL', + name: 'Semi-Trailers', + category: 'trailer', + }, + { + id: 'STACTRN', + name: 'Semi-Trailers - A-Trains and C-Trains', + category: 'trailer', + }, + { + id: 'STBTRAN', + name: 'Semi-Trailers - B-Trains', + category: 'trailer', + }, + { + id: 'STCHIPS', + name: 'Semi-Trailers - Walled B-Trains (Chip Trucks)', + category: 'trailer', + }, + { + id: 'STCRANE', + name: 'Semi-Trailers with Crane', + category: 'trailer', + }, + { + id: 'STINGAT', + name: 'Stinger Steered Automobile Transporters', + category: 'trailer', + }, + { + id: 'STLOGNG', + name: 'Semi-Trailers - Logging', + category: 'trailer', + }, + { + id: 'STNTSHC', + name: 'Semi-Trailers - Non-Tac Short Chassis', + category: 'trailer', + }, + { + id: 'STREEFR', + name: 'Semi-Trailers - Insulated Vans with Reefer/Refrigeration Units', + category: 'trailer', + }, + { + id: 'STROPRT', + name: 'Steering Trailers - Manned', + category: 'trailer', + }, + { + id: 'STRSELF', + name: 'Steering Trailers - Self/Remote', + category: 'trailer', + }, + { + id: 'STSDBDK', + name: 'Semi-Trailers - Single Drop, Double Drop, Step Decks, Lowbed, Expandos, etc.', + category: 'trailer', + }, + { + id: 'STSTEER', + name: 'Semi-Trailers - Steering Trailers', + category: 'trailer', + }, + { + id: 'STSTNGR', + name: 'Semi-Trailers - Stinger Steered Automobile Transporters', + category: 'trailer', + }, + { + id: 'STWDTAN', + name: 'Semi-Trailers - Spread Tandems', + category: 'trailer', + }, + { + id: 'STWHELR', + name: 'Semi-Trailers - Wheelers', + category: 'trailer', + }, + { + id: 'STWIDWH', + name: 'Semi-Trailers - Wide Wheelers', + category: 'trailer', + }, + { + id: 'NONEXXX', + name: 'No trailer', + category: 'pseudo', + }, + ], + }, + commodities: [ + { + id: 'WODWIDB', + name: 'Wood on wide bunks', + }, + { + id: 'LEGALLO', + name: 'Legal loads', + }, + { + id: 'GRADERS', + name: 'Grader', + }, + { + id: 'HUSBAND', + name: 'Implements of husbandry', + }, + { + id: 'OILFILD', + name: 'Oil field equipment', + }, + { + id: 'ROKTRUK', + name: 'Rock truck', + }, + { + id: 'TIRESXX', + name: 'Tires', + }, + { + id: 'SRVCRIG', + name: 'Service rig', + }, + { + id: 'BUNCHER', + name: 'Buncher', + }, + { + id: 'CONVEYR', + name: 'Conveyor', + }, + { + id: 'SKIDUNT', + name: 'Oil field skid unit', + }, + { + id: 'EMPTYXX', + name: 'Empty', + powerUnits: [ + { + type: 'TRKTRAC', + canFollow: [], + }, + { + type: 'PICKRTT', + canFollow: [], + }, + ], + trailers: [ + { + type: 'JEEPSRG', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + }, + { + type: 'BOOSTER', + canFollow: [ + 'OGOSFDT', + 'PLATFRM', + 'HIBOEXP', + 'STWHELR', + 'STWIDWH', + 'STCRANE', + 'HIBOFLT', + 'STSDBDK', + 'BOOSTER', + ], + }, + { + type: 'LOGOWBK', + canFollow: ['TRKTRAC'], + sizeDimensions: [ + { + width: 3.2, + length: 23, + }, + ], + }, + { + type: 'OGOSFDT', + canFollow: ['PICKRTT', 'JEEPSRG'], + sizeDimensions: [ + { + width: 3.2, + height: 4.3, + length: 23, + }, + ], + }, + { + type: 'PLATFRM', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + canSelfIssue: false, + sizeDimensions: [ + { + width: 3.2, + height: 4.88, + length: 27.5, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'HIBOEXP', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + sizeDimensions: [ + { + height: 4.4, + length: 25, + regions: [ + { + region: 'PCE', + height: 5.33, + length: 27.5, + }, + ], + modifiers: [ + { + position: 'first', + type: 'PICKRTT', + }, + ], + }, + { + height: 4.4, + length: 31, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + modifiers: [ + { + position: 'first', + type: 'TRKTRAC', + }, + ], + }, + ], + }, + { + type: 'STWHELR', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + canSelfIssue: false, + sizeDimensions: [ + { + width: 3.2, + height: 4.88, + length: 27.5, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'STWIDWH', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + canSelfIssue: false, + sizeDimensions: [ + { + width: 3.2, + height: 4.88, + length: 27.5, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'STCRANE', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + sizeDimensions: [ + { + width: 3.2, + height: 4.88, + length: 27.5, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'HIBOFLT', + canFollow: ['TRKTRAC', 'JEEPSRG'], + sizeDimensions: [ + { + height: 4.4, + length: 27.5, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'STSDBDK', + canFollow: ['TRKTRAC', 'JEEPSRG'], + sizeDimensions: [ + { + width: 3.2, + height: 4.4, + length: 31, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + ], + }, + { + id: 'GENERAL', + name: 'General commodities', + }, + { + id: 'CULVERT', + name: 'Culverts', + }, + { + id: 'RAMPSXX', + name: 'Ramps', + }, + { + id: 'REBARXX', + name: 'Rebar', + }, + { + id: 'RESTEEL', + name: 'Reinforcing steel', + }, + { + id: 'STSTEEL', + name: 'Structural steel', + }, + { + id: 'PIPEXXX', + name: 'Pipe', + }, + { + id: 'GRBBINS', + name: 'Garbage bins', + }, + { + id: 'BOATSXX', + name: 'Boats', + }, + { + id: 'BOOMSTK', + name: 'Boomsticks', + }, + { + id: 'CONTANR', + name: 'Containers', + }, + { + id: 'HAYROND', + name: 'Hay bales round', + }, + { + id: 'HAYLREC', + name: 'Hay bales large rectangular', + }, + { + id: 'HAYBALE', + name: 'Hay bales', + }, + { + id: 'HAYSREC', + name: 'Hay bales small rectangular', + }, + { + id: 'BUILDNG', + name: 'House or building', + }, + { + id: 'LONGLOG', + name: 'Long logs', + }, + { + id: 'PILINGS', + name: 'Piling', + }, + { + id: 'POLESXX', + name: 'Poles', + }, + { + id: 'RUGHLUM', + name: 'Rough cut lumber', + }, + { + id: 'VENEERX', + name: 'Veneer', + }, + { + id: 'MODULAR', + name: 'Modular building', + }, + { + id: 'MOBHOME', + name: 'Mobile home manufactured', + }, + { + id: 'PARKTRL', + name: 'Park model trailers', + }, + { + id: 'AIRCRFT', + name: 'Aircraft', + }, + { + id: 'BACKHOE', + name: 'Backhoes', + }, + { + id: 'BRIDGES', + name: 'Bridges', + }, + { + id: 'BRGBEAM', + name: 'Bridge beams', + powerUnits: [ + { + type: 'TRKTRAC', + canFollow: [], + }, + ], + trailers: [ + { + type: 'JEEPSRG', + canFollow: ['TRKTRAC', 'JEEPSRG'], + }, + { + type: 'BOOSTER', + canFollow: ['POLETRL', 'BOOSTER'], + }, + { + type: 'POLETRL', + canFollow: ['TRKTRAC', 'JEEPSRG'], + }, + ], + }, + { + id: 'BARGESX', + name: 'Barges', + }, + { + id: 'BLADESX', + name: 'Blades', + }, + { + id: 'BUCKETS', + name: 'Buckets', + }, + { + id: 'CATCRLR', + name: 'Cat or crawler', + }, + { + id: 'COALTRK', + name: 'Coal truck', + }, + { + id: 'CMPRSOR', + name: 'Compressor', + }, + { + id: 'CRANESX', + name: 'Cranes', + }, + { + id: 'CRAWLER', + name: 'Crawler', + }, + { + id: 'CRUSHER', + name: 'Crusher parts', + }, + { + id: 'EXCVATR', + name: 'Excavator', + }, + { + id: 'FORKLFT', + name: 'Fork lifts', + }, + { + id: 'GENRTOR', + name: 'Generator', + }, + { + id: 'LOADERX', + name: 'Loader', + }, + { + id: 'LOGGING', + name: 'Logging machinery', + }, + { + id: 'LOGLODR', + name: 'Log loader', + }, + { + id: 'MINEMAC', + name: 'Mine machinery', + }, + { + id: 'MILLMAC', + name: 'Mill machinery', + }, + { + id: 'NONREDU', + name: 'Non reducible loads', + }, + { + id: 'OILDRRG', + name: 'Oil drill rig', + }, + { + id: 'OREBOXS', + name: 'Ore boxes', + }, + { + id: 'PLSTEEL', + name: 'Plate steel', + }, + { + id: 'STBEAMS', + name: 'Steel beams', + }, + { + id: 'SHOVELS', + name: 'Shovels', + }, + { + id: 'SKIDDER', + name: 'Skidder', + }, + { + id: 'TRUSSES', + name: 'Trusses', + }, + { + id: 'TANKSXX', + name: 'Tanks', + }, + { + id: 'TRNSFRM', + name: 'Transformer', + }, + { + id: 'TWRTUBE', + name: 'Tower tube', + }, + { + id: 'VESSELS', + name: 'Vessels', + }, + { + id: 'LMBEAMS', + name: 'Laminated beams', + }, + { + id: 'YARDERX', + name: 'Yarder', + }, + { + id: 'SKDSTAK', + name: 'Skid stack', + }, + { + id: 'OTHERVE', + name: 'Other vehicle', + }, + { + id: 'SCRAPER', + name: 'Scrapers', + }, + { + id: 'BMCRANE', + name: 'Booms (cranes)', + }, + { + id: 'GENVEHC', + name: 'General - vehicle and loads', + }, + { + id: 'GENMOHO', + name: 'General - mobile homes', + }, + { + id: 'AUTOCRR', + name: 'Auto carrier, campers and boats (stinger steered)', + powerUnits: [ + { + type: 'STINGER', + canFollow: [], + }, + ], + trailers: [ + { + type: 'STSTNGR', + canFollow: ['STINGER'], + sizeDimensions: [ + { + frontProjection: 1, + rearProjection: 1.2, + width: 2.6, + height: 4.4, + length: 25, + regions: [ + { + region: 'LMN', + height: 4.3, + }, + { + region: 'KTN', + height: 4.3, + }, + { + region: 'PCE', + height: 4.88, + }, + ], + }, + ], + }, + ], + }, + { + id: 'BMPOLES', + name: 'Boomsticks and poles', + }, + { + id: 'HAYRNPR', + name: 'Haybales (round) Peace River only', + }, + { + id: 'BUSPONY', + name: 'Inter-city bus with pony trailer', + }, + { + id: 'TOWTRCK', + name: 'Tow trucks and disabled vehicles', + }, + { + id: 'BRSHCUT', + name: 'Brushcutters (Peace only)', + powerUnits: [ + { + type: 'TRKTRAC', + canFollow: [], + }, + { + type: 'REGTRCK', + canFollow: [], + }, + ], + trailers: [ + { + type: 'JEEPSRG', + canFollow: ['TRKTRAC', 'JEEPSRG'], + }, + { + type: 'BOOSTER', + canFollow: ['STSDBDK', 'BOOSTER'], + }, + { + type: 'SEMITRL', + canFollow: ['TRKTRAC'], + sizeDimensions: [ + { + length: 23, + regions: [ + { + region: 'PCE', + width: 4.57, + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'STSDBDK', + canFollow: ['TRKTRAC', 'JEEPSRG'], + sizeDimensions: [ + { + length: 23, + regions: [ + { + region: 'PCE', + width: 4.57, + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'FULLLTL', + canFollow: ['REGTRCK'], + sizeDimensions: [ + { + frontProjection: 1, + rearProjection: 1, + length: 23, + regions: [ + { + region: 'PCE', + width: 4.57, + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'NONEXXX', + canFollow: ['REGTRCK'], + sizeDimensions: [ + { + frontProjection: 1, + rearProjection: 1, + length: 12.5, + regions: [ + { + region: 'PCE', + width: 4.57, + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'PONYTRL', + canFollow: ['REGTRCK'], + sizeDimensions: [ + { + frontProjection: 1, + rearProjection: 1, + length: 23, + regions: [ + { + region: 'PCE', + width: 4.57, + height: 5.33, + }, + ], + }, + ], + }, + ], + }, + { + id: 'FIXEDEQ', + name: 'Fixed equipment', + }, + ], + globalSizeDefaults: { + frontProjection: 3, + rearProjection: 6.5, + width: 2.6, + height: 4.15, + length: 31, + }, +}; diff --git a/policy-engine/src/_test/policy-config/master.sample.ts b/policy-engine/src/_test/policy-config/master.sample.ts index da14422bb..cc5267c30 100644 --- a/policy-engine/src/_test/policy-config/master.sample.ts +++ b/policy-engine/src/_test/policy-config/master.sample.ts @@ -312,6 +312,14 @@ export const masterPolicyConfig: PolicyDefinition = { }, }, ], + costRules: [ + { + fact: 'costPerMonth', + params: { + cost: 30, + }, + }, + ], }, { id: 'TROW', @@ -391,6 +399,14 @@ export const masterPolicyConfig: PolicyDefinition = { }, }, ], + costRules: [ + { + fact: 'costPerMonth', + params: { + cost: 100, + }, + }, + ], }, ], globalWeightDefaults: { diff --git a/policy-engine/src/_test/policy-config/stos-vehicle-config.sample.ts b/policy-engine/src/_test/policy-config/stos-vehicle-config.sample.ts new file mode 100644 index 000000000..bf9302464 --- /dev/null +++ b/policy-engine/src/_test/policy-config/stos-vehicle-config.sample.ts @@ -0,0 +1,1242 @@ +import { PolicyDefinition } from '../../types/policy-definition'; + +export const stosPolicyConfig: PolicyDefinition = { + version: '2024.03.18.001', + geographicRegions: [ + { + id: 'LMN', + name: 'Lower Mainland', + }, + { + id: 'KTN', + name: 'Kootenay', + }, + { + id: 'PCE', + name: 'Peace', + }, + { + id: 'BCD', + name: 'BC Default', + }, + ], + commonRules: [], + permitTypes: [ + { + id: 'STOS', + name: 'Single Trip Oversize', + routingRequired: true, + weightDimensionRequired: false, + sizeDimensionRequired: true, + commodityRequired: true, + allowedCommodities: ['EMPTYXX', 'BRGBEAM', 'AUTOCRR', 'BRSHCUT'], + allowedVehicles: [ + 'DOLLIES', + 'FEBGHSE', + 'FECVYER', + 'FEDRMMX', + 'FEPNYTR', + 'FESEMTR', + 'FEWHELR', + 'REDIMIX', + 'CONCRET', + 'CRAFTAT', + 'CRAFTMB', + 'GRADERS', + 'MUNFITR', + 'OGOILSW', + 'OGSERVC', + 'OGSRRAH', + 'PICKRTT', + 'TOWVEHC', + ], + rules: [ + { + conditions: { + any: [ + { + not: { + fact: 'permitData.permitDuration', + operator: 'lessThanInclusive', + value: 7, + }, + }, + { + not: { + fact: 'permitData.permitDuration', + operator: 'greaterThan', + value: 0, + }, + }, + ], + }, + event: { + type: 'violation', + params: { + message: 'Duration must be 7 days or less', + code: 'field-validation-error', + fieldReference: 'permitData.permitDuration', + }, + }, + }, + { + conditions: { + not: { + fact: 'permitData.vehicleDetails.vehicleSubType', + operator: 'in', + value: { + fact: 'allowedVehicles', + }, + }, + }, + event: { + type: 'violation', + params: { + message: 'Vehicle type not permittable for this permit type', + code: 'field-validation-error', + fieldReference: 'permitData.vehicleDetails.vehicleSubType', + }, + }, + }, + ], + costRules: [ + { + fact: 'fixedCost', + params: { + cost: 15, + }, + }, + ], + }, + { + id: 'TROS', + name: 'Term Oversize', + routingRequired: false, + weightDimensionRequired: false, + sizeDimensionRequired: false, + commodityRequired: false, + allowedVehicles: ['TRKTRAC'], + rules: [], + }, +], + globalWeightDefaults: { + powerUnits: [], + trailers: [], + }, + vehicleCategories: { + trailerCategories: [ + { + id: 'trailer', + name: 'Default trailer category', + }, + { + id: 'accessory', + name: 'Accessory trailer such as jeep or booster, to be used alongside other trailers. Not permittable on its own as a trailer in a combination.', + }, + { + id: 'pseudo', + name: 'Placeholder for a trailer in a combination with no trailer (such as when a brushcutter is permitted with no trailer).', + }, + ], + powerUnitCategories: [ + { + id: 'powerunit', + name: 'Default power unit category', + }, + ], + }, + vehicleTypes: { + powerUnitTypes: [ + { + id: 'BUSCRUM', + name: 'Buses/Crummies', + category: 'powerunit', + }, + { + id: 'BUSTRLR', + name: 'Intercity Buses (Pulling Pony Trailers)', + category: 'powerunit', + }, + { + id: 'CONCRET', + name: 'Concrete Pumper Trucks', + category: 'powerunit', + }, + { + id: 'CRAFTAT', + name: 'Cranes, Rubber-Tired Loaders, Firetrucks - All Terrain', + category: 'powerunit', + }, + { + id: 'CRAFTMB', + name: 'Cranes, Rubber-Tired Loaders, Firetrucks - Mobile', + category: 'powerunit', + }, + { + id: 'DDCKBUS', + name: 'Double Decker Buses', + category: 'powerunit', + }, + { + id: 'FARMVEH', + name: 'Farm Vehicles', + category: 'powerunit', + }, + { + id: 'GRADERS', + name: 'Fixed Equipment - Trucks/Graders etc.', + category: 'powerunit', + }, + { + id: 'LCVRMDB', + name: 'Long Combination Vehicles (LCV) - Rocky Mountain Doubles', + category: 'powerunit', + }, + { + id: 'LCVTPDB', + name: 'Long Combination Vehicles (LCV) - Turnpike Doubles', + category: 'powerunit', + }, + { + id: 'LOGGING', + name: 'Logging Trucks', + category: 'powerunit', + }, + { + id: 'LOGOFFH', + name: 'Logging Trucks - Off-Highway', + category: 'powerunit', + }, + { + id: 'LWBTRCT', + name: 'Long Wheelbase Truck Tractors Exceeding 6.2 m up to 7.25 m', + category: 'powerunit', + }, + { + id: 'MUNFITR', + name: 'Municipal Fire Trucks', + category: 'powerunit', + }, + { + id: 'OGBEDTK', + name: 'Oil and Gas - Bed Trucks', + category: 'powerunit', + }, + { + id: 'OGOILSW', + name: 'Oil and Gas - Oilfield Sows', + category: 'powerunit', + }, + { + id: 'OGSERVC', + name: 'Oil and Gas - Service Rigs', + category: 'powerunit', + }, + { + id: 'OGSRRAH', + name: 'Oil and Gas - Service Rigs and Rathole Augers Only Equipped with Heavy Front Projected Crane (must exceed 14,000 kg tare weight)', + category: 'powerunit', + }, + { + id: 'PICKRTT', + name: 'Picker Truck Tractors', + category: 'powerunit', + }, + { + id: 'PLOWBLD', + name: 'Trucks Equipped with Front or Underbody Plow Blades', + category: 'powerunit', + }, + { + id: 'PUTAXIS', + name: 'Taxis', + category: 'powerunit', + }, + { + id: 'REGTRCK', + name: 'Trucks', + category: 'powerunit', + }, + { + id: 'SCRAPER', + name: 'Scrapers', + category: 'powerunit', + }, + { + id: 'SPAUTHV', + name: 'Specially Authorized Vehicles', + category: 'powerunit', + }, + { + id: 'STINGER', + name: 'Truck Tractors - Stinger Steered', + category: 'powerunit', + }, + { + id: 'TOWVEHC', + name: 'Tow Vehicles', + category: 'powerunit', + }, + { + id: 'TRKTRAC', + name: 'Truck Tractors', + category: 'powerunit', + }, + ], + trailerTypes: [ + { + id: 'BOOSTER', + name: 'Boosters', + category: 'accessory', + ignoreForSizeDimensions: true, + }, + { + id: 'DBTRBTR', + name: 'Tandem/Tridem Drive B-Train (Super B-Train)', + category: 'trailer', + }, + { + id: 'DOLLIES', + name: 'Dollies', + category: 'trailer', + }, + { + id: 'EXPANDO', + name: 'Expando Semi-Trailers', + category: 'trailer', + }, + { + id: 'FEBGHSE', + name: 'Fixed Equipment - Portable Asphalt Baghouses', + category: 'trailer', + }, + { + id: 'FECVYER', + name: 'Fixed Equipment - Conveyors', + category: 'trailer', + }, + { + id: 'FEDRMMX', + name: 'Fixed Equipment - Counter Flow Asphalt Drum Mixers', + category: 'trailer', + }, + { + id: 'FEPNYTR', + name: 'Fixed Equipment - Pony Trailers', + category: 'trailer', + }, + { + id: 'FESEMTR', + name: 'Fixed Equipment - Semi-Trailers', + category: 'trailer', + }, + { + id: 'FEWHELR', + name: 'Fixed Equipment - Wheeler Semi-Trailers', + category: 'wheeler', + }, + { + id: 'FLOATTR', + name: 'Float Trailers', + category: 'wheeler', + }, + { + id: 'FULLLTL', + name: 'Full Trailers', + category: 'trailer', + }, + { + id: 'HIBOEXP', + name: 'Semi-Trailers - Hiboys/Expandos', + category: 'trailer', + }, + { + id: 'HIBOFLT', + name: 'Semi-Trailers - Hiboys/Flat Decks', + category: 'trailer', + }, + { + id: 'JEEPSRG', + name: 'Jeeps', + category: 'accessory', + ignoreForSizeDimensions: true, + }, + { + id: 'LOGDGLG', + name: 'Legacy Logging Trailer Combinations - Tandem Pole Trailers, Dogloggers', + category: 'trailer', + }, + { + id: 'LOGFULL', + name: 'Logging Trailers - Full Trailers, Tri Axle, Quad Axle', + category: 'trailer', + }, + { + id: 'LOGNTAC', + name: 'Legacy Logging Trailer Combinations - Non-TAC B-Trains', + category: 'trailer', + }, + { + id: 'LOGOWBK', + name: 'Logging Trailers - Overwidth Bunks', + category: 'trailer', + }, + { + id: 'LOGSMEM', + name: 'Logging Semi-Trailer - Empty, 3.2 m Bunks', + category: 'trailer', + }, + { + id: 'LOGTNDM', + name: 'Legacy Logging Trailer Combinations - Single Axle Jeeps, Tandem Axle Pole Trailers, Dogloggers', + category: 'trailer', + }, + { + id: 'LOGTRIX', + name: 'Legacy Logging Trailer Combinations - Single Axle Jeeps, Tri Axle Trailers', + category: 'trailer', + }, + { + id: 'MHMBSHG', + name: 'Manufactured Homes, Modular Buildings, Structures and Houseboats (> 5.0 m OAW) with Attached Axles', + category: 'trailer', + }, + { + id: 'MHMBSHL', + name: 'Manufactured Homes, Modular Buildings, Structures and Houseboats (<= 5.0 m OAW) with Attached Axles', + category: 'trailer', + }, + { + id: 'ODTRLEX', + name: 'Overdimensional Trailers and Semi-Trailers (For Export)', + category: 'trailer', + }, + { + id: 'OGOSFDT', + name: 'Oil and Gas - Oversize Oilfield Flat Deck Semi-Trailers', + category: 'trailer', + }, + { + id: 'PLATFRM', + name: 'Platform Trailers', + category: 'trailer', + }, + { + id: 'PMHWAAX', + name: 'Park Model Homes with Attached Axles', + category: 'trailer', + }, + { + id: 'POLETRL', + name: 'Pole Trailers', + category: 'trailer', + }, + { + id: 'PONYTRL', + name: 'Pony Trailers', + category: 'trailer', + }, + { + id: 'REDIMIX', + name: 'Ready Mix Concrete Pump Semi-Trailers', + category: 'trailer', + }, + { + id: 'SEMITRL', + name: 'Semi-Trailers', + category: 'trailer', + }, + { + id: 'STACTRN', + name: 'Semi-Trailers - A-Trains and C-Trains', + category: 'trailer', + }, + { + id: 'STBTRAN', + name: 'Semi-Trailers - B-Trains', + category: 'trailer', + }, + { + id: 'STCHIPS', + name: 'Semi-Trailers - Walled B-Trains (Chip Trucks)', + category: 'trailer', + }, + { + id: 'STCRANE', + name: 'Semi-Trailers with Crane', + category: 'trailer', + }, + { + id: 'STINGAT', + name: 'Stinger Steered Automobile Transporters', + category: 'trailer', + }, + { + id: 'STLOGNG', + name: 'Semi-Trailers - Logging', + category: 'trailer', + }, + { + id: 'STNTSHC', + name: 'Semi-Trailers - Non-Tac Short Chassis', + category: 'trailer', + }, + { + id: 'STREEFR', + name: 'Semi-Trailers - Insulated Vans with Reefer/Refrigeration Units', + category: 'trailer', + }, + { + id: 'STROPRT', + name: 'Steering Trailers - Manned', + category: 'trailer', + }, + { + id: 'STRSELF', + name: 'Steering Trailers - Self/Remote', + category: 'trailer', + }, + { + id: 'STSDBDK', + name: 'Semi-Trailers - Single Drop, Double Drop, Step Decks, Lowbed, Expandos, etc.', + category: 'trailer', + }, + { + id: 'STSTEER', + name: 'Semi-Trailers - Steering Trailers', + category: 'trailer', + }, + { + id: 'STSTNGR', + name: 'Semi-Trailers - Stinger Steered Automobile Transporters', + category: 'trailer', + }, + { + id: 'STWDTAN', + name: 'Semi-Trailers - Spread Tandems', + category: 'trailer', + }, + { + id: 'STWHELR', + name: 'Semi-Trailers - Wheelers', + category: 'trailer', + }, + { + id: 'STWIDWH', + name: 'Semi-Trailers - Wide Wheelers', + category: 'trailer', + }, + { + id: 'NONEXXX', + name: 'No trailer', + category: 'pseudo', + }, + ], + }, + commodities: [ + { + id: 'WODWIDB', + name: 'Wood on wide bunks', + }, + { + id: 'LEGALLO', + name: 'Legal loads', + }, + { + id: 'GRADERS', + name: 'Grader', + }, + { + id: 'HUSBAND', + name: 'Implements of husbandry', + }, + { + id: 'OILFILD', + name: 'Oil field equipment', + }, + { + id: 'ROKTRUK', + name: 'Rock truck', + }, + { + id: 'TIRESXX', + name: 'Tires', + }, + { + id: 'SRVCRIG', + name: 'Service rig', + }, + { + id: 'BUNCHER', + name: 'Buncher', + }, + { + id: 'CONVEYR', + name: 'Conveyor', + }, + { + id: 'SKIDUNT', + name: 'Oil field skid unit', + }, + { + id: 'EMPTYXX', + name: 'Empty', + powerUnits: [ + { + type: 'TRKTRAC', + canFollow: [], + }, + { + type: 'PICKRTT', + canFollow: [], + }, + ], + trailers: [ + { + type: 'JEEPSRG', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + }, + { + type: 'BOOSTER', + canFollow: [ + 'OGOSFDT', + 'PLATFRM', + 'HIBOEXP', + 'STWHELR', + 'STWIDWH', + 'STCRANE', + 'HIBOFLT', + 'STSDBDK', + 'BOOSTER', + ], + }, + { + type: 'LOGOWBK', + canFollow: ['TRKTRAC'], + }, + { + type: 'OGOSFDT', + canFollow: ['PICKRTT', 'JEEPSRG'], + sizeDimensions: [ + { + width: 3.2, + height: 4.3, + length: 23, + }, + ], + }, + { + type: 'PLATFRM', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + canSelfIssue: false, + sizeDimensions: [ + { + width: 3.2, + height: 4.88, + length: 27.5, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'HIBOEXP', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + sizeDimensions: [ + { + height: 4.4, + length: 25, + width: 2.5, + regions: [ + { + region: 'PCE', + height: 5.33, + length: 27.5, + width: 2.8 + }, + { + region: 'KTN', + height: 4.0, + length: 30, + }, + { + region: 'LMN', + height: 4.5, + length: 24, + } + ], + modifiers: [ + { + position: 'first', + type: 'PICKRTT', + }, + ], + }, + { + height: 4.4, + length: 31, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + modifiers: [ + { + position: 'first', + type: 'TRKTRAC', + }, + ], + }, + ], + }, + { + type: 'STWHELR', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + canSelfIssue: false, + sizeDimensions: [ + { + width: 3.2, + height: 4.88, + length: 27.5, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'STWIDWH', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + canSelfIssue: false, + sizeDimensions: [ + { + width: 3.2, + height: 4.88, + length: 27.5, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'STCRANE', + canFollow: ['TRKTRAC', 'PICKRTT', 'JEEPSRG'], + sizeDimensions: [ + { + width: 3.2, + height: 4.88, + length: 27.5, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'HIBOFLT', + canFollow: ['TRKTRAC', 'JEEPSRG'], + sizeDimensions: [ + { + height: 4.4, + length: 27.5, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'STSDBDK', + canFollow: ['TRKTRAC', 'JEEPSRG'], + sizeDimensions: [ + { + width: 3.2, + height: 4.4, + length: 31, + regions: [ + { + region: 'PCE', + height: 5.33, + }, + ], + }, + ], + }, + ], + }, + { + id: 'GENERAL', + name: 'General commodities', + }, + { + id: 'CULVERT', + name: 'Culverts', + }, + { + id: 'RAMPSXX', + name: 'Ramps', + }, + { + id: 'REBARXX', + name: 'Rebar', + }, + { + id: 'RESTEEL', + name: 'Reinforcing steel', + }, + { + id: 'STSTEEL', + name: 'Structural steel', + }, + { + id: 'PIPEXXX', + name: 'Pipe', + }, + { + id: 'GRBBINS', + name: 'Garbage bins', + }, + { + id: 'BOATSXX', + name: 'Boats', + }, + { + id: 'BOOMSTK', + name: 'Boomsticks', + }, + { + id: 'CONTANR', + name: 'Containers', + }, + { + id: 'HAYROND', + name: 'Hay bales round', + }, + { + id: 'HAYLREC', + name: 'Hay bales large rectangular', + }, + { + id: 'HAYBALE', + name: 'Hay bales', + }, + { + id: 'HAYSREC', + name: 'Hay bales small rectangular', + }, + { + id: 'BUILDNG', + name: 'House or building', + }, + { + id: 'LONGLOG', + name: 'Long logs', + }, + { + id: 'PILINGS', + name: 'Piling', + }, + { + id: 'POLESXX', + name: 'Poles', + }, + { + id: 'RUGHLUM', + name: 'Rough cut lumber', + }, + { + id: 'VENEERX', + name: 'Veneer', + }, + { + id: 'MODULAR', + name: 'Modular building', + }, + { + id: 'MOBHOME', + name: 'Mobile home manufactured', + }, + { + id: 'PARKTRL', + name: 'Park model trailers', + }, + { + id: 'AIRCRFT', + name: 'Aircraft', + }, + { + id: 'BACKHOE', + name: 'Backhoes', + }, + { + id: 'BRIDGES', + name: 'Bridges', + }, + { + id: 'BRGBEAM', + name: 'Bridge beams', + powerUnits: [ + { + type: 'TRKTRAC', + canFollow: [], + }, + ], + trailers: [ + { + type: 'JEEPSRG', + canFollow: ['TRKTRAC', 'JEEPSRG'], + }, + { + type: 'BOOSTER', + canFollow: ['POLETRL', 'BOOSTER'], + }, + { + type: 'POLETRL', + canFollow: ['TRKTRAC', 'JEEPSRG'], + }, + ], + }, + { + id: 'BARGESX', + name: 'Barges', + }, + { + id: 'BLADESX', + name: 'Blades', + }, + { + id: 'BUCKETS', + name: 'Buckets', + }, + { + id: 'CATCRLR', + name: 'Cat or crawler', + }, + { + id: 'COALTRK', + name: 'Coal truck', + }, + { + id: 'CMPRSOR', + name: 'Compressor', + }, + { + id: 'CRANESX', + name: 'Cranes', + }, + { + id: 'CRAWLER', + name: 'Crawler', + }, + { + id: 'CRUSHER', + name: 'Crusher parts', + }, + { + id: 'EXCVATR', + name: 'Excavator', + }, + { + id: 'FORKLFT', + name: 'Fork lifts', + }, + { + id: 'GENRTOR', + name: 'Generator', + }, + { + id: 'LOADERX', + name: 'Loader', + }, + { + id: 'LOGGING', + name: 'Logging machinery', + }, + { + id: 'LOGLODR', + name: 'Log loader', + }, + { + id: 'MINEMAC', + name: 'Mine machinery', + }, + { + id: 'MILLMAC', + name: 'Mill machinery', + }, + { + id: 'NONREDU', + name: 'Non reducible loads', + }, + { + id: 'OILDRRG', + name: 'Oil drill rig', + }, + { + id: 'OREBOXS', + name: 'Ore boxes', + }, + { + id: 'PLSTEEL', + name: 'Plate steel', + }, + { + id: 'STBEAMS', + name: 'Steel beams', + }, + { + id: 'SHOVELS', + name: 'Shovels', + }, + { + id: 'SKIDDER', + name: 'Skidder', + }, + { + id: 'TRUSSES', + name: 'Trusses', + }, + { + id: 'TANKSXX', + name: 'Tanks', + }, + { + id: 'TRNSFRM', + name: 'Transformer', + }, + { + id: 'TWRTUBE', + name: 'Tower tube', + }, + { + id: 'VESSELS', + name: 'Vessels', + }, + { + id: 'LMBEAMS', + name: 'Laminated beams', + }, + { + id: 'YARDERX', + name: 'Yarder', + }, + { + id: 'SKDSTAK', + name: 'Skid stack', + }, + { + id: 'OTHERVE', + name: 'Other vehicle', + }, + { + id: 'SCRAPER', + name: 'Scrapers', + }, + { + id: 'BMCRANE', + name: 'Booms (cranes)', + }, + { + id: 'GENVEHC', + name: 'General - vehicle and loads', + }, + { + id: 'GENMOHO', + name: 'General - mobile homes', + }, + { + id: 'AUTOCRR', + name: 'Auto carrier, campers and boats (stinger steered)', + powerUnits: [ + { + type: 'STINGER', + canFollow: [], + }, + ], + trailers: [ + { + type: 'STSTNGR', + canFollow: ['STINGER'], + sizeDimensions: [ + { + frontProjection: 1, + rearProjection: 1.2, + width: 2.6, + height: 4.4, + length: 25, + regions: [ + { + region: 'LMN', + height: 4.3, + }, + { + region: 'KTN', + height: 4.3, + }, + { + region: 'PCE', + height: 4.88, + }, + ], + }, + ], + }, + ], + }, + { + id: 'BMPOLES', + name: 'Boomsticks and poles', + }, + { + id: 'HAYRNPR', + name: 'Haybales (round) Peace River only', + }, + { + id: 'BUSPONY', + name: 'Inter-city bus with pony trailer', + }, + { + id: 'TOWTRCK', + name: 'Tow trucks and disabled vehicles', + }, + { + id: 'BRSHCUT', + name: 'Brushcutters (Peace only)', + powerUnits: [ + { + type: 'TRKTRAC', + canFollow: [], + }, + { + type: 'REGTRCK', + canFollow: [], + }, + ], + trailers: [ + { + type: 'JEEPSRG', + canFollow: ['TRKTRAC', 'JEEPSRG'], + }, + { + type: 'BOOSTER', + canFollow: ['STSDBDK', 'BOOSTER'], + }, + { + type: 'SEMITRL', + canFollow: ['TRKTRAC'], + sizeDimensions: [ + { + length: 23, + regions: [ + { + region: 'PCE', + width: 4.57, + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'STSDBDK', + canFollow: ['TRKTRAC', 'JEEPSRG'], + sizeDimensions: [ + { + length: 23, + regions: [ + { + region: 'PCE', + width: 4.57, + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'FULLLTL', + canFollow: ['REGTRCK'], + sizeDimensions: [ + { + frontProjection: 1, + rearProjection: 1, + length: 23, + regions: [ + { + region: 'PCE', + width: 4.57, + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'NONEXXX', + canFollow: ['REGTRCK'], + sizeDimensions: [ + { + frontProjection: 1, + rearProjection: 1, + length: 12.5, + regions: [ + { + region: 'PCE', + width: 4.57, + height: 5.33, + }, + ], + }, + ], + }, + { + type: 'PONYTRL', + canFollow: ['REGTRCK'], + sizeDimensions: [ + { + frontProjection: 1, + rearProjection: 1, + length: 23, + regions: [ + { + region: 'PCE', + width: 4.57, + height: 5.33, + }, + ], + }, + ], + }, + ], + }, + { + id: 'FIXEDEQ', + name: 'Fixed equipment', + }, + ], + globalSizeDefaults: { + frontProjection: 3, + rearProjection: 6.5, + width: 2.6, + height: 4.15, + length: 31, + }, +}; diff --git a/policy-engine/src/_test/policy-config/tros-multiple-cost-rules.sample.ts b/policy-engine/src/_test/policy-config/tros-multiple-cost-rules.sample.ts new file mode 100644 index 000000000..cd53d7bea --- /dev/null +++ b/policy-engine/src/_test/policy-config/tros-multiple-cost-rules.sample.ts @@ -0,0 +1,53 @@ +import { PolicyDefinition } from '../../types/policy-definition'; + +export const multipleCostRules: PolicyDefinition = { + version: '2024.03.18.001', + geographicRegions: [], + commonRules: [], + permitTypes: [ + { + id: 'TROS', + name: 'Term Oversize', + routingRequired: false, + weightDimensionRequired: false, + sizeDimensionRequired: false, + commodityRequired: false, + allowedVehicles: [], + rules: [], + costRules: [ + { + fact: 'costPerMonth', + params: { + cost: 30, + }, + }, + { + fact: 'fixedCost', + params: { + cost: 15, + }, + }, + ], + }, + ], + globalWeightDefaults: { + powerUnits: [], + trailers: [], + }, + vehicleCategories: { + trailerCategories: [], + powerUnitCategories: [], + }, + vehicleTypes: { + powerUnitTypes: [], + trailerTypes: [], + }, + commodities: [], + globalSizeDefaults: { + frontProjection: 3, + rearProjection: 6.5, + width: 2.6, + height: 4.15, + length: 31, + }, +}; diff --git a/policy-engine/src/_test/unit/configuration.spec.ts b/policy-engine/src/_test/unit/configuration.spec.ts new file mode 100644 index 000000000..2565158d0 --- /dev/null +++ b/policy-engine/src/_test/unit/configuration.spec.ts @@ -0,0 +1,104 @@ +import { Policy } from "../../policy-engine"; +import { stosPolicyConfig } from "../policy-config/stos-vehicle-config.sample"; + +describe('Permit Engine Oversize Configuration Functions', () => { + const policy: Policy = new Policy(stosPolicyConfig); + + it('should retrieve all permittable power unit types for STOS', async () => { + const puTypes = policy.getPermittablePowerUnitTypes('STOS', 'EMPTYXX'); + expect(puTypes.size).toBe(2); + expect(puTypes.keys()).toContain('TRKTRAC'); + expect(puTypes.keys()).toContain('PICKRTT'); + }); + + it('should return an empty map for invalid permit type', async () => { + const puTypes = policy.getPermittablePowerUnitTypes('_INVALID', 'EMPTYXX'); + expect(puTypes.size).toBe(0) + }); + + it('should return an empty map for invalid commodity', async () => { + const puTypes = policy.getPermittablePowerUnitTypes('STOS', '_INVALID'); + expect(puTypes.size).toBe(0) + }); + + it('should get all correct vehicles for a given commodity', async () => { + const vehicles = policy.getAllVehiclesForCommodity('STOS', 'BRGBEAM'); + expect(vehicles.length).toBe(4); + expect(vehicles.map((v) => v.type)).toContain('TRKTRAC'); + expect(vehicles.map((v) => v.type)).toContain('POLETRL'); + }); +}); + +describe('Permit Engine Get Next Permittable Vehicles', () => { + const policy: Policy = new Policy(stosPolicyConfig); + + it('should return permittable power units with empty current configuration', async () => { + const vehicles = policy.getNextPermittableVehicles('STOS', 'BRGBEAM', []); + expect(vehicles.size).toBe(1); + expect(vehicles.keys()).toContain('TRKTRAC'); + }); + + it('should return jeep and a trailer when current config is just power unit', async () => { + const vehicles = policy.getNextPermittableVehicles('STOS', 'BRGBEAM', ['TRKTRAC']); + expect(vehicles.size).toBe(2); + expect(vehicles.keys()).toContain('JEEPSRG'); + expect(vehicles.keys()).toContain('POLETRL'); + }); + + it('should return jeep and a trailer when current config is power unit and jeep', async () => { + const vehicles = policy.getNextPermittableVehicles('STOS', 'BRGBEAM', ['TRKTRAC', 'JEEPSRG']); + expect(vehicles.size).toBe(2); + expect(vehicles.keys()).toContain('JEEPSRG'); + expect(vehicles.keys()).toContain('POLETRL'); + }); + + it('should return booster when current config is power unit and trailer', async () => { + const vehicles = policy.getNextPermittableVehicles('STOS', 'BRGBEAM', ['TRKTRAC', 'POLETRL']); + expect(vehicles.size).toBe(1); + expect(vehicles.keys()).toContain('BOOSTER'); + }); + + it('should return empty map when current configuration is invalid', async () => { + const vehicles = policy.getNextPermittableVehicles('STOS', 'BRGBEAM', ['TRKTRAC', '_INVALID']); + expect(vehicles.size).toBe(0); + }); +}); + +describe('Permit Engine Configuration Validation', () => { + const policy: Policy = new Policy(stosPolicyConfig); + + it('should return true for a valid configuration with power unit and trailer', async () => { + const isValid = policy.isConfigurationValid('STOS', 'BRGBEAM', ['TRKTRAC', 'POLETRL']); + expect(isValid).toBe(true); + }); + + it('should return true for a valid configuration with power unit and trailer and jeep and booster', async () => { + const isValid = policy.isConfigurationValid('STOS', 'BRGBEAM', ['TRKTRAC', 'JEEPSRG', 'POLETRL', 'BOOSTER']); + expect(isValid).toBe(true); + }); + + it('should return false for a configuration out of order', async () => { + const isValid = policy.isConfigurationValid('STOS', 'BRGBEAM', ['TRKTRAC', 'POLETRL', 'JEEPSRG', 'BOOSTER']); + expect(isValid).toBe(false); + }); + + it('should return false for an invalid permit type', async () => { + const isValid = policy.isConfigurationValid('_INVALID', 'BRGBEAM', ['TRKTRAC', 'POLETRL']); + expect(isValid).toBe(false); + }); + + it('should return false for an invalid commodity', async () => { + const isValid = policy.isConfigurationValid('STOS', '_INVALID', ['TRKTRAC', 'POLETRL']); + expect(isValid).toBe(false); + }); + + it('should return false for a configuration missing a trailer', async () => { + const isValid = policy.isConfigurationValid('STOS', 'BRGBEAM', ['TRKTRAC', 'JEEPSRG']); + expect(isValid).toBe(false); + }); + + it('should return false for a permit type not requiring commodity', async () => { + const isValid = policy.isConfigurationValid('TROS', 'BRGBEAM', ['TRKTRAC']); + expect(isValid).toBe(false); + }); +}); \ No newline at end of file diff --git a/policy-engine/src/_test/unit/cost.spec.ts b/policy-engine/src/_test/unit/cost.spec.ts new file mode 100644 index 000000000..5bfce40aa --- /dev/null +++ b/policy-engine/src/_test/unit/cost.spec.ts @@ -0,0 +1,109 @@ +import { Policy } from 'onroute-policy-engine'; +import { completePolicyConfig } from '../policy-config/complete-in-progress.sample'; +import { multipleCostRules } from '../policy-config/tros-multiple-cost-rules.sample'; +import { validTros30Day } from '../permit-app/valid-tros-30day'; +import { validTrow120Day } from '../permit-app/valid-trow-120day'; +import { testStos } from '../permit-app/test-stos'; +import dayjs from 'dayjs'; +import { PermitAppInfo } from '../../enum/permit-app-info'; + +describe('Policy Engine Cost Calculator', () => { + const policy: Policy = new Policy(completePolicyConfig); + const multipleCostRulesPolicy = new Policy(multipleCostRules); + + it('should calculate 30 day TROS cost correctly', async () => { + const permit = JSON.parse(JSON.stringify(validTros30Day)); + // Set startDate to today + permit.permitData.startDate = dayjs().format( + PermitAppInfo.PermitDateFormat.toString(), + ); + + const validationResult = await policy.validate(permit); + expect(validationResult.cost).toHaveLength(1); + expect(validationResult.cost[0].cost).toBe(30); + }); + + it('should calculate 31 day TROS cost as 2 months', async () => { + const permit = JSON.parse(JSON.stringify(validTros30Day)); + // Set startDate to today + permit.permitData.startDate = dayjs().format( + PermitAppInfo.PermitDateFormat.toString(), + ); + // Set duration to 31 + permit.permitData.permitDuration = 31; + + const validationResult = await policy.validate(permit); + expect(validationResult.cost).toHaveLength(1); + expect(validationResult.cost[0].cost).toBe(60); + }); + + it('should calculate 1 year TROS cost correctly', async () => { + const permit = JSON.parse(JSON.stringify(validTros30Day)); + const today = dayjs(); + // Set startDate to today + permit.permitData.startDate = today.format( + PermitAppInfo.PermitDateFormat.toString(), + ); + // Set duration to full year (365 or 366 depending on leap year) + const oneYearDuration: number = today.add(1, 'year').diff(today, 'day'); + permit.permitData.permitDuration = oneYearDuration; + + const validationResult = await policy.validate(permit); + expect(validationResult.cost).toHaveLength(1); + expect(validationResult.cost[0].cost).toBe(360); + }); + + it('should calculate 120 day TROW cost correctly', async () => { + const permit = JSON.parse(JSON.stringify(validTrow120Day)); + // Set startDate to today + permit.permitData.startDate = dayjs().format( + PermitAppInfo.PermitDateFormat.toString(), + ); + + const validationResult = await policy.validate(permit); + expect(validationResult.cost).toHaveLength(1); + expect(validationResult.cost[0].cost).toBe(400); + }); + + it('should calculate STOS cost correctly', async () => { + const permit = JSON.parse(JSON.stringify(testStos)); + // Set startDate to today + permit.permitData.startDate = dayjs().format( + PermitAppInfo.PermitDateFormat.toString(), + ); + + const validationResult = await policy.validate(permit); + expect(validationResult.cost).toHaveLength(1); + expect(validationResult.cost[0].cost).toBe(15); + }); + + it('should calculate valid TROS with multiple cost rules correctly', async () => { + const permit = JSON.parse(JSON.stringify(validTros30Day)); + // Set startDate to today + permit.permitData.startDate = dayjs().format( + PermitAppInfo.PermitDateFormat.toString(), + ); + + const validationResult = await multipleCostRulesPolicy.validate(permit); + expect(validationResult.cost).toHaveLength(2); + const cost1: number = validationResult.cost[0]?.cost || 0; + const cost2: number = validationResult.cost[1]?.cost || 0; + expect(cost1 + cost2).toBe(45); + }); + + it('should calculate 31 day TROS with multiple cost rules correctly', async () => { + const permit = JSON.parse(JSON.stringify(validTros30Day)); + // Set startDate to today + permit.permitData.startDate = dayjs().format( + PermitAppInfo.PermitDateFormat.toString(), + ); + // Set duration to 31 + permit.permitData.permitDuration = 31; + + const validationResult = await multipleCostRulesPolicy.validate(permit); + expect(validationResult.cost).toHaveLength(2); + const cost1: number = validationResult.cost[0]?.cost || 0; + const cost2: number = validationResult.cost[1]?.cost || 0; + expect(cost1 + cost2).toBe(75); + }); +}); diff --git a/policy-engine/src/_test/unit/size-dimension.spec.ts b/policy-engine/src/_test/unit/size-dimension.spec.ts new file mode 100644 index 000000000..ae9648657 --- /dev/null +++ b/policy-engine/src/_test/unit/size-dimension.spec.ts @@ -0,0 +1,71 @@ +import { Policy } from "../../policy-engine"; +import { stosPolicyConfig } from "../policy-config/stos-vehicle-config.sample"; + +describe('Permit Engine Size Dimension Functions', () => { + const policy: Policy = new Policy(stosPolicyConfig); + + it('should assume all regions if none are supplied', async () => { + // This configuration has minimum values pulled from multiple regions + const sizeDimension = policy.getSizeDimension('STOS', 'EMPTYXX', ['PICKRTT', 'JEEPSRG', 'HIBOEXP']); + expect(sizeDimension?.height).toBe(4.0); + expect(sizeDimension?.width).toBe(2.5); + expect(sizeDimension?.length).toBe(24); + }); + + it('should retrieve correct values for a single specified region', async () => { + const sizeDimension = policy.getSizeDimension('STOS', 'EMPTYXX', ['PICKRTT', 'JEEPSRG', 'HIBOEXP'], ['PCE']); + expect(sizeDimension?.height).toBe(5.33); + expect(sizeDimension?.width).toBe(2.8); + expect(sizeDimension?.length).toBe(27.5); + }); + + it('should retrieve correct values for a configuration with modifier', async () => { + const sizeDimension = policy.getSizeDimension('STOS', 'EMPTYXX', ['TRKTRAC', 'JEEPSRG', 'HIBOEXP']); + expect(sizeDimension?.height).toBe(4.4); + expect(sizeDimension?.width).toBe(2.6); + expect(sizeDimension?.length).toBe(31); + }); + + it('should revert to global defaults if no specific values configured', async () => { + const sizeDimension = policy.getSizeDimension('STOS', 'EMPTYXX', ['TRKTRAC', 'LOGOWBK']); + expect(sizeDimension?.frontProjection).toBe(3); + expect(sizeDimension?.rearProjection).toBe(6.5); + expect(sizeDimension?.height).toBe(4.15); + expect(sizeDimension?.width).toBe(2.6); + expect(sizeDimension?.length).toBe(31); + }); + + it('should return default values if an unconfigured region is specified', async () => { + const sizeDimension = policy.getSizeDimension('STOS', 'EMPTYXX', ['TRKTRAC', 'JEEPSRG', 'STWHELR'], ['LMN']); + expect(sizeDimension?.height).toBe(4.88); + expect(sizeDimension?.width).toBe(3.2); + expect(sizeDimension?.length).toBe(27.5); + }); + + it('should return default values if an invalid region is specified', async () => { + const sizeDimension = policy.getSizeDimension('STOS', 'EMPTYXX', ['TRKTRAC', 'JEEPSRG', 'STWHELR'], ['_INVALID']); + expect(sizeDimension?.height).toBe(4.88); + expect(sizeDimension?.width).toBe(3.2); + expect(sizeDimension?.length).toBe(27.5); + }); + + it('should return null if an invalid permit type is specified', async () => { + const sizeDimension = policy.getSizeDimension('_INVALID', 'EMPTYXX', ['TRKTRAC', 'JEEPSRG', 'STWHELR']); + expect(sizeDimension).toBeNull(); + }); + + it('should return null if an invalid configuration is specified', async () => { + const sizeDimension = policy.getSizeDimension('STOS', 'EMPTYXX', ['_INVALID', 'JEEPSRG', 'STWHELR']); + expect(sizeDimension).toBeNull(); + }); + + it('should return null if no dimensionable trailer is specified', async () => { + const sizeDimension = policy.getSizeDimension('STOS', 'EMPTYXX', ['TRKTRAC', 'JEEPSRG']); + expect(sizeDimension).toBeNull(); + }); + + it('should return null if an invalid commodity is specified', async () => { + const sizeDimension = policy.getSizeDimension('STOS', '_INVALID', ['TRKTRAC', 'JEEPSRG']); + expect(sizeDimension).toBeNull(); + }); +}); \ No newline at end of file diff --git a/policy-engine/src/_test/unit/policy-engine-utility.spec.ts b/policy-engine/src/_test/unit/utility.spec.ts similarity index 83% rename from policy-engine/src/_test/unit/policy-engine-utility.spec.ts rename to policy-engine/src/_test/unit/utility.spec.ts index c7c4fac4e..148ea6cab 100644 --- a/policy-engine/src/_test/unit/policy-engine-utility.spec.ts +++ b/policy-engine/src/_test/unit/utility.spec.ts @@ -21,6 +21,11 @@ describe('Permit Engine Utility Functions', () => { expect(commodities.size).toBe(12); }); + it('should return the correct number of commodities for a single permit type', async () => { + const commodities: Map = policy.getCommodities('STOS'); + expect(commodities.size).toBe(5); + }); + it('should return the correct number of power unit types', async () => { const powerUnitTypes: Map = policy.getPowerUnitTypes(); expect(powerUnitTypes.size).toBe(3); @@ -43,3 +48,7 @@ describe('Permit Engine Utility Functions', () => { expect(permitType).toBeNull(); }); }); + +describe('Permit Engine Utility Functions', () => { + const policy: Policy = new Policy(fiveTypes); +}); diff --git a/policy-engine/src/_test/unit/policy-engine-validation.spec.ts b/policy-engine/src/_test/unit/validation.spec.ts similarity index 100% rename from policy-engine/src/_test/unit/policy-engine-validation.spec.ts rename to policy-engine/src/_test/unit/validation.spec.ts diff --git a/policy-engine/src/enum/facts.ts b/policy-engine/src/enum/facts.ts index 52936e848..0172e4e41 100644 --- a/policy-engine/src/enum/facts.ts +++ b/policy-engine/src/enum/facts.ts @@ -2,4 +2,7 @@ export enum PolicyFacts { AllowedVehicles = 'allowedVehicles', DaysInPermitYear = 'daysInPermitYear', ValidationDate = 'validationDate', + FixedCost = 'fixedCost', + CostPerMonth = 'costPerMonth', + RangeMatrixCostLookup = 'rangeMatrixCostLookup', } diff --git a/policy-engine/src/enum/permit-app-info.ts b/policy-engine/src/enum/permit-app-info.ts index 30822f6d8..cf6d33691 100644 --- a/policy-engine/src/enum/permit-app-info.ts +++ b/policy-engine/src/enum/permit-app-info.ts @@ -7,4 +7,5 @@ export enum PermitAppInfo { PermitStartDate = 'permitData.startDate', PermitDateFormat = 'YYYY-MM-DD', CompanyName = 'permitData.companyName', + PermitDuration = 'permitData.permitDuration', } diff --git a/policy-engine/src/enum/validation-result-code.ts b/policy-engine/src/enum/validation-result-code.ts index a8b77fb72..5938cdb0a 100644 --- a/policy-engine/src/enum/validation-result-code.ts +++ b/policy-engine/src/enum/validation-result-code.ts @@ -3,4 +3,5 @@ export enum ValidationResultCode { FieldValidationError = 'field-validation-error', ConfigurationInvalid = 'configuration-invalid', GeneralResult = 'general-result', + CostValue = 'cost-value', } diff --git a/policy-engine/src/enum/validation-result-type.ts b/policy-engine/src/enum/validation-result-type.ts index 11929f843..5bcce4d9d 100644 --- a/policy-engine/src/enum/validation-result-type.ts +++ b/policy-engine/src/enum/validation-result-type.ts @@ -3,4 +3,5 @@ export enum ValidationResultType { Requirement = 'requirement', Warning = 'warning', Information = 'information', + Cost = 'cost', } diff --git a/policy-engine/src/helper/facts.helper.ts b/policy-engine/src/helper/facts.helper.ts index 0324b8220..ab9c3f260 100644 --- a/policy-engine/src/helper/facts.helper.ts +++ b/policy-engine/src/helper/facts.helper.ts @@ -15,8 +15,11 @@ export function addRuntimeFacts(engine: Engine): void { ); engine.addFact(PolicyFacts.ValidationDate.toString(), today); - // Will be either 365 or 366, for use when comparing against - // duration for 1 year permits. + /** + * Add runtime fact for number of days in the permit year. + * Will be either 365 or 366, for use when comparing against + * duration for 1 year permits. + */ engine.addFact( PolicyFacts.DaysInPermitYear.toString(), async function (params, almanac) { @@ -27,7 +30,49 @@ export function addRuntimeFacts(engine: Engine): void { startDate, PermitAppInfo.PermitDateFormat.toString(), ); - return dateFrom.diff(dateFrom.add(1, 'year'), 'day'); + const daysInPermitYear = dateFrom.add(1, 'year').diff(dateFrom, 'day'); + return daysInPermitYear; + }, + ); + + /** + * Add runtime fact for a fixed permit cost, the cost supplied + * as a parameter. + */ + engine.addFact( + PolicyFacts.FixedCost.toString(), + async function (params, almanac) { + return params.cost; + }, + ); + + /** + * Add runtime fact for cost per month, where month is defined by + * policy as a 30 day period or portion thereof, except in the + * case of a full year in which case it is 12 months. + */ + engine.addFact( + PolicyFacts.CostPerMonth.toString(), + async function (params, almanac) { + const duration: number = await almanac.factValue( + PermitAppInfo.PermitDuration.toString(), + ); + const daysInPermitYear: number = await almanac.factValue( + PolicyFacts.DaysInPermitYear.toString(), + ); + + let months: number = Math.floor(duration / 30); + const extraDays: number = duration % 30; + + if (extraDays > 0 && duration !== daysInPermitYear) { + // Add an extra month for a partial month unless it is + // a full year. Note this does not handle the case where 2 + // or more years duration is specified, since that is not + // a valid permit and does not warrant considetation. + months++; + } + + return months * params.cost; }, ); } diff --git a/policy-engine/src/helper/rules-engine.helper.ts b/policy-engine/src/helper/rules-engine.helper.ts index 1fac81ccf..dcfdf683c 100644 --- a/policy-engine/src/helper/rules-engine.helper.ts +++ b/policy-engine/src/helper/rules-engine.helper.ts @@ -1,4 +1,4 @@ -import { Engine } from 'json-rules-engine'; +import { Engine, RuleProperties } from 'json-rules-engine'; import { CustomOperators } from '../rule-operator/custom-operators'; import { Policy } from 'onroute-policy-engine'; import { PolicyDefinition } from 'onroute-policy-engine/types'; @@ -11,7 +11,7 @@ import { PolicyFacts } from 'onroute-policy-engine/enum'; * @returns json-rules-engine Engine instance. */ function getEngine(policyDefinition: PolicyDefinition): Engine { - const engine = new Engine(); + const engine = new Engine([], { replaceFactsInEventParams: true }); CustomOperators.forEach((o) => engine.addOperator(o)); policyDefinition.commonRules.forEach((r) => engine.addRule(r)); return engine; @@ -32,6 +32,35 @@ export function getRulesEngines(policy: Policy): Map { permitType.rules?.forEach((r) => engine.addRule(r)); + // Convert the cost definitions into proper rules so the + // results can be added to the validation run. This takes advantage + // of the replaceFactsInEventParams option for the json-rules-engine + // Engine object. + permitType.costRules?.forEach((c) => { + const costRule: RuleProperties = { + conditions: { + all: [ + { + fact: c.fact, + params: c.params, + operator: 'greaterThanInclusive', + value: 0, + }, + ], + }, + event: { + type: 'cost', + params: { + message: 'Calculated permit cost', + code: 'cost-value', + cost: c, + }, + }, + }; + + engine.addRule(costRule); + }); + let allowedVehicles: Array; if (permitType.allowedVehicles && permitType.allowedVehicles.length > 0) { allowedVehicles = permitType.allowedVehicles; diff --git a/policy-engine/src/policy-engine.ts b/policy-engine/src/policy-engine.ts index d5edda253..662b753b5 100644 --- a/policy-engine/src/policy-engine.ts +++ b/policy-engine/src/policy-engine.ts @@ -1,4 +1,13 @@ -import { PolicyDefinition, PermitType } from 'onroute-policy-engine/types'; +import { + PolicyDefinition, + PermitType, + Commodity, + Vehicle, + VehicleType, + SizeDimension, + Trailer, + RegionSizeOverride, +} from 'onroute-policy-engine/types'; import { extractIdentifiedObjects } from './helper/lists.helper'; import { Engine, EngineResult } from 'json-rules-engine'; import { getRulesEngines } from './helper/rules-engine.helper'; @@ -8,6 +17,7 @@ import { addRuntimeFacts, transformPermitFacts } from './helper/facts.helper'; import { ValidationResultType, ValidationResultCode, + RelativePosition, } from 'onroute-policy-engine/enum'; /** Class representing commercial vehicle policy. */ @@ -105,6 +115,401 @@ export class Policy { return commodities; } + /** + * Gets a list of all allowable power unit types for the given permit type + * and commodity. + * @param permitTypeId ID of the permit type to get power units for + * @param commodityId ID of the commodity to get power units for + * @returns Map of power unit IDs to power unit names. + */ + getPermittablePowerUnitTypes( + permitTypeId: string, + commodityId: string, + ): Map { + let puTypes = new Map(); + if (permitTypeId && commodityId) { + const permitType = this.getPermitTypeDefinition(permitTypeId); + if (permitType) { + if (permitType.commodityRequired) { + // If commodity is not required, this method cannot calculate the + // permittable power unit types since they will not be configured. + const allowableCommodities = this.getCommodities(permitTypeId); + if (allowableCommodities.has(commodityId)) { + // If the commodity is not allowed for the permit type, no power + // units are allowed. + const commodity = this.getCommodityDefinition(commodityId); + if (commodity) { + // If commodity is null, this indicates a configuration error. + const puTypeIds: Array | undefined = + commodity.powerUnits?.map((p) => p.type); + puTypes = extractIdentifiedObjects( + this.policyDefinition.vehicleTypes.powerUnitTypes, + puTypeIds, + ); + } + } + } + } + } + return puTypes; + } + + /** + * Gets a list of all configured vehicles for the given permit type and commodity. + * Includes both power units and trailers in a single return value. + * @param permitTypeId ID of the permit type to get vehicles for + * @param commodityId ID of the commodity to get vehicles for + * @returns Array of vehicle objects for the permit type and commodityId + */ + getAllVehiclesForCommodity( + permitTypeId: string, + commodityId: string, + ): Array { + let vehicles: Array = new Array(); + if (permitTypeId && commodityId) { + const permitType = this.getPermitTypeDefinition(permitTypeId); + if (permitType) { + if (permitType.commodityRequired) { + const allowableCommodities = this.getCommodities(permitTypeId); + if (allowableCommodities.has(commodityId)) { + const commodity = this.getCommodityDefinition(commodityId); + if (commodity) { + let pu: Array | undefined = commodity.powerUnits; + if (!pu) { + pu = new Array(); + } + let tr: Array | undefined = commodity.trailers; + if (!tr) { + tr = new Array(); + } + vehicles = pu.concat(tr); + } + } + } + } + } + return vehicles; + } + + /** + * Gets the next permittable vehicles based on the supplied permit type, + * commodity, and current configuration. + * If the permit type does not require commodity or if the commodity + * is not valid for the permit type, this will return an empty map + * (meaning no vehicles are permittable). + * @param permitTypeId ID of the permit type to get vehicles for + * @param commodityId ID of the commodity to get vehicles for + * @param currentConfiguration Current vehicle configuration to which will be + * added the next permittable vehicle + * @returns Map of vehicle ID to vehicle name. Includes both power units and + * trailers in a single return value (if applicable). + */ + getNextPermittableVehicles( + permitTypeId: string, + commodityId: string, + currentConfiguration: Array, + ): Map { + let vehicleTypes = new Map(); + if (permitTypeId && commodityId && currentConfiguration) { + const allVehiclesForCommodity: Array = + this.getAllVehiclesForCommodity(permitTypeId, commodityId); + if (allVehiclesForCommodity.length > 0) { + let vehicleTypeIds: Array; + if (currentConfiguration.length == 0) { + // If the current configuration has no vehicles, return only those vehicles + // that have an empty canFollow array (typically power units). + vehicleTypeIds = allVehiclesForCommodity + .filter((v) => v.canFollow.length == 0) + .map((v) => v.type); + } else { + const lastVehicleType = + currentConfiguration[currentConfiguration.length - 1]; + vehicleTypeIds = allVehiclesForCommodity + .filter((v) => v.canFollow.includes(lastVehicleType)) + .map((v) => v.type); + } + const allVehicles: Array = + this.policyDefinition.vehicleTypes.powerUnitTypes.concat( + this.policyDefinition.vehicleTypes.trailerTypes, + ); + vehicleTypes = extractIdentifiedObjects(allVehicles, vehicleTypeIds); + } + } + return vehicleTypes; + } + + /** + * Returns whether the supplied configuration is valid for the given permit type + * and commodity. If the permit type does not require a commodity, this method + * will return false. This will not validate incomplete configurations - if the + * current configuration has only a power unit this will return false even if the + * power unit is acceptable because there is no trailer which is mandatory. + * If this is called for a permit type that does not require commodity, or with + * a commodity not permitted for the permit type, it will return false. + * @param permitTypeId ID of the permit type to validate the configuration against + * @param commodityId ID of the commodity used for the configuration + * @param currentConfiguration Current vehicle configuration to validate + */ + isConfigurationValid( + permitTypeId: string, + commodityId: string, + currentConfiguration: Array, + ): boolean { + let isValid: boolean = false; + + const runningConfig: Array = new Array(); + for (let i = 0; i < currentConfiguration.length; i++) { + // Note getNextPermittableVehicles will always return an empty map if either the + // permit type or commodity is invalid + const nextVehicles: Map = this.getNextPermittableVehicles( + permitTypeId, + commodityId, + runningConfig, + ); + if (nextVehicles.has(currentConfiguration[i])) { + // The next vehicle in the configuration is permittable as per policy + runningConfig.push(currentConfiguration[i]); + const permitType = this.getPermitTypeDefinition(permitTypeId); + if (permitType?.sizeDimensionRequired) { + // Size dimension is required for this permit type, so we need to + // flip the isValid flag only if we find a trailer that counts + // for size dimensions + const vehicleType = + this.policyDefinition.vehicleTypes.trailerTypes?.find( + (v) => v.id == currentConfiguration[i], + ); + if (vehicleType) { + if (!vehicleType.ignoreForSizeDimensions) { + isValid = true; + } + } + } else { + // Size dimensions are not required for this permit type, so there are + // no requirements with respect to what trailers are permitted. + isValid = true; + } + } else { + // The next vehicle in the list is not permittable, or there are no + // permittable vehicles at all in the list. + isValid = false; + break; + } + } + return isValid; + } + + /** + * Gets the maximum size dimensions for a given permit type, commodity, + * and vehicle configuration. A vehicle configuration's size dimension is + * dictated by the configuration on the trailer. For configurations that + * are just a power unit, a pseudo trailer type 'NONE' is used and the + * size dimension is configured on that. Accessory category trailers + * (e.g. jeeps and boosters) are not used for configuration since they + * do not impact the size dimension in policy. + * @param permitTypeId ID of the permit type to get size dimension for + * @param commodityId ID of the commodity to get size dimension for + * @param currentConfiguration Current vehicle configuration to get size dimension for + * @param regions List of regions the vehicle will be traveling in. If not + * supplied this defaults to the most restrictive size dimension (if multiple + * are configured). + * @returns SizeDimension for the given permit type, commodity, and configuration + */ + getSizeDimension( + permitTypeId: string, + commodityId: string, + configuration: Array, + regions?: Array, + ): SizeDimension | null { + // Initialize the sizeDimension with global defaults + let sizeDimension: SizeDimension | null = null; + + // Validate that the configuration is permittable + if ( + this.isConfigurationValid(permitTypeId, commodityId, configuration) + ) { + // Get the last trailer in the configuration that can be used for size calculations + const sizeTrailer: string | undefined = Array.from(configuration) + .reverse() + .find((vehicleId) => { + const trailerType = + this.policyDefinition.vehicleTypes.trailerTypes?.find( + (v) => v.id == vehicleId, + ); + return !trailerType?.ignoreForSizeDimensions; + }); + + if (sizeTrailer) { + // Get the trailer size dimension array for the commodity + const commodity = this.getCommodityDefinition(commodityId); + const trailer: Trailer | undefined = commodity?.trailers?.find( + (t) => t.type == sizeTrailer, + ); + let sizeDimensions: Array; + if ( + trailer && + trailer.sizeDimensions && + trailer.sizeDimensions.length > 0 + ) { + sizeDimensions = trailer.sizeDimensions; + } else { + sizeDimensions = new Array(); + } + + let sizeDimensionConfigured: SizeDimension | null = null; + if (sizeDimensions.length == 0) { + // If there are no dimensions configured but the configuration is still + // valid, we inherit the size dimensions from the global defaults. + sizeDimensionConfigured = this.policyDefinition.globalSizeDefaults; + } else if (sizeDimensions.length == 1) { + sizeDimensionConfigured = sizeDimensions[0]; + } else { + // If multiple size dimensions, select the first one matching all modifiers + sizeDimensionConfigured = this.selectCorrectSizeDimension( + sizeDimensions, + configuration, + sizeTrailer, + ); + } + + if (sizeDimensionConfigured) { + // Adjust the size dimensions for the regions travelled, if needed + if (!regions) { + // If we are not provided a list of regions, assume we are + // traveling through all regions (will take the most restrictive + // of all dimensions in all cases). + console.log('Assuming all regions for size dimension lookup'); + regions = this.policyDefinition.geographicRegions.map((g) => g.id); + } else { + console.log('Using ' + regions + ' as regions for size lookup'); + } + const valueOverrides: Array = []; + regions?.forEach((r) => { + let valueOverride: RegionSizeOverride; + // Check to see if this region has specific size dimensions + let regionOverride = sizeDimensionConfigured.regions?.find((cr) => cr.region == r); + if (!regionOverride) { + // The region travelled does not have an override, so it assumes + // the dimensions of the base size dimension configured. + valueOverride = { + region: r, + height: sizeDimensionConfigured.height ?? this.policyDefinition.globalSizeDefaults.height ?? 0, + width: sizeDimensionConfigured.width ?? this.policyDefinition.globalSizeDefaults.width ?? 0, + length: sizeDimensionConfigured.length ?? this.policyDefinition.globalSizeDefaults.length ?? 0, + } + } else { + // There is a region override with one or more dimensions. Use this + // value preferentially, using base dimension and global default in + // descending order of preference. + valueOverride = { + region: r, + height: regionOverride.height ?? sizeDimensionConfigured.height ?? this.policyDefinition.globalSizeDefaults.height ?? 0, + width: regionOverride.width ?? sizeDimensionConfigured.width ?? this.policyDefinition.globalSizeDefaults.width ?? 0, + length: regionOverride.length ?? sizeDimensionConfigured.length ?? this.policyDefinition.globalSizeDefaults.length ?? 0, + } + } + valueOverrides.push(valueOverride); + }); + + // At this point we have a complete set of size dimensions for each of + // the regions that will be traversed. Take the minimum value of each + // dimension for the final value. + const minimumOverrides = valueOverrides.reduce((accumulator, currentValue) => { + + if (typeof accumulator.height === 'undefined') { + accumulator.height = currentValue.height ?? 0; + } + if (typeof accumulator.width === 'undefined') { + accumulator.width = currentValue.width ?? 0; + } + if (typeof accumulator.length === 'undefined') { + accumulator.length = currentValue.length ?? 0; + } + + accumulator.height = Math.min(accumulator.height, currentValue.height ?? 0); + accumulator.width = Math.min(accumulator.width, currentValue.width ?? 0); + accumulator.length = Math.min(accumulator.length, currentValue.length ?? 0); + return accumulator; + }, { 'region': '' }); + + sizeDimension = { + rearProjection: sizeDimensionConfigured.rearProjection ?? this.policyDefinition.globalSizeDefaults.rearProjection, + frontProjection: sizeDimensionConfigured.frontProjection ?? this.policyDefinition.globalSizeDefaults.frontProjection, + height: minimumOverrides.height ?? sizeDimensionConfigured.height ?? this.policyDefinition.globalSizeDefaults.height, + width: minimumOverrides.width ?? sizeDimensionConfigured.width ?? this.policyDefinition.globalSizeDefaults.width, + length: minimumOverrides.length ?? sizeDimensionConfigured.length ?? this.policyDefinition.globalSizeDefaults.length, + } + } else { + console.log('Size dimension not configured for trailer'); + } + } else { + console.log('Could not locate trailer to use for size dimension'); + } + } else { + console.log('Configuration is invalid, returning null size dimension'); + } + + return sizeDimension; + } + + /** + * Selects the correct size dimension from a list of potential candidates, based + * on the modifier of each dimension. If none of the modifiers match, returns + * null. + * @param sizeDimensions Size dimension options to choose from + * @param currentConfiguration The full vehicle configuration + */ + selectCorrectSizeDimension( + sizeDimensions: Array, + configuration: Array, + sizeTrailer: string, + ): SizeDimension | null { + let matchingDimension: SizeDimension | null = null; + + if ( + sizeDimensions?.length > 0 && + configuration?.length > 0 + ) { + for (let i = 0; i < sizeDimensions.length; i++) { + if (!sizeDimensions[i].modifiers) { + // This dimension has no modifiers, so it is the default if none of + // the other specific modifiers match. + matchingDimension = sizeDimensions[i]; + console.log('Using default size dimension, no modifiers specified'); + } else { + const sizeTrailerIndex = configuration.findIndex((c) => sizeTrailer == c); + const isMatch: boolean | undefined = sizeDimensions[i].modifiers?.every( + (m) => { + if (!m.type) { + return false; + } + switch (m.position) { + case RelativePosition.First.toString(): + return configuration[0] == m.type; + case RelativePosition.Last.toString(): + return configuration[configuration.length - 1] == m.type; + case RelativePosition.Before.toString(): + return configuration[sizeTrailerIndex - 1] == m.type; + case RelativePosition.After.toString(): + return configuration[sizeTrailerIndex + 1] == m.type; + default: + return false; + } + }, + ); + // As soon as we find a dimension with matching modifiers, + // set it and return it. + if (isMatch) { + matchingDimension = sizeDimensions[i]; + break; + } + } + } + } else { + console.log('Either size dimensions or configuration array is null, no matching dimension returned'); + } + return matchingDimension; + } + /** * Gets a list of all configured power unit types in the policy definition. * @returns Map of power unit type IDs to power unit type names. @@ -144,4 +549,22 @@ export class Policy { return null; } } + + /** + * Gets a full Commodity definition by ID + * @param type Type ID of the commodity to return. + * @returns Commodity definition for the supplied ID. + */ + getCommodityDefinition(type?: string): Commodity | null { + let commodity: Commodity | undefined; + if (this.policyDefinition.commodities) { + commodity = this.policyDefinition.commodities.find((c) => c.id === type); + } + + if (commodity) { + return commodity; + } else { + return null; + } + } } diff --git a/policy-engine/src/types/cost-rule.ts b/policy-engine/src/types/cost-rule.ts new file mode 100644 index 000000000..d910c1ebf --- /dev/null +++ b/policy-engine/src/types/cost-rule.ts @@ -0,0 +1,4 @@ +export type CostRule = { + fact: string; + params: Record; +}; diff --git a/policy-engine/src/types/index.ts b/policy-engine/src/types/index.ts index 6bb8fc3f7..b8591eaee 100644 --- a/policy-engine/src/types/index.ts +++ b/policy-engine/src/types/index.ts @@ -1,10 +1,12 @@ export { Commodity } from './commodity'; +export { CostRule } from './cost-rule'; export { DimensionModifier } from './dimension-modifier'; export { PermitFacts } from './facts'; export { GeographicRegion } from './geographic-region'; export { IdentifiedObject } from './identified-object'; export { PermitType } from './permit-type'; export { PolicyDefinition } from './policy-definition'; +export { Matrix, RangeMatrix } from './range-matrix'; export { RegionSizeOverride } from './region-size-override'; export { SelfIssuable } from './self-issuable'; export { SizeDimension } from './size-dimension'; diff --git a/policy-engine/src/types/permit-type.ts b/policy-engine/src/types/permit-type.ts index e6f8df9cb..1a5280c4a 100644 --- a/policy-engine/src/types/permit-type.ts +++ b/policy-engine/src/types/permit-type.ts @@ -1,5 +1,5 @@ import { RuleProperties } from 'json-rules-engine'; -import { IdentifiedObject } from 'onroute-policy-engine/types'; +import { CostRule, IdentifiedObject } from 'onroute-policy-engine/types'; export type PermitType = IdentifiedObject & { routingRequired: boolean; @@ -9,4 +9,5 @@ export type PermitType = IdentifiedObject & { allowedVehicles: Array; allowedCommodities?: Array; rules?: Array; + costRules?: Array; }; diff --git a/policy-engine/src/types/policy-definition.ts b/policy-engine/src/types/policy-definition.ts index 5d57e4c14..4efcb78a1 100644 --- a/policy-engine/src/types/policy-definition.ts +++ b/policy-engine/src/types/policy-definition.ts @@ -6,6 +6,7 @@ import { Commodity, SizeDimension, VehicleCategories, + RangeMatrix, } from 'onroute-policy-engine/types'; import { RuleProperties } from 'json-rules-engine'; @@ -19,4 +20,5 @@ export type PolicyDefinition = { vehicleCategories: VehicleCategories; vehicleTypes: VehicleTypes; commodities: Array; + rangeMatrices?: Array; }; diff --git a/policy-engine/src/types/range-matrix.ts b/policy-engine/src/types/range-matrix.ts new file mode 100644 index 000000000..145178ef6 --- /dev/null +++ b/policy-engine/src/types/range-matrix.ts @@ -0,0 +1,11 @@ +import { IdentifiedObject } from 'onroute-policy-engine/types'; + +export type RangeMatrix = IdentifiedObject & { + matrix: Matrix[]; +}; + +export type Matrix = { + min?: number; + max?: number; + value: number; +}; diff --git a/policy-engine/src/types/size-dimension.ts b/policy-engine/src/types/size-dimension.ts index 837f8c29a..6c3356a1e 100644 --- a/policy-engine/src/types/size-dimension.ts +++ b/policy-engine/src/types/size-dimension.ts @@ -1,14 +1,15 @@ import { DimensionModifier, RegionSizeOverride, + SelfIssuable, } from 'onroute-policy-engine/types'; -export type SizeDimension = { - frontProjection: number; - rearProjection: number; - width: number; - height: number; - length: number; +export type SizeDimension = SelfIssuable & { + frontProjection?: number; + rearProjection?: number; + width?: number; + height?: number; + length?: number; modifiers?: Array; regions?: Array; }; diff --git a/policy-engine/src/types/vehicle-type.ts b/policy-engine/src/types/vehicle-type.ts index 9d2af5edf..faef34df4 100644 --- a/policy-engine/src/types/vehicle-type.ts +++ b/policy-engine/src/types/vehicle-type.ts @@ -8,6 +8,7 @@ import { export type VehicleType = IdentifiedObject & { category: string; defaultSizeDimensions?: SizeDimension; + ignoreForSizeDimensions?: boolean; }; export type PowerUnitType = VehicleType & { diff --git a/policy-engine/src/types/vehicle.ts b/policy-engine/src/types/vehicle.ts index a167689ce..81e9c0218 100644 --- a/policy-engine/src/types/vehicle.ts +++ b/policy-engine/src/types/vehicle.ts @@ -1,10 +1,11 @@ import { + SelfIssuable, SizeDimension, PowerUnitWeightDimension, TrailerWeightDimension, } from 'onroute-policy-engine/types'; -export type Vehicle = { +export type Vehicle = SelfIssuable & { type: string; canFollow: Array; sizeDimensions?: Array; diff --git a/policy-engine/src/validation-result.ts b/policy-engine/src/validation-result.ts index 2bbe4c281..9face0a63 100644 --- a/policy-engine/src/validation-result.ts +++ b/policy-engine/src/validation-result.ts @@ -21,4 +21,5 @@ export class ValidationResult { code: string; message: string; fieldReference?: string; + cost?: number; } diff --git a/policy-engine/src/validation-results.ts b/policy-engine/src/validation-results.ts index 6250fff7f..99fd395bc 100644 --- a/policy-engine/src/validation-results.ts +++ b/policy-engine/src/validation-results.ts @@ -21,6 +21,11 @@ export class ValidationResults { * permit. */ information: Array = []; + /** + * Array of cost messages, based on cost rules. If there are more than + * one cost message, all should be summed to get the total permit cost. + */ + cost: Array = []; /** * Creates a new ValidationResults from the json-rules-engine result. @@ -45,6 +50,7 @@ export class ValidationResults { message, ); result.fieldReference = e.params?.fieldReference; + result.cost = e.params?.cost; switch (type) { case ValidationResultType.Violation: @@ -59,6 +65,9 @@ export class ValidationResults { case ValidationResultType.Information: this.information.push(result); break; + case ValidationResultType.Cost: + this.cost.push(result); + break; default: // Treat any unknown validation event as a violation since // it is an unexpected scenario.