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..6e8d68c2c --- /dev/null +++ b/vehicles/src/common/helper/validate-loa.helper.ts @@ -0,0 +1,207 @@ +import { ReadLoaDto } from 'src/modules/special-auth/dto/response/read-loa.dto'; +import { PermitType } from '../enum/permit-type.enum'; +import { Loas, PermitData } from '../interface/permit.template.interface'; +import { Permit } from 'src/modules/permit-application-payment/permit/entities/permit.entity'; +import * as dayjs from 'dayjs'; +import * as isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; +import * as isSameOrAfter from 'dayjs/plugin/isSameOrAfter'; + +import { In, QueryRunner } from 'typeorm'; +import { LoaDetail } from 'src/modules/special-auth/entities/loa-detail.entity'; +import { Mapper } from '@automapper/core'; +import { UnprocessableEntityException } from '@nestjs/common'; + +dayjs.extend(isSameOrBefore); +dayjs.extend(isSameOrAfter); +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 ( + isStartDateValid(startDate, permitStartDate) && + isEndDateValid(expiryDate, permitExpiryDate) + ); +}; + +export const isStartDateValid = ( + startDate: string, + permitStartDate: string, +): boolean => { + return dayjs(startDate).isSameOrBefore(permitStartDate, 'day'); +}; + +export const isEndDateValid = ( + expiryDate: string, + permitExpiryDate: string, +): boolean => { + return expiryDate + ? dayjs(expiryDate).isSameOrAfter(permitExpiryDate, 'day') + : true; +}; +export const isValidLoa = async ( + permit: Permit, + queryRunner: QueryRunner, + mapper: Mapper, +) => { + const permitData = JSON.parse(permit.permitData.permitData) as PermitData; + if (permitData.loas) { + const { vehicleId: permitVehicleId, vehicleType: permitVehicleType } = + permitData.vehicleDetails; + const { companyId } = permit.company; + const loaNumbers = permitData.loas.map((loa) => loa.loaNumber); + const readLoaDto = await findLoas( + companyId, + loaNumbers, + queryRunner, + mapper, + ); + + // Validate LOA details and permit data against database entries + validateLoaDetails(readLoaDto, permit, permitVehicleId, permitVehicleType); + + // validate LoA snapshot in permit Data + validatePermitDataAgainstLoas( + permitData, + permit, + permitVehicleId, + permitVehicleType, + ); + } +}; +export const validateLoaDetails = ( + readLoaDtos: ReadLoaDto[], + permit: Permit, + permitVehicleId: string, + permitVehicleType: string, +) => { + for (const readLoaDto of readLoaDtos) { + const { + powerUnits: loaPowerUnits, + trailers: loaTrailers, + loaPermitType: loaPermitTypes, + } = readLoaDto; + if (!isValidDateForLoa(readLoaDto, permit)) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} has LoA ${readLoaDto.loaNumber} with invalid date(s).`, + ); + } + if ( + !isVehicleTypeValid( + permitVehicleType, + permitVehicleId, + loaPowerUnits, + loaTrailers, + ) + ) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} has LoA ${readLoaDto.loaNumber} with invalid vehicle(s).`, + ); + } + if (!isPermitTypeValid(permit.permitType, loaPermitTypes)) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} has LoA ${readLoaDto.loaNumber} with invalid permitType.`, + ); + } + } +}; + +export const 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 ${loa.loaNumber} snapshot with invalid date(s).`, + ); + } + + if ( + !isVehicleTypeValid( + permitVehicleType, + permitVehicleId, + permitLoaPowerUnits, + permitLoaTrailers, + ) + ) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} has LoA ${loa.loaNumber} snapshot with invalid vehicle(s).`, + ); + } + if (!isPermitTypeValid(permit.permitType, permitTypesLoa)) { + throw new UnprocessableEntityException( + `${permit.applicationNumber} has LoA ${loa.loaNumber} 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} loaNumber - Number of the LOA to be fetched. + * @returns {Promise} - Returns a Promise that resolves to the LOA detail. + */ +export const findLoas = async ( + companyId: number, + loaNumbers: number[], + queryRunner: QueryRunner, + mapper: Mapper, +): Promise => { + // Fetch initial active LOA details + const loaDetails = await queryRunner.manager.find(LoaDetail, { + where: { + loaNumber: In(loaNumbers), + isActive: true, + company: { companyId }, + }, + relations: ['company', 'loaVehicles', 'loaPermitTypes'], + }); + const readLoaDto = await mapper.mapArrayAsync( + loaDetails, + LoaDetail, + ReadLoaDto, + { + extraArgs: () => ({ companyId: companyId }), + }, + ); + return readLoaDto; +}; diff --git a/vehicles/src/common/interface/permit.template.interface.ts b/vehicles/src/common/interface/permit.template.interface.ts index 971cdc9f0..8ed38b9ac 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 @@ -94,6 +95,7 @@ interface ContactDetails { } interface VehicleDetails { + vehicleId: string; vin: string; plate: string; make: string; @@ -114,8 +116,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..e6af1a02e 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,8 @@ 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 { SpecialAuthService } from 'src/modules/special-auth/special-auth.service'; +import { SpecialAuth } from 'src/modules/special-auth/entities/special-auth.entity'; @Module({ imports: [ @@ -22,10 +24,11 @@ import { CfsTransactionDetail } from './entities/cfs-transaction.entity'; PaymentCardType, PaymentMethodType, CfsTransactionDetail, + SpecialAuth, ]), ], controllers: [PaymentController], - providers: [PaymentService, TransactionProfile, PaymentReportService], + providers: [PaymentService, TransactionProfile, PaymentReportService, SpecialAuthService], exports: [PaymentService], }) export class PaymentModule {} 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..6446579b8 100644 --- a/vehicles/src/modules/permit-application-payment/payment/payment.service.ts +++ b/vehicles/src/modules/permit-application-payment/payment/payment.service.ts @@ -13,14 +13,7 @@ import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; import { Transaction } from './entities/transaction.entity'; import { InjectRepository } from '@nestjs/typeorm'; -import { - Brackets, - DataSource, - In, - QueryRunner, - Repository, - UpdateResult, -} from 'typeorm'; +import { DataSource, In, QueryRunner, Repository, UpdateResult } from 'typeorm'; import { PermitTransaction } from './entities/permit-transaction.entity'; import { IUserJWT } from 'src/common/interface/user-jwt.interface'; import { callDatabaseSequence } from 'src/common/helper/database.helper'; @@ -43,7 +36,6 @@ import { UpdatePaymentGatewayTransactionDto } from './dto/request/update-payment import { PaymentCardType } from './entities/payment-card-type.entity'; import { PaymentMethodType } from './entities/payment-method-type.entity'; import { LogAsyncMethodExecution } from '../../../common/decorator/log-async-method-execution.decorator'; -import { PermitHistoryDto } from '../permit/dto/response/permit-history.dto'; import { calculatePermitAmount, permitFee, @@ -56,7 +48,6 @@ import { validApplicationDates, } from '../../../common/helper/permit-application.helper'; import { isCfsPaymentMethodType } from 'src/common/helper/payment.helper'; -import { PgApprovesStatus } from 'src/common/enum/pg-approved-status-type.enum'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { CacheKey } from 'src/common/enum/cache-key.enum'; @@ -71,8 +62,11 @@ import { isCVClient, isFeatureEnabled, } 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 { isValidLoa } from 'src/common/helper/validate-loa.helper'; +import { PermitHistoryDto } from '../permit/dto/response/permit-history.dto'; +import { SpecialAuthService } from 'src/modules/special-auth/special-auth.service'; @Injectable() export class PaymentService { @@ -87,6 +81,9 @@ export class PaymentService { private paymentMethodTypeRepository: Repository, @InjectRepository(PaymentCardType) private paymentCardTypeRepository: Repository, + @InjectRepository(Permit) + private permitRepository: Repository, + private readonly specialAuthService: SpecialAuthService, @InjectMapper() private readonly classMapper: Mapper, @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, @@ -331,12 +328,18 @@ 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 isValidLoa(application, queryRunner, this.classMapper); + } } const totalTransactionAmount = await this.validateApplicationAndPayment( createTransactionDto, existingApplications, currentUser, - queryRunner, ); const transactionOrderNumber = await this.generateTransactionOrderNumber(); @@ -509,7 +512,6 @@ export class PaymentService { createTransactionDto: CreateTransactionDto, applications: Permit[], currentUser: IUserJWT, - queryRunner: QueryRunner, ) { let totalTransactionAmountCalculated = 0; const isCVClientUser: boolean = isCVClient(currentUser.identity_provider); @@ -524,10 +526,8 @@ export class PaymentService { `Atleast one of the application has invalid startDate or expiryDate.`, ); } - totalTransactionAmountCalculated += await this.permitFeeCalculator( - application, - queryRunner, - ); + totalTransactionAmountCalculated += + await this.permitFeeCalculator(application); } const totalTransactionAmount = createTransactionDto.applicationDetails?.reduce( @@ -795,21 +795,15 @@ export class PaymentService { * @param queryRunner - An optional QueryRunner for database transactions. * @returns {Promise} - The calculated permit fee or refund amount. */ - @LogAsyncMethodExecution() - async permitFeeCalculator( - application: Permit, - queryRunner?: QueryRunner, - ): Promise { + async permitFeeCalculator(application: Permit): Promise { if (application.permitStatus === ApplicationStatus.REVOKED) return 0; + const companyId = application.company.companyId; const permitPaymentHistory = await this.findPermitHistory( application.originalPermitId, - queryRunner, - ); - const isNoFee = await this.findNoFee( - application.company.companyId, - queryRunner, + companyId, ); + const isNoFee = await this.specialAuthService.findNoFee(companyId); const oldAmount = permitPaymentHistory.length > 0 ? calculatePermitAmount(permitPaymentHistory) @@ -818,51 +812,26 @@ export class PaymentService { return fee; } - @LogAsyncMethodExecution() - async findNoFee( - companyId: number, - queryRunner: QueryRunner, - ): Promise { - const specialAuth = await queryRunner.manager - .createQueryBuilder() - .select('specialAuth') - .from(SpecialAuth, 'specialAuth') - .innerJoinAndSelect('specialAuth.company', 'company') - .where('company.companyId = :companyId', { companyId: companyId }) - .getOne(); - return !!specialAuth && !!specialAuth.noFeeType; - } + /** + * + * This function is deprecated and will be removed once the validation endpoints are established. + */ @LogAsyncMethodExecution() - async findPermitHistory( + public async findPermitHistory( originalPermitId: string, - queryRunner: QueryRunner, + companyId?: number, ): Promise { - // Fetches the permit history for a given originalPermitId using the provided QueryRunner - // This includes all related transactions and filters permits by non-null permit numbers - // Orders the results by transaction submission date in descending order - - const permits = await queryRunner.manager - .createQueryBuilder() - .select('permit') - .from(Permit, 'permit') + const permits = await this.permitRepository + .createQueryBuilder('permit') + .leftJoinAndSelect('permit.company', 'company') .innerJoinAndSelect('permit.permitTransactions', 'permitTransactions') .innerJoinAndSelect('permitTransactions.transaction', 'transaction') .where('permit.permitNumber IS NOT NULL') .andWhere('permit.originalPermitId = :originalPermitId', { originalPermitId: originalPermitId, }) - .andWhere( - new Brackets((qb) => { - qb.where( - 'transaction.paymentMethodTypeCode != :paymentType OR ( transaction.paymentMethodTypeCode = :paymentType AND transaction.pgApproved = :approved)', - { - paymentType: PaymentMethodTypeEnum.WEB, - approved: PgApprovesStatus.APPROVED, - }, - ); - }), - ) + .andWhere('company.companyId = :companyId', { companyId: companyId }) .orderBy('transaction.transactionSubmitDate', 'DESC') .getMany(); diff --git a/vehicles/src/modules/special-auth/special-auth.module.ts b/vehicles/src/modules/special-auth/special-auth.module.ts index f8b2ddf76..2527a2b9f 100644 --- a/vehicles/src/modules/special-auth/special-auth.module.ts +++ b/vehicles/src/modules/special-auth/special-auth.module.ts @@ -22,5 +22,6 @@ import { SpecialAuthProfile } from './profile/special-auth.profile'; ], controllers: [SpecialAuthController, LoaController], providers: [SpecialAuthService, LoaService, LoaProfile, SpecialAuthProfile], + exports: [SpecialAuthService], }) export class SpecialAuthModule {} diff --git a/vehicles/src/modules/special-auth/special-auth.service.ts b/vehicles/src/modules/special-auth/special-auth.service.ts index 3109eea1e..60c4dfde9 100644 --- a/vehicles/src/modules/special-auth/special-auth.service.ts +++ b/vehicles/src/modules/special-auth/special-auth.service.ts @@ -125,4 +125,13 @@ export class SpecialAuthService { ReadSpecialAuthDto, ); } + + async findNoFee(companyId: number): Promise { + const specialAuth = await this.specialAuthRepository + .createQueryBuilder('specialAuth') + .innerJoinAndSelect('specialAuth.company', 'company') + .where('company.companyId = :companyId', { companyId: companyId }) + .getOne(); + return !!specialAuth && !!specialAuth.noFeeType; + } }