diff --git a/vehicles/src/common/helper/validate-loa.helper.ts b/vehicles/src/common/helper/validate-loa.helper.ts new file mode 100644 index 000000000..f881e88bc --- /dev/null +++ b/vehicles/src/common/helper/validate-loa.helper.ts @@ -0,0 +1,42 @@ +import { ReadLoaDto } from 'src/modules/special-auth/dto/response/read-loa.dto'; +import { PermitType } from '../enum/permit-type.enum'; +import { Loas } from '../interface/permit.template.interface'; +import { Permit } from 'src/modules/permit-application-payment/permit/entities/permit.entity'; +import * as dayjs from 'dayjs'; + +export const isVehicleTypeValid = ( + permitVehicleType: string, + permitVehicleId: string, + powerUnits?: string[], + trailers?: string[], +): boolean => { + const isPowerUnitAllowed = + permitVehicleType === 'powerUnit' + ? powerUnits.includes(permitVehicleId) + : true; + + const isTrailerAllowed = + permitVehicleType === 'trailer' ? trailers.includes(permitVehicleId) : true; + + return isPowerUnitAllowed && isTrailerAllowed; +}; + +export const isPermitTypeValid = ( + permitTypePermit: PermitType, + permitType: PermitType[], +): boolean => { + return permitType.includes(permitTypePermit); +}; + +export const isValidDateForLoa = ( + loaDetail: Loas | ReadLoaDto, + permit: Permit, +): boolean => { + const { startDate, expiryDate } = loaDetail; + const { startDate: permitStartDate, expiryDate: permitExpiryDate } = + permit.permitData; + return ( + dayjs(startDate).isBefore(permitStartDate, 'day') && + (expiryDate ? dayjs(expiryDate).isAfter(permitExpiryDate, 'day') : true) + ); +}; diff --git a/vehicles/src/common/interface/permit.template.interface.ts b/vehicles/src/common/interface/permit.template.interface.ts index 1e66955ab..056625b04 100644 --- a/vehicles/src/common/interface/permit.template.interface.ts +++ b/vehicles/src/common/interface/permit.template.interface.ts @@ -1,3 +1,4 @@ +import { PermitType } from '../enum/permit-type.enum'; import { ThirdPartyLiability } from '../enum/third-party-liability.enum'; // Data used to populate a .docx template @@ -92,6 +93,7 @@ interface ContactDetails { } interface VehicleDetails { + vehicleId: string; vin: string; plate: string; make: string; @@ -112,8 +114,14 @@ interface Commodities { disabled?: boolean; } -interface Loas { - loaId: string; +export interface Loas { + loaId: number; + loaNumber: number; checked: boolean; + loaPermitType: PermitType[]; + startDate: string; + expiryDate?: string; + powerUnits?: string[]; + trailers?: string[]; disabled?: boolean; } diff --git a/vehicles/src/modules/permit-application-payment/payment/payment.module.ts b/vehicles/src/modules/permit-application-payment/payment/payment.module.ts index 3f608c805..ce08d7fb3 100644 --- a/vehicles/src/modules/permit-application-payment/payment/payment.module.ts +++ b/vehicles/src/modules/permit-application-payment/payment/payment.module.ts @@ -11,6 +11,7 @@ import { PaymentMethodType } from './entities/payment-method-type.entity'; import { PaymentReportService } from './payment-report.service'; import { Permit } from '../permit/entities/permit.entity'; import { CfsTransactionDetail } from './entities/cfs-transaction.entity'; +import { LoaDetail } from 'src/modules/special-auth/entities/loa-detail.entity'; @Module({ imports: [ @@ -22,6 +23,7 @@ import { CfsTransactionDetail } from './entities/cfs-transaction.entity'; PaymentCardType, PaymentMethodType, CfsTransactionDetail, + LoaDetail, ]), ], controllers: [PaymentController], diff --git a/vehicles/src/modules/permit-application-payment/payment/payment.service.ts b/vehicles/src/modules/permit-application-payment/payment/payment.service.ts index e511f1ced..7b0f1e115 100644 --- a/vehicles/src/modules/permit-application-payment/payment/payment.service.ts +++ b/vehicles/src/modules/permit-application-payment/payment/payment.service.ts @@ -73,6 +73,14 @@ import { } from '../../../common/helper/common.helper'; import { SpecialAuth } from 'src/modules/special-auth/entities/special-auth.entity'; import { TIMEZONE_PACIFIC } from 'src/common/constants/api.constant'; +import { PermitData } from 'src/common/interface/permit.template.interface'; +import { LoaDetail } from 'src/modules/special-auth/entities/loa-detail.entity'; +import { + isPermitTypeValid, + isValidDateForLoa, + isVehicleTypeValid, +} from 'src/common/helper/validate-loa.helper'; +import { ReadLoaDto } from 'src/modules/special-auth/dto/response/read-loa.dto'; @Injectable() export class PaymentService { @@ -87,6 +95,8 @@ export class PaymentService { private paymentMethodTypeRepository: Repository, @InjectRepository(PaymentCardType) private paymentCardTypeRepository: Repository, + @InjectRepository(LoaDetail) + private loaDetailRepository: Repository, @InjectMapper() private readonly classMapper: Mapper, @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, @@ -331,6 +341,13 @@ export class PaymentService { throw new BadRequestException( 'Application in its current status cannot be processed for payment.', ); + const permitData = JSON.parse( + application.permitData.permitData, + ) as PermitData; + // If application includes LoAs then validate Loa data. + if (permitData.loas) { + await this.isValidLoa(application); + } } const totalTransactionAmount = await this.validateApplicationAndPayment( createTransactionDto, @@ -887,4 +904,137 @@ export class PaymentService { })), ) as PermitHistoryDto[]; } + + async isValidLoa(permit: Permit): Promise { + const { companyId } = permit.company; + const permitData = JSON.parse(permit.permitData.permitData) as PermitData; + const { vehicleId: permitVehicleId, vehicleType: permitVehicleType } = + permitData.vehicleDetails; + const loaNumbers = permitData.loas.map((loa) => loa.loaNumber); + const readLoaDto = await this.findLoas(companyId, loaNumbers); + + // Validate LOA details and permit data against database entries + this.validateLoaDetails( + readLoaDto, + permit, + permitVehicleId, + permitVehicleType, + ); + + // validate LoA snapshot in permit Data + this.validatePermitDataAgainstLoas( + permitData, + permit, + permitVehicleId, + permitVehicleType, + ); + } + private validateLoaDetails( + readLoaDtos: ReadLoaDto[], + permit: Permit, + permitVehicleId: string, + permitVehicleType: string, + ) { + for (const readLoaDto of readLoaDtos) { + const loaPowerUnits = readLoaDto.powerUnits; + const loaTrailers = readLoaDto.trailers; + const loaPermitTypes = readLoaDto.loaPermitType; + if (!isValidDateForLoa(readLoaDto, permit)) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} has LoA with invalid date(s).`, + ); + } + if ( + !isVehicleTypeValid( + permitVehicleType, + permitVehicleId, + loaPowerUnits, + loaTrailers, + ) + ) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} has LoA with invalid vehicle(s).`, + ); + } + if (!isPermitTypeValid(permit.permitType, loaPermitTypes)) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} ha LoA with invalid permitType.`, + ); + } + } + } + + private validatePermitDataAgainstLoas( + permitData: PermitData, + permit: Permit, + permitVehicleId: string, + permitVehicleType: string, + ) { + for (const loa of permitData.loas) { + const permitLoaPowerUnits = loa.powerUnits; + const permitLoaTrailers = loa.trailers; + const permitTypesLoa = loa.loaPermitType; + if (!isValidDateForLoa(loa, permit)) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} has LoA snapshot with invalid date(s).`, + ); + } + + if ( + !isVehicleTypeValid( + permitVehicleType, + permitVehicleId, + permitLoaPowerUnits, + permitLoaTrailers, + ) + ) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} has LoA snapshot with invalid vehicle(s).`, + ); + } + if (!isPermitTypeValid(permit.permitType, permitTypesLoa)) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} has LoA snapshot with invalid permitType.`, + ); + } + } + } + + /** + * Retrieves a single LOA (Letter of Authorization) detail for a specified company. + * + * Steps: + * 1. Fetches the LOA detail from the repository based on company ID and LOA Number. + * 2. Ensures the fetched LOA detail is active. + * 3. Includes relations (company, loaVehicles, loaPermitTypes) in the query. + * + * @param {number} companyId - ID of the company for which to fetch the LOA detail. + * @param {number} loaId - ID of the LOA to be fetched. + * @returns {Promise} - Returns a Promise that resolves to the LOA detail. + */ + @LogAsyncMethodExecution() + async findLoas( + companyId: number, + loaNumbers: number[], + ): Promise { + // Fetch initial active LOA details + const loaDetails = await this.loaDetailRepository.find({ + where: { + loaNumber: In(loaNumbers), + isActive: true, + company: { companyId }, + }, + relations: ['company', 'loaVehicles', 'loaPermitTypes'], + }); + + const readLoaDto = await this.classMapper.mapArrayAsync( + loaDetails, + LoaDetail, + ReadLoaDto, + { + extraArgs: () => ({ companyId: companyId }), + }, + ); + return readLoaDto; + } }