diff --git a/frontend/src/features/permits/constants/constants.ts b/frontend/src/features/permits/constants/constants.ts index 984ab4e01..646c54861 100644 --- a/frontend/src/features/permits/constants/constants.ts +++ b/frontend/src/features/permits/constants/constants.ts @@ -27,6 +27,7 @@ export const PERMIT_TYPE_CHOOSE_FROM_OPTIONS = [ export const BASE_DAYS_IN_YEAR = 365; export const COMMON_MIN_DURATION = 30; +export const TERM_DURATION_INTERVAL_DAYS = 30; export const COMMON_DURATION_OPTIONS = [ { value: COMMON_MIN_DURATION, label: "30 Days" }, diff --git a/frontend/src/features/permits/constants/tros.ts b/frontend/src/features/permits/constants/tros.ts index a2d22f75c..76b44d910 100644 --- a/frontend/src/features/permits/constants/tros.ts +++ b/frontend/src/features/permits/constants/tros.ts @@ -1,6 +1,11 @@ import { tros } from "./tros.json"; import { PermitCommodity } from "../types/PermitCommodity"; -import { COMMON_DURATION_OPTIONS, COMMON_MIN_DURATION } from "./constants"; +import { + BASE_DAYS_IN_YEAR, + COMMON_DURATION_OPTIONS, + COMMON_MIN_DURATION, + TERM_DURATION_INTERVAL_DAYS, +} from "./constants"; export const TROS_INELIGIBLE_POWERUNITS = [...tros.ineligiblePowerUnitSubtypes]; export const TROS_INELIGIBLE_TRAILERS = [...tros.ineligibleTrailerSubtypes]; @@ -12,4 +17,6 @@ export const MANDATORY_TROS_COMMODITIES: PermitCommodity[] = ); export const MIN_TROS_DURATION = COMMON_MIN_DURATION; +export const MAX_TROS_DURATION = BASE_DAYS_IN_YEAR; export const TROS_DURATION_OPTIONS = [...COMMON_DURATION_OPTIONS]; +export const TROS_DURATION_INTERVAL_DAYS = TERM_DURATION_INTERVAL_DAYS; diff --git a/frontend/src/features/permits/constants/trow.ts b/frontend/src/features/permits/constants/trow.ts index 854984e80..bda99ef4c 100644 --- a/frontend/src/features/permits/constants/trow.ts +++ b/frontend/src/features/permits/constants/trow.ts @@ -1,10 +1,17 @@ import { trow } from "./trow.json"; import { PermitCommodity } from "../types/PermitCommodity"; -import { COMMON_DURATION_OPTIONS, COMMON_MIN_DURATION } from "./constants"; +import { + BASE_DAYS_IN_YEAR, + COMMON_DURATION_OPTIONS, + COMMON_MIN_DURATION, + TERM_DURATION_INTERVAL_DAYS, +} from "./constants"; export const TROW_INELIGIBLE_POWERUNITS = [...trow.ineligiblePowerUnitSubtypes]; export const TROW_INELIGIBLE_TRAILERS = [...trow.ineligibleTrailerSubtypes]; export const TROW_COMMODITIES: PermitCommodity[] = [...trow.commodities]; export const MANDATORY_TROW_COMMODITIES: PermitCommodity[] = [...TROW_COMMODITIES]; export const MIN_TROW_DURATION = COMMON_MIN_DURATION; +export const MAX_TROW_DURATION = BASE_DAYS_IN_YEAR; export const TROW_DURATION_OPTIONS = [...COMMON_DURATION_OPTIONS]; +export const TROW_DURATION_INTERVAL_DAYS = TERM_DURATION_INTERVAL_DAYS; diff --git a/frontend/src/features/permits/helpers/dateSelection.ts b/frontend/src/features/permits/helpers/dateSelection.ts index d7be4f8bf..ac034f061 100644 --- a/frontend/src/features/permits/helpers/dateSelection.ts +++ b/frontend/src/features/permits/helpers/dateSelection.ts @@ -1,6 +1,18 @@ -import { MIN_TROS_DURATION, TROS_DURATION_OPTIONS } from "../constants/tros"; -import { MIN_TROW_DURATION, TROW_DURATION_OPTIONS } from "../constants/trow"; +import { BASE_DAYS_IN_YEAR, TERM_DURATION_INTERVAL_DAYS } from "../constants/constants"; import { PERMIT_TYPES, PermitType } from "../types/PermitType"; +import { + MAX_TROS_DURATION, + MIN_TROS_DURATION, + TROS_DURATION_INTERVAL_DAYS, + TROS_DURATION_OPTIONS, +} from "../constants/tros"; + +import { + MAX_TROW_DURATION, + MIN_TROW_DURATION, + TROW_DURATION_INTERVAL_DAYS, + TROW_DURATION_OPTIONS, +} from "../constants/trow"; /** * Get list of selectable duration options for a given permit type. @@ -23,3 +35,30 @@ export const minDurationForPermitType = (permitType: PermitType) => { if (permitType === PERMIT_TYPES.TROW) return MIN_TROW_DURATION; return 0; }; + +/** + * Get the maximum allowable duration for a given permit type. + * @param permitType Permit type to get max duration for + * @returns Maxinum allowable duration for the permit type + */ +export const maxDurationForPermitType = (permitType: PermitType) => { + if (permitType === PERMIT_TYPES.TROS) return MAX_TROS_DURATION; + if (permitType === PERMIT_TYPES.TROW) return MAX_TROW_DURATION; + return BASE_DAYS_IN_YEAR; +}; + +/** + * Get the duration interval (in days) for a given permit type. + * @param permitType Permit type to get duration interval for + * @returns Number of days as duration interval for the permit type. + */ +export const getDurationIntervalDays = (permitType: PermitType) => { + switch (permitType) { + case PERMIT_TYPES.TROW: + return TROW_DURATION_INTERVAL_DAYS; + case PERMIT_TYPES.TROS: + return TROS_DURATION_INTERVAL_DAYS; + default: + return TERM_DURATION_INTERVAL_DAYS; // This needs to be updated once more permit types are added + } +}; diff --git a/frontend/src/features/permits/helpers/feeSummary.ts b/frontend/src/features/permits/helpers/feeSummary.ts index 68d56f78f..61927211b 100644 --- a/frontend/src/features/permits/helpers/feeSummary.ts +++ b/frontend/src/features/permits/helpers/feeSummary.ts @@ -3,9 +3,9 @@ import { TRANSACTION_TYPES, TransactionType } from "../types/payment"; import { Permit } from "../types/permit"; import { isValidTransaction } from "./payment"; import { Nullable } from "../../../common/types/common"; -import { PERMIT_STATES, getPermitState } from "./permitState"; +import { PERMIT_STATES, daysLeftBeforeExpiry, getPermitState } from "./permitState"; import { PERMIT_TYPES, PermitType } from "../types/PermitType"; - +import { getDurationIntervalDays, maxDurationForPermitType } from "./dateSelection"; import { applyWhenNotNullable, getDefaultRequiredVal, @@ -18,13 +18,26 @@ import { * @returns Fee to be paid for the permit duration */ export const calculateFeeByDuration = (permitType: PermitType, duration: number) => { + const maxAllowableDuration = maxDurationForPermitType(permitType); + + // Make sure that duration is between 0 and max allowable duration (for given permit type) + const safeDuration = duration < 0 + ? 0 + : (duration > maxAllowableDuration) ? maxAllowableDuration : duration; + + const intervalDays = getDurationIntervalDays(permitType); + + const intervalPeriodsToPay = safeDuration > 360 + ? Math.ceil(360 / intervalDays) : Math.ceil(safeDuration / intervalDays); + if (permitType === PERMIT_TYPES.TROW) { - // Only for TROW - return duration > 360 ? 1200 : Math.floor(duration / 30) * 100; + // Only for TROW, $100 per interval (30 days) + return intervalPeriodsToPay * 100; } // Add more conditions for other permit types if needed - // 1 Year === 365 days, but the fee for one year is only $360 - return duration > 360 ? 360 : duration; + + // For TROS, $30 per interval (30 days) + return intervalPeriodsToPay * 30; }; /** @@ -115,12 +128,16 @@ export const isZeroAmount = (amount: number) => { */ export const calculateAmountForVoid = ( permit: Permit, - permitHistory: PermitHistory[], ) => { const permitState = getPermitState(permit); if (permitState === PERMIT_STATES.EXPIRED) { return 0; } - return calculateNetAmount(permitHistory); + const daysLeft = daysLeftBeforeExpiry(permit); + const intervalDays = getDurationIntervalDays(permit.permitType); + return calculateFeeByDuration( + permit.permitType, + Math.floor(daysLeft / intervalDays) * intervalDays, + ); }; diff --git a/frontend/src/features/permits/helpers/permitState.ts b/frontend/src/features/permits/helpers/permitState.ts index e7e826a67..e5c9dcef2 100644 --- a/frontend/src/features/permits/helpers/permitState.ts +++ b/frontend/src/features/permits/helpers/permitState.ts @@ -44,8 +44,7 @@ export const daysLeftBeforeExpiry = (permit: Permit) => { } // Active permit (current datetime is between the start date and end date) - const tomorrow = dayjs(getStartOfDate(currDate)).add(1, "day"); - return getDateDiffInDays(permitExpiryDate, tomorrow); + return getDateDiffInDays(permitExpiryDate, currDate); }; /** diff --git a/frontend/src/features/permits/hooks/hooks.ts b/frontend/src/features/permits/hooks/hooks.ts index 1ea7fe26f..6f99df931 100644 --- a/frontend/src/features/permits/hooks/hooks.ts +++ b/frontend/src/features/permits/hooks/hooks.ts @@ -35,6 +35,21 @@ import { getPendingPermits, } from "../apiManager/permitsAPI"; +const QUERY_KEYS = { + PERMIT_DETAIL: ( + permitId?: Nullable, + companyId?: Nullable, + ) => ["permit", permitId, companyId], + AMEND_APPLICATION: ( + originalPermitId?: Nullable, + companyId?: Nullable, + ) => ["amendmentApplication", originalPermitId, companyId], + PERMIT_HISTORY: ( + originalPermitId?: Nullable, + companyId?: Nullable, + ) => ["permitHistory", originalPermitId, companyId], +}; + /** * A custom react query mutation hook that saves the application data to the backend API * The hook checks for an existing application number to decide whether to send an update or create request @@ -146,7 +161,7 @@ export const usePermitDetailsQuery = ( permitId?: Nullable, ) => { return useQuery({ - queryKey: ["permit"], + queryKey: QUERY_KEYS.PERMIT_DETAIL(permitId, companyId), queryFn: async () => { const res = await getPermit(permitId, companyId); return res ? deserializePermitResponse(res) : res; @@ -262,7 +277,7 @@ export const usePermitHistoryQuery = ( companyId?: Nullable, ) => { return useQuery({ - queryKey: ["permitHistory"], + queryKey: QUERY_KEYS.PERMIT_HISTORY(originalPermitId, companyId), queryFn: () => getPermitHistory(originalPermitId, companyId), enabled: Boolean(originalPermitId) && Boolean(companyId), retry: false, @@ -288,7 +303,7 @@ export const useIssuePermits = (companyIdParam?: Nullable) => { retry: false, onSuccess: (issueResponseData) => { queryClient.invalidateQueries({ - queryKey: ["application", "permit"], + queryKey: ["application"], }); setIssueResults(issueResponseData); }, @@ -314,13 +329,13 @@ export const useAmendPermit = (companyIdParam?: Nullable) => { const amendResult = await amendPermit(data, companyIdParam); if (amendResult.status === 200 || amendResult.status === 201) { queryClient.invalidateQueries({ - queryKey: ["permit"], + queryKey: QUERY_KEYS.PERMIT_DETAIL(data.permitId, companyIdParam), }); queryClient.invalidateQueries({ - queryKey: ["amendmentApplication"], + queryKey: QUERY_KEYS.AMEND_APPLICATION(data.originalPermitId, companyIdParam), }); queryClient.invalidateQueries({ - queryKey: ["permitHistory"], + queryKey: QUERY_KEYS.PERMIT_HISTORY(data.originalPermitId, companyIdParam), }); return { @@ -348,13 +363,16 @@ export const useModifyAmendmentApplication = () => { if (amendResult.status === 200 || amendResult.status === 201) { queryClient.invalidateQueries({ - queryKey: ["permit"], - }); - queryClient.invalidateQueries({ - queryKey: ["amendmentApplication"], + queryKey: QUERY_KEYS.AMEND_APPLICATION( + data.application.originalPermitId, + data.companyId, + ), }); queryClient.invalidateQueries({ - queryKey: ["permitHistory"], + queryKey: QUERY_KEYS.PERMIT_HISTORY( + data.application.originalPermitId, + data.companyId, + ), }); return { @@ -380,7 +398,7 @@ export const useAmendmentApplicationQuery = ( companyId?: Nullable, ) => { return useQuery({ - queryKey: ["amendmentApplication"], + queryKey: QUERY_KEYS.AMEND_APPLICATION(originalPermitId, companyId), queryFn: async () => { const res = await getCurrentAmendmentApplication( originalPermitId, diff --git a/frontend/src/features/permits/pages/Void/FinishVoid.tsx b/frontend/src/features/permits/pages/Void/FinishVoid.tsx index aaea07d33..80453e594 100644 --- a/frontend/src/features/permits/pages/Void/FinishVoid.tsx +++ b/frontend/src/features/permits/pages/Void/FinishVoid.tsx @@ -11,7 +11,7 @@ import { useVoidPermit } from "./hooks/useVoidPermit"; import { isValidTransaction } from "../../helpers/payment"; import { Nullable } from "../../../../common/types/common"; import { hasPermitsActionFailed } from "../../helpers/permitState"; -import { getDefaultRequiredVal } from "../../../../common/helpers/util"; +import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../../common/helpers/util"; export const FinishVoid = ({ permit, @@ -26,7 +26,13 @@ export const FinishVoid = ({ const { email, additionalEmail, fax, reason } = voidPermitData; - const permitHistoryQuery = usePermitHistoryQuery(permit?.originalPermitId); + const permitHistoryQuery = usePermitHistoryQuery( + permit?.originalPermitId, + applyWhenNotNullable( + id => `${id}`, + permit?.companyId, + ), + ); const permitHistory = getDefaultRequiredVal([], permitHistoryQuery.data); @@ -38,7 +44,7 @@ export const FinishVoid = ({ const amountToRefund = !permit ? 0 - : -1 * calculateAmountForVoid(permit, transactionHistory); + : -1 * calculateAmountForVoid(permit); const { mutation: voidPermitMutation, voidResults } = useVoidPermit(); diff --git a/frontend/src/features/permits/pages/Void/components/VoidPermitForm.tsx b/frontend/src/features/permits/pages/Void/components/VoidPermitForm.tsx index 727ec09c7..c82465f3d 100644 --- a/frontend/src/features/permits/pages/Void/components/VoidPermitForm.tsx +++ b/frontend/src/features/permits/pages/Void/components/VoidPermitForm.tsx @@ -8,13 +8,11 @@ import { useVoidPermitForm } from "../hooks/useVoidPermitForm"; import { VoidPermitHeader } from "./VoidPermitHeader"; import { Permit } from "../../../types/permit"; import { RevokeDialog } from "./RevokeDialog"; -import { usePermitHistoryQuery } from "../../../hooks/hooks"; import { calculateAmountForVoid } from "../../../helpers/feeSummary"; import { FeeSummary } from "../../../components/feeSummary/FeeSummary"; import { VoidPermitFormData } from "../types/VoidPermit"; import { useVoidPermit } from "../hooks/useVoidPermit"; import { mapToRevokeRequestData } from "../helpers/mapper"; -import { isValidTransaction } from "../../../helpers/payment"; import { Nullable } from "../../../../../common/types/common"; import { hasPermitsActionFailed } from "../../../helpers/permitState"; import { getDefaultRequiredVal } from "../../../../../common/helpers/util"; @@ -46,14 +44,6 @@ export const VoidPermitForm = ({ const { formMethods, permitId, setVoidPermitData, next } = useVoidPermitForm(); - const permitHistoryQuery = usePermitHistoryQuery(permit?.originalPermitId); - - const permitHistory = getDefaultRequiredVal([], permitHistoryQuery.data); - - const validTransactionHistory = permitHistory.filter((history) => - isValidTransaction(history.paymentMethodTypeCode, history.pgApproved), - ); - const { mutation: revokePermitMutation, voidResults } = useVoidPermit(); useEffect(() => { @@ -67,10 +57,7 @@ export const VoidPermitForm = ({ } }, [voidResults]); - const amountToRefund = - permitHistoryQuery.isLoading || !permit - ? 0 - : -1 * calculateAmountForVoid(permit, validTransactionHistory); + const amountToRefund = !permit ? 0 : -1 * calculateAmountForVoid(permit); const { control, diff --git a/vehicles/src/common/constants/permit.constant.ts b/vehicles/src/common/constants/permit.constant.ts index 3b8bfe925..afe5b7e3d 100644 --- a/vehicles/src/common/constants/permit.constant.ts +++ b/vehicles/src/common/constants/permit.constant.ts @@ -1,8 +1,8 @@ export const TROS_TERM = 30; export const TROS_PRICE_PER_TERM = 30; -export const TROS_MIN_VALID_DURATION = TROS_TERM; +export const TROS_MIN_VALID_DURATION = 1; export const TROS_MAX_VALID_DURATION = 366; export const TROW_TERM = 30; export const TROW_PRICE_PER_TERM = 100; -export const TROW_MIN_VALID_DURATION = TROS_TERM; +export const TROW_MIN_VALID_DURATION = 1; export const TROW_MAX_VALID_DURATION = 366; diff --git a/vehicles/src/common/enum/pg-approved-status-type.enum.ts b/vehicles/src/common/enum/pg-approved-status-type.enum.ts new file mode 100644 index 000000000..9c9f1cdbb --- /dev/null +++ b/vehicles/src/common/enum/pg-approved-status-type.enum.ts @@ -0,0 +1,4 @@ +export enum PgApprovesStatus { + DECLINED = 0, + APPROVED = 1, +} diff --git a/vehicles/src/common/helper/permit-fee.helper.ts b/vehicles/src/common/helper/permit-fee.helper.ts index 38da2783f..76c84ec35 100644 --- a/vehicles/src/common/helper/permit-fee.helper.ts +++ b/vehicles/src/common/helper/permit-fee.helper.ts @@ -15,6 +15,7 @@ import { } from '../constants/permit.constant'; import { differenceBetween } from './date-time.helper'; import * as dayjs from 'dayjs'; +import { ApplicationStatus } from '../enum/application-status.enum'; /** * Calculates the permit fee based on the application and old amount. @@ -24,13 +25,8 @@ import * as dayjs from 'dayjs'; * @throws {NotAcceptableException} If the duration is invalid for TROS permit type. * @throws {BadRequestException} If the permit type is not recognized. */ -export const permitFee = (application: Permit, oldAmount: number): number => { - let duration = - differenceBetween( - application.permitData.startDate, - application.permitData.expiryDate, - ) + 1; - +export const permitFee = (application: Permit, oldAmount?: number): number => { + let duration = calculateDuration(application); switch (application.permitType) { case PermitType.TERM_OVERSIZE: { const validDuration = isValidDuration( @@ -58,6 +54,7 @@ export const permitFee = (application: Permit, oldAmount: number): number => { TROS_PRICE_PER_TERM, TROS_TERM, oldAmount, + application.permitStatus, ); } case PermitType.TERM_OVERWEIGHT: { @@ -86,6 +83,7 @@ export const permitFee = (application: Permit, oldAmount: number): number => { TROW_PRICE_PER_TERM, TROW_TERM, oldAmount, + application.permitStatus, ); } default: @@ -99,6 +97,24 @@ export const yearlyPermit = (duration: number): boolean => { return duration <= 365 && duration >= 361; }; +export const calculateDuration = (application: Permit): number => { + let startDate = application.permitData.startDate; + const endDate = application.permitData.expiryDate; + const today = dayjs(new Date()).format('YYYY-MM-DD'); + if ( + application.permitStatus === ApplicationStatus.VOIDED && + startDate < today + ) + startDate = today; + if ( + application.permitStatus === ApplicationStatus.VOIDED && + today === startDate + ) + startDate = dayjs(today).add(1, 'day').format('YYYY-MM-DD'); + const duration = differenceBetween(startDate, endDate) + 1; + return duration; +}; + export const leapYear = ( duration: number, startDate: string, @@ -131,9 +147,14 @@ export const currentPermitFee = ( duration: number, pricePerTerm: number, allowedPermitTerm: number, - oldAmount: number, + oldAmount?: number, + permitStatus?: ApplicationStatus, ): number => { - const permitTerms = Math.ceil(duration / allowedPermitTerm); + let permitTerms = Math.ceil(duration / allowedPermitTerm); // ex: if duraion is 40 days then charge for 60 days. + if (permitStatus === ApplicationStatus.VOIDED) { + permitTerms = Math.floor(duration / allowedPermitTerm); //ex: if duration is 40 days then refund only 30 days. + return pricePerTerm * permitTerms * -1; + } return oldAmount > 0 ? pricePerTerm * permitTerms - oldAmount : pricePerTerm * permitTerms + oldAmount; 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 bcc4b8ee9..03b61b997 100644 --- a/vehicles/src/modules/permit-application-payment/payment/payment.service.ts +++ b/vehicles/src/modules/permit-application-payment/payment/payment.service.ts @@ -11,7 +11,14 @@ import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; import { Transaction } from './entities/transaction.entity'; import { InjectRepository } from '@nestjs/typeorm'; -import { DataSource, In, QueryRunner, Repository, UpdateResult } from 'typeorm'; +import { + Brackets, + 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,6 +50,7 @@ import { CfsTransactionDetail } from './entities/cfs-transaction.entity'; import { CfsFileStatus } from 'src/common/enum/cfs-file-status.enum'; import { isAmendmentApplication } 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'; @Injectable() export class PaymentService { @@ -434,7 +442,9 @@ export class PaymentService { totalTransactionAmount.toFixed(2) != Math.abs(totalTransactionAmountCalculated).toFixed(2) ) { - throw new BadRequestException('Transaction Amount Mismatch'); + throw new BadRequestException( + `Transaction Amount Mismatch. Amount received is $${totalTransactionAmount.toFixed(2)} but amount calculated is $${Math.abs(totalTransactionAmountCalculated).toFixed(2)}`, + ); } //For transaction type refund, total transaction amount in backend should be less than zero and vice a versa. @@ -709,12 +719,12 @@ export class PaymentService { ); if (application.permitStatus === ApplicationStatus.VOIDED) { - const oldAmount = calculatePermitAmount(permitPaymentHistory); - if (oldAmount > 0) return -oldAmount; - return oldAmount; + const newAmount = permitFee(application); + return newAmount; } const oldAmount = calculatePermitAmount(permitPaymentHistory); - return permitFee(application, oldAmount); + const fee = permitFee(application, oldAmount); + return fee; } @LogAsyncMethodExecution() @@ -736,6 +746,17 @@ export class PaymentService { .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, + }, + ); + }), + ) .orderBy('transaction.transactionSubmitDate', 'DESC') .getMany();