diff --git a/frontend/src/features/permits/ApplicationSteps.tsx b/frontend/src/features/permits/ApplicationSteps.tsx index 27259c0328..1d8d7cfe3e 100644 --- a/frontend/src/features/permits/ApplicationSteps.tsx +++ b/frontend/src/features/permits/ApplicationSteps.tsx @@ -3,7 +3,10 @@ import { ErrorBoundary } from "react-error-boundary"; import { ApplicationStepPage } from "./components/dashboard/ApplicationStepPage"; import { ErrorFallback } from "../../common/pages/ErrorFallback"; -import { ApplicationStep, ApplicationStepContext } from "../../routes/constants"; +import { + ApplicationStep, + ApplicationStepContext, +} from "../../routes/constants"; export const ApplicationSteps = React.memo( ({ diff --git a/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx b/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx index a813011e87..b2231ca3b2 100644 --- a/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx +++ b/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx @@ -6,10 +6,8 @@ import "../../../../common/components/dashboard/Dashboard.scss"; import { Banner } from "../../../../common/components/dashboard/components/banner/Banner"; import { ApplicationForm } from "../../pages/Application/ApplicationForm"; import { ApplicationContext } from "../../context/ApplicationContext"; -import { ApplicationReview } from "../../pages/Application/ApplicationReview"; import { getCompanyIdFromSession } from "../../../../common/apiManager/httpRequestHandler"; import { Loading } from "../../../../common/pages/Loading"; -import { ApplicationInQueueReview } from "../../../queue/components/ApplicationInQueueReview"; import { useApplicationForStepsQuery } from "../../hooks/hooks"; import { PERMIT_STATUSES } from "../../types/PermitStatus"; import { @@ -25,12 +23,12 @@ import { } from "../../types/PermitType"; import { - APPLICATION_STEP_CONTEXTS, APPLICATION_STEPS, ApplicationStep, ApplicationStepContext, ERROR_ROUTES, } from "../../../../routes/constants"; +import { ApplicationReview } from "../../pages/Application/ApplicationReview"; const displayHeaderText = (stepKey: ApplicationStep) => { switch (stepKey) { @@ -121,12 +119,11 @@ export const ApplicationStepPage = ({ const renderApplicationStep = () => { if (applicationStep === APPLICATION_STEPS.REVIEW) { - return applicationStepContext === APPLICATION_STEP_CONTEXTS.QUEUE ? ( - - ) : ( - ); } @@ -134,6 +131,7 @@ export const ApplicationStepPage = ({ ); }; diff --git a/frontend/src/features/permits/pages/Application/ApplicationForm.tsx b/frontend/src/features/permits/pages/Application/ApplicationForm.tsx index 823489d768..c3ef3fd7ea 100644 --- a/frontend/src/features/permits/pages/Application/ApplicationForm.tsx +++ b/frontend/src/features/permits/pages/Application/ApplicationForm.tsx @@ -1,6 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { FormProvider } from "react-hook-form"; import { Navigate, useNavigate } from "react-router-dom"; -import { useContext, useMemo, useState } from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; import dayjs from "dayjs"; import { isAxiosError } from "axios"; @@ -46,7 +47,10 @@ import { import { APPLICATIONS_ROUTES, + APPLICATION_QUEUE_ROUTES, APPLICATION_STEPS, + APPLICATION_STEP_CONTEXTS, + ApplicationStepContext, ERROR_ROUTES, } from "../../../../routes/constants"; @@ -59,9 +63,11 @@ const FEATURE = "application"; export const ApplicationForm = ({ permitType, companyId, + applicationStepContext, }: { permitType: PermitType; companyId: number; + applicationStepContext: ApplicationStepContext; }) => { // Context to hold all of the application data related to the application const applicationContext = useContext(ApplicationContext); @@ -125,7 +131,8 @@ export const ApplicationForm = ({ applicationContext?.applicationData?.updatedDateTime, ); - const { mutateAsync: saveApplication } = useSaveApplicationMutation(); + const { mutateAsync: saveApplication, error: saveApplicationError } = + useSaveApplicationMutation(); const snackBar = useContext(SnackBarContext); // Show leave application dialog @@ -198,13 +205,17 @@ export const ApplicationForm = ({ const vehicleData = serializePermitVehicleDetails( data.permitData.vehicleDetails, ); + // TODO show UnavailableApplicationModal here const savedVehicleDetails = await handleSaveVehicle(vehicleData); // Save application before continuing - await onSaveApplication( - (permitId) => navigate(APPLICATIONS_ROUTES.REVIEW(permitId)), - savedVehicleDetails, - ); + await onSaveApplication((permitId) => { + return navigate( + applicationStepContext === APPLICATION_STEP_CONTEXTS.QUEUE + ? APPLICATION_QUEUE_ROUTES.REVIEW(companyId, permitId) + : APPLICATIONS_ROUTES.REVIEW(permitId), + ); + }, savedVehicleDetails); }; const onSaveSuccess = (savedApplication: Application, status: number) => { @@ -242,7 +253,7 @@ export const ApplicationForm = ({ }, }, }; - + // TODO show UnavailableApplicationModal here await saveApplication( { data: applicationToBeSaved, @@ -350,6 +361,23 @@ export const ApplicationForm = ({ if (isUndefined(policyEngine)) return ; if (isNull(policyEngine)) return ; + // TODO we will need to handle errors when attempting to save an application which has been claimed by another user + // once the BE is updated to handle this + const [currentClaimant, setCurrentClaimant] = useState(""); + + // const saveApplicationErrorStatus = saveApplicationError?.response?.status; + + useEffect(() => { + console.log({ saveApplicationError }); + // if (saveApplicationErrorStatus === 422) { + // setCurrentClaimant( + // saveApplicationError.response.data.error[0].additionalInfo + // .currentClaimant, + // ); + // setShowUnavailableApplicationModal(true); + // } + }, [saveApplicationError]); + return (
diff --git a/frontend/src/features/permits/pages/Application/ApplicationReview.tsx b/frontend/src/features/permits/pages/Application/ApplicationReview.tsx index 715c490ee5..13b1ac31a1 100644 --- a/frontend/src/features/permits/pages/Application/ApplicationReview.tsx +++ b/frontend/src/features/permits/pages/Application/ApplicationReview.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { useContext, useEffect, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useNavigate, useParams } from "react-router-dom"; @@ -23,23 +24,55 @@ import { DEFAULT_PERMIT_TYPE, PERMIT_TYPES } from "../../types/PermitType"; import { PERMIT_REVIEW_CONTEXTS } from "../../types/PermitReviewContext"; import { usePolicyEngine } from "../../../policy/hooks/usePolicyEngine"; import { useCommodityOptions } from "../../hooks/useCommodityOptions"; -import { useSubmitApplicationForReview } from "../../../queue/hooks/hooks"; import { deserializeApplicationResponse } from "../../helpers/serialize/deserializeApplication"; import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext"; import { APPLICATIONS_ROUTES, + APPLICATION_QUEUE_ROUTES, APPLICATION_STEPS, + APPLICATION_STEP_CONTEXTS, + ApplicationStepContext, ERROR_ROUTES, + IDIR_ROUTES, } from "../../../../routes/constants"; +import { CASE_ACTIVITY_TYPES } from "../../../queue/types/CaseActivityType"; +import { QueueBreadcrumb } from "../../../queue/components/QueueBreadcrumb"; +import { RejectApplicationModal } from "../../../queue/components/RejectApplicationModal"; +import { + useSubmitApplicationForReview, + useUpdateApplicationInQueueStatus, +} from "../../../queue/hooks/hooks"; +import { Nullable } from "../../../../common/types/common"; +import { UnavailableApplicationModal } from "../../../queue/components/UnavailableApplicationModal"; export const ApplicationReview = ({ - companyId, + companyIdProp, + applicationStepContext, + applicationData: queueApplicationData, }: { - companyId: number; + applicationStepContext: ApplicationStepContext; + companyIdProp?: Nullable; + applicationData?: Nullable; }) => { const { applicationData, setApplicationData: setApplicationContextData } = useContext(ApplicationContext); + const isQueueContext = + applicationStepContext === APPLICATION_STEP_CONTEXTS.QUEUE; + + const application = isQueueContext ? queueApplicationData : applicationData; + + const companyId = getDefaultRequiredVal( + 0, + companyIdProp, + queueApplicationData?.companyId, + ); + + const applicationId = getDefaultRequiredVal( + queueApplicationData?.permitId, + applicationData?.permitId, + ); + const { idirUserDetails } = useContext(OnRouteBCContext); const isStaffUser = Boolean(idirUserDetails?.userRole); @@ -49,12 +82,15 @@ export const ApplicationReview = ({ const { data: companyInfo } = useCompanyInfoDetailsQuery(companyId); const doingBusinessAs = companyInfo?.alternateName; - const permitType = getDefaultRequiredVal(DEFAULT_PERMIT_TYPE, applicationData?.permitType); + const permitType = getDefaultRequiredVal( + DEFAULT_PERMIT_TYPE, + application?.permitType, + ); const fee = isNoFeePermitType ? "0" : `${calculateFeeByDuration( permitType, - getDefaultRequiredVal(0, applicationData?.permitData?.permitDuration), + getDefaultRequiredVal(0, application?.permitData?.permitDuration), )}`; const { setSnackBar } = useContext(SnackBarContext); @@ -71,20 +107,26 @@ export const ApplicationReview = ({ const trailerSubTypesQuery = useTrailerSubTypesQuery(); const methods = useForm(); - // For the confirmation checkboxes - const [allConfirmed, setAllConfirmed] = useState(false); + const [allConfirmed, setAllConfirmed] = useState(isQueueContext); const [hasAttemptedSubmission, setHasAttemptedSubmission] = useState(false); const { mutateAsync: saveApplication } = useSaveApplicationMutation(); const addToCartMutation = useAddToCart(); - - // Submit for review (if applicable) + const { mutateAsync: submitForReview } = useSubmitApplicationForReview(); const { - mutateAsync: submitForReview, - } = useSubmitApplicationForReview(); + mutateAsync: updateApplication, + data: updateApplicationResponse, + error: updateApplicationError, + isPending: updateApplicationMutationPending, + } = useUpdateApplicationInQueueStatus(); - const back = () => { - navigate(APPLICATIONS_ROUTES.DETAILS(permitId), { replace: true }); + const handleEdit = () => { + navigate( + isQueueContext + ? APPLICATION_QUEUE_ROUTES.EDIT(companyId, applicationId) + : APPLICATIONS_ROUTES.DETAILS(permitId), + { replace: true }, + ); }; const handleSaveApplication = async ( @@ -98,42 +140,45 @@ export const ApplicationReview = ({ if (!allConfirmed) return; - const companyId = applicationData?.companyId; - const permitId = applicationData?.permitId; - const applicationNumber = applicationData?.applicationNumber; + const companyId = application?.companyId; + const permitId = application?.permitId; + const applicationNumber = application?.applicationNumber; if (!companyId || !permitId || !applicationNumber) { return navigate(ERROR_ROUTES.UNEXPECTED); } - await saveApplication({ - data: { - ...applicationData, - permitData: { - ...applicationData.permitData, - doingBusinessAs, // always set most recent DBA from company info + await saveApplication( + { + data: { + ...application, + permitData: { + ...application?.permitData, + doingBusinessAs, + }, }, + companyId, }, - companyId, - }, { - onSuccess: ({ data: savedApplication }) => { - setApplicationContextData( - deserializeApplicationResponse(savedApplication), - ); - followUpAction(companyId, permitId, applicationNumber); - }, - onError: (e) => { - console.error(e); - if (isAxiosError(e)) { - navigate(ERROR_ROUTES.UNEXPECTED, { - state: { - correlationId: e?.response?.headers["x-correlation-id"], - }, - }); - } else { - navigate(ERROR_ROUTES.UNEXPECTED); - } + { + onSuccess: ({ data: savedApplication }) => { + setApplicationContextData( + deserializeApplicationResponse(savedApplication), + ); + followUpAction(companyId, permitId, applicationNumber); + }, + onError: (e) => { + console.error(e); + if (isAxiosError(e)) { + navigate(ERROR_ROUTES.UNEXPECTED, { + state: { + correlationId: e?.response?.headers["x-correlation-id"], + }, + }); + } else { + navigate(ERROR_ROUTES.UNEXPECTED); + } + }, }, - }); + ); }; const proceedWithAddToCart = async ( @@ -156,64 +201,129 @@ export const ApplicationReview = ({ const setShowSnackbar = () => true; const handleAddToCart = async () => { - await handleSaveApplication(async (companyId, permitId, applicationNumber) => { - await proceedWithAddToCart(companyId, [permitId], () => { + await handleSaveApplication( + async (companyId, permitId, applicationNumber) => { + await proceedWithAddToCart(companyId, [permitId], () => { + setSnackBar({ + showSnackbar: true, + setShowSnackbar, + message: `Application ${applicationNumber} added to cart`, + alertType: "success", + }); + + refetchCartCount(); + navigate(APPLICATIONS_ROUTES.BASE); + }); + }, + ); + }; + const continueBtnText = + permitType === PERMIT_TYPES.STOS && !isStaffUser + ? "Submit for Review" + : undefined; + + const handleSubmitForReview = async () => { + if (permitType !== PERMIT_TYPES.STOS || isStaffUser) return; + + await handleSaveApplication( + async (companyId, permitId, applicationNumber) => { + await submitForReview({ companyId, applicationId: permitId }); setSnackBar({ showSnackbar: true, - setShowSnackbar, - message: `Application ${applicationNumber} added to cart`, + setShowSnackbar: () => true, + message: `Application ${applicationNumber} submitted for review`, alertType: "success", }); - - refetchCartCount(); navigate(APPLICATIONS_ROUTES.BASE); - }); + }, + ); + }; + + const handleApprove = async () => { + setHasAttemptedSubmission(true); + await updateApplication({ + applicationId: permitId, + companyId, + caseActivityType: CASE_ACTIVITY_TYPES.APPROVED, }); }; - const continueBtnText = permitType === PERMIT_TYPES.STOS && !isStaffUser - ? "Submit for Review" : undefined; + const [showRejectApplicationModal, setShowRejectApplicationModal] = + useState(false); - const handleSubmitForReview = async () => { - if (permitType !== PERMIT_TYPES.STOS) return; - if (isStaffUser) return; + const handleRejectButton = () => { + setShowRejectApplicationModal(true); + }; - await handleSaveApplication(async (companyId, permitId, applicationNumber) => { - await submitForReview({ - companyId, - applicationId: permitId, - }, { - onSuccess: () => { - setSnackBar({ - showSnackbar: true, - setShowSnackbar, - message: `Application ${applicationNumber} submitted for review`, - alertType: "success", - }); - - navigate(APPLICATIONS_ROUTES.BASE); - }, - onError: () => { - navigate(ERROR_ROUTES.UNEXPECTED); - }, - }); + const handleReject = async (comment: string) => { + setHasAttemptedSubmission(true); + await updateApplication({ + applicationId: permitId, + companyId, + caseActivityType: CASE_ACTIVITY_TYPES.REJECTED, + comment, }); }; + const [showUnavailableApplicationModal, setShowUnavailableApplicationModal] = + useState(false); + + const [currentClaimant, setCurrentClaimant] = useState(""); + + const updateApplicationResponseStatus = updateApplicationResponse?.status; + + const handleCloseApplication = () => { + navigate(IDIR_ROUTES.STAFF_HOME); + }; + + const handleCloseUnavailableApplicationModal = () => { + setShowUnavailableApplicationModal(false); + showRejectApplicationModal && setShowRejectApplicationModal(false); + }; + + useEffect(() => { + if (updateApplicationResponseStatus === 201) { + navigate(IDIR_ROUTES.STAFF_HOME); + } + }, [updateApplicationResponse, updateApplicationResponseStatus, navigate]); + + const updateApplicationErrorStatus = updateApplicationError?.response?.status; + + useEffect(() => { + if (updateApplicationErrorStatus === 422) { + setCurrentClaimant( + updateApplicationError.response.data.error[0].additionalInfo + .currentClaimant, + ); + setShowUnavailableApplicationModal(true); + } + }, [updateApplicationError]); + useEffect(() => { window.scrollTo(0, 0); }, []); return (
- + {isQueueContext ? ( + + ) : ( + + )} + + {isQueueContext && showRejectApplicationModal && ( + setShowRejectApplicationModal(false)} + onConfirm={handleReject} + isPending={updateApplicationMutationPending} + /> + )} + + {isQueueContext && showUnavailableApplicationModal && ( + + )}
); }; diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx b/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx index 4e9d069f72..4798ffee77 100644 --- a/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx +++ b/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx @@ -78,7 +78,7 @@ export const ReviewActions = ({ ) : null} - {reviewContext === PERMIT_REVIEW_CONTEXTS.QUEUE ? ( + {reviewContext === PERMIT_REVIEW_CONTEXTS.QUEUE && ( <> +
+ + + ); +}; diff --git a/frontend/src/features/queue/hooks/hooks.ts b/frontend/src/features/queue/hooks/hooks.ts index 08c843a5b8..42ce4ac333 100644 --- a/frontend/src/features/queue/hooks/hooks.ts +++ b/frontend/src/features/queue/hooks/hooks.ts @@ -1,6 +1,5 @@ import { useContext } from "react"; import { useNavigate } from "react-router-dom"; -import { AxiosError } from "axios"; import { MRT_PaginationState, MRT_SortingState } from "material-react-table"; import { keepPreviousData, @@ -16,7 +15,10 @@ import OnRouteBCContext from "../../../common/authentication/OnRouteBCContext"; import { IDIRUserRoleType } from "../../../common/authentication/types"; import { Nullable } from "../../../common/types/common"; import { useTableControls } from "../../permits/hooks/useTableControls"; -import { APPLICATION_QUEUE_STATUSES, ApplicationQueueStatus } from "../types/ApplicationQueueStatus"; +import { + APPLICATION_QUEUE_STATUSES, + ApplicationQueueStatus, +} from "../types/ApplicationQueueStatus"; import { claimApplicationInQueue, getApplicationsInQueue, @@ -33,23 +35,19 @@ const QUEUE_QUERY_KEYS_BASE = "queue"; * * eg. ["queue"] and ["queue", undefined] refers to all (ApplicationQueueStatus) queue items * (regardless of pagination and sorting) - * + * * eg. ["queue", "IN_REVIEW"] only refers to "IN_REVIEW" queue items */ const QUEUE_QUERY_KEYS = { ALL_ITEMS: [QUEUE_QUERY_KEYS_BASE] as const, - WITH_STATUS: (status?: ApplicationQueueStatus) => [ - ...QUEUE_QUERY_KEYS.ALL_ITEMS, - status, - ] as const, + WITH_STATUS: (status?: ApplicationQueueStatus) => + [...QUEUE_QUERY_KEYS.ALL_ITEMS, status] as const, WITH_PAGINATION: ( pagination: MRT_PaginationState, sorting: MRT_SortingState, status?: ApplicationQueueStatus, - ) => [ - ...QUEUE_QUERY_KEYS.WITH_STATUS(status), - { pagination, sorting }, - ] as const, + ) => + [...QUEUE_QUERY_KEYS.WITH_STATUS(status), { pagination, sorting }] as const, }; /** @@ -195,8 +193,9 @@ export const useUpdateApplicationInQueueStatus = () => { onSuccess: () => { invalidate(); }, - onError: (err: AxiosError) => { + onError: (err: any) => { if (err.response?.status === 422) { + // console.log({ err }); return err; } else { navigate(ERROR_ROUTES.UNEXPECTED, { @@ -215,14 +214,8 @@ export const useSubmitApplicationForReview = () => { const { invalidate } = useInvalidateApplicationsInQueue(); return useMutation({ - mutationFn: async (data: { - companyId: number; - applicationId: string; - }) => { - return submitApplicationForReview( - data.companyId, - data.applicationId, - ); + mutationFn: async (data: { companyId: number; applicationId: string }) => { + return submitApplicationForReview(data.companyId, data.applicationId); }, onSuccess: () => { invalidate(); diff --git a/frontend/src/features/queue/pages/ReviewApplicationInQueue.tsx b/frontend/src/features/queue/pages/ReviewApplicationInQueue.tsx deleted file mode 100644 index b09ada4c10..0000000000 --- a/frontend/src/features/queue/pages/ReviewApplicationInQueue.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Box } from "@mui/material"; -import { Navigate, useParams } from "react-router-dom"; -import { Banner } from "../../../common/components/dashboard/components/banner/Banner"; -import { Loading } from "../../../common/pages/Loading"; -import { useApplicationDetailsQuery } from "../../permits/hooks/hooks"; -import { ApplicationInQueueReview } from "../components/ApplicationInQueueReview"; -import { - applyWhenNotNullable, - getDefaultRequiredVal, -} from "../../../common/helpers/util"; -import { ERROR_ROUTES } from "../../../routes/constants"; -import { deserializeApplicationResponse } from "../../permits/helpers/serialize/deserializeApplication"; -import { UniversalUnexpected } from "../../../common/pages/UniversalUnexpected"; - -export const ReviewApplicationInQueue = () => { - const { companyId: companyIdParam, permitId: permitIdParam } = useParams(); - - const companyId: number = applyWhenNotNullable( - (id) => Number(id), - companyIdParam, - 0, - ); - const permitId = getDefaultRequiredVal("", permitIdParam); - - const { - query: { data: applicationData, isLoading: applicationDataIsLoading }, - } = useApplicationDetailsQuery({ - companyId, - permitId, - }); - - if (!companyId || !permitId) { - return ; - } - - if (applicationDataIsLoading) { - return ; - } - - if (!applicationData) { - return ; - } - - return ( -
- - - - - -
- ); -}; diff --git a/frontend/src/routes/constants.ts b/frontend/src/routes/constants.ts index 69b5d7e870..4102c88bf4 100644 --- a/frontend/src/routes/constants.ts +++ b/frontend/src/routes/constants.ts @@ -71,10 +71,7 @@ const PERMITS_ROUTE_BASE = "/permits"; export const PERMITS_ROUTES = { BASE: PERMITS_ROUTE_BASE, SUCCESS: `${PERMITS_ROUTE_BASE}/success`, - VOID: ( - companyId?: Nullable, - permitId?: Nullable, - ) => + VOID: (companyId?: Nullable, permitId?: Nullable) => `${DYNAMIC_ROUTE_URI( "/companies", ROUTE_PLACEHOLDERS.COMPANY_ID, @@ -84,10 +81,7 @@ export const PERMITS_ROUTES = { ROUTE_PLACEHOLDERS.PERMIT_ID, permitId, )}/void`, - AMEND: ( - companyId?: Nullable, - permitId?: Nullable, - ) => + AMEND: (companyId?: Nullable, permitId?: Nullable) => `${DYNAMIC_ROUTE_URI( "/companies", ROUTE_PLACEHOLDERS.COMPANY_ID, @@ -160,10 +154,7 @@ export const APPLICATION_QUEUE_ROUTES = { ROUTE_PLACEHOLDERS.PERMIT_ID, permitId, )}/review`, - EDIT: ( - companyId?: Nullable, - permitId?: Nullable, - ) => + EDIT: (companyId?: Nullable, permitId?: Nullable) => `${DYNAMIC_ROUTE_URI( "/companies", ROUTE_PLACEHOLDERS.COMPANY_ID, @@ -229,6 +220,5 @@ export const ONROUTE_WEBPAGE_LINKS = { "https://www2.gov.bc.ca/gov/content/governments/organizational-structure/ministries-organizations/ministries/citizens-services/servicebc#locations", LIST_OF_BC_HIGHWAYS: "https://www2.gov.bc.ca/gov/content/transportation/transportation-reports-and-reference/reference-information/numbered-routes", - HEIGHT_CLEARANCE_TOOL: - "https://www.drivebc.ca/cvrp/?c=hct", + HEIGHT_CLEARANCE_TOOL: "https://www.drivebc.ca/cvrp/?c=hct", }; diff --git a/vehicles/src/common/helper/format-template-data.helper.ts b/vehicles/src/common/helper/format-template-data.helper.ts index b851fba92c..13e7e63f11 100644 --- a/vehicles/src/common/helper/format-template-data.helper.ts +++ b/vehicles/src/common/helper/format-template-data.helper.ts @@ -89,12 +89,15 @@ export const formatTemplateData = ( template.clientNumber = companyInfo.clientNumber; template.companyAlternateName = companyInfo.alternateName; - // Format Fee Summary const transcation = permit.permitTransactions?.at(0)?.transaction; + // Format Fee Summary template.permitData.feeSummary = formatAmount( transcation.transactionTypeId, - permit.permitTransactions?.at(0)?.transactionAmount, + permit.permitTransactions?.reduce( + (accumulator, item) => accumulator + item.transactionAmount, + 0, + ), ).toString(); if (permit.revision > 0) { diff --git a/vehicles/src/modules/case-management/case-management.service.ts b/vehicles/src/modules/case-management/case-management.service.ts index 3a51b80090..50a19a96bf 100644 --- a/vehicles/src/modules/case-management/case-management.service.ts +++ b/vehicles/src/modules/case-management/case-management.service.ts @@ -496,6 +496,7 @@ export class CaseManagementService { } else if (existingCase.assignedUser?.userGUID !== currentUser.userGUID) { throwUnprocessableEntityException( `Application no longer available. This application is claimed by ${existingCase.assignedUser?.userName}`, + { currentClaimant: existingCase.assignedUser?.userName }, ); } else if (existingCase.caseStatusType !== CaseStatusType.IN_PROGRESS) { throwUnprocessableEntityException('Application no longer available.'); diff --git a/vehicles/src/modules/permit-application-payment/payment/dto/common/payment-transaction.dto.ts b/vehicles/src/modules/permit-application-payment/payment/dto/common/payment-transaction.dto.ts new file mode 100644 index 0000000000..fb9e6183fc --- /dev/null +++ b/vehicles/src/modules/permit-application-payment/payment/dto/common/payment-transaction.dto.ts @@ -0,0 +1,67 @@ +import { AutoMap } from '@automapper/classes'; +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEnum, + IsNumber, + IsOptional, + IsString, + Length, + MaxLength, + Min, +} from 'class-validator'; +import { PaymentCardType } from '../../../../../common/enum/payment-card-type.enum'; +import { PaymentMethodType } from '../../../../../common/enum/payment-method-type.enum'; + +export class PaymentTransactionDto { + @AutoMap() + @ApiProperty({ + example: '10000148', + required: false, + description: + 'Bambora-assigned eight-digit unique id number used to identify an individual transaction.', + }) + @IsOptional() + @IsString() + @MaxLength(20) + pgTransactionId: string; + + @AutoMap() + @ApiProperty({ + example: 'CC', + required: false, + description: 'Represents the payment method of a transaction.', + }) + @IsOptional() + @IsString() + @Length(1, 2) + pgPaymentMethod: string; + + @AutoMap() + @ApiProperty({ + example: '30.00', + description: 'Represents the amount of the transaction.', + }) + @IsNumber() + @Min(0) + transactionAmount: number; + + @AutoMap() + @ApiProperty({ + enum: PaymentMethodType, + example: PaymentMethodType.WEB, + description: 'The identifier of the user selected payment method.', + }) + @IsEnum(PaymentMethodType) + paymentMethodTypeCode: PaymentMethodType; + + @AutoMap() + @ApiProperty({ + example: PaymentCardType.VISA, + enum: PaymentCardType, + description: 'The payment types.', + required: false, + }) + @IsOptional() + @IsEnum(PaymentCardType) + paymentCardTypeCode?: PaymentCardType; +} diff --git a/vehicles/src/modules/permit-application-payment/payment/dto/request/create-refund-transaction.dto.ts b/vehicles/src/modules/permit-application-payment/payment/dto/request/create-refund-transaction.dto.ts new file mode 100644 index 0000000000..a689c9714e --- /dev/null +++ b/vehicles/src/modules/permit-application-payment/payment/dto/request/create-refund-transaction.dto.ts @@ -0,0 +1,35 @@ +import { AutoMap } from '@automapper/classes'; +import { ApiProperty } from '@nestjs/swagger'; +import { + ArrayMinSize, + IsArray, + IsNumberString, + MaxLength, + ValidateNested, +} from 'class-validator'; + +import { PaymentTransactionDto } from '../common/payment-transaction.dto'; +import { Type } from 'class-transformer'; + +export class CreateRefundTransactionDto { + @AutoMap() + @ApiProperty({ + description: 'Application/Permit Id.', + example: '1', + }) + @IsNumberString() + @MaxLength(20) + applicationId: string; + + @AutoMap() + @ApiProperty({ + description: 'The transaction details specific to application/permit.', + required: true, + type: [PaymentTransactionDto], + }) + @IsArray() + @ValidateNested({ each: true }) + @ArrayMinSize(1) + @Type(() => PaymentTransactionDto) + transactions: PaymentTransactionDto[]; +} diff --git a/vehicles/src/modules/permit-application-payment/payment/entities/receipt.entity.ts b/vehicles/src/modules/permit-application-payment/payment/entities/receipt.entity.ts index 7a677c5fc1..3223710225 100644 --- a/vehicles/src/modules/permit-application-payment/payment/entities/receipt.entity.ts +++ b/vehicles/src/modules/permit-application-payment/payment/entities/receipt.entity.ts @@ -1,10 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { - Entity, - Column, - PrimaryGeneratedColumn, - OneToMany, -} from 'typeorm'; +import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; import { AutoMap } from '@automapper/classes'; import { Base } from '../../../common/entities/base.entity'; import { Transaction } from './transaction.entity'; diff --git a/vehicles/src/modules/permit-application-payment/payment/payment.controller.ts b/vehicles/src/modules/permit-application-payment/payment/payment.controller.ts index a57526caa1..cb7e6922d9 100644 --- a/vehicles/src/modules/permit-application-payment/payment/payment.controller.ts +++ b/vehicles/src/modules/permit-application-payment/payment/payment.controller.ts @@ -37,6 +37,7 @@ import { CLIENT_USER_ROLE_LIST, IDIRUserRole, } from '../../../common/enum/user-role.enum'; +import { CreateRefundTransactionDto } from './dto/request/create-refund-transaction.dto'; @ApiBearerAuth() @ApiTags('Payment') @@ -90,6 +91,34 @@ export class PaymentController { return paymentDetails; } + @ApiCreatedResponse({ + description: 'The Transaction Resource', + isArray: true, + type: ReadTransactionDto, + }) + @Permissions({ + allowedIdirRoles: [ + IDIRUserRole.PPC_CLERK, + IDIRUserRole.SYSTEM_ADMINISTRATOR, + IDIRUserRole.CTPO, + ], + }) + @Post('refund') + async createRefundTransactionDetails( + @Req() request: Request, + @Body() { applicationId, transactions }: CreateRefundTransactionDto, + ): Promise { + const currentUser = request.user as IUserJWT; + + const paymentDetails = await this.paymentService.createRefundTransactions({ + currentUser, + applicationId, + transactions, + }); + + return paymentDetails; + } + @ApiOkResponse({ description: 'The Payment Gateway Transaction Resource', type: UpdatePaymentGatewayTransactionDto, 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 bae87d4afd..6a817f6151 100644 --- a/vehicles/src/modules/permit-application-payment/payment/payment.service.ts +++ b/vehicles/src/modules/permit-application-payment/payment/payment.service.ts @@ -16,7 +16,10 @@ import { InjectRepository } from '@nestjs/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'; +import { + callDatabaseSequence, + setBaseEntityProperties, +} from 'src/common/helper/database.helper'; import { Permit } from '../permit/entities/permit.entity'; import { ApplicationStatus } from '../../../common/enum/application-status.enum'; import { PaymentMethodType as PaymentMethodTypeEnum } from '../../../common/enum/payment-method-type.enum'; @@ -30,6 +33,7 @@ import { PAYMENT_CURRENCY, CRYPTO_ALGORITHM_MD5, GL_PROJ_CODE_PLACEHOLDER, + PPC_FULL_TEXT, } from '../../../common/constants/api.constant'; import { convertToHash } from 'src/common/helper/crypto.helper'; import { UpdatePaymentGatewayTransactionDto } from './dto/request/update-payment-gateway-transaction.dto'; @@ -66,6 +70,8 @@ import { isFeatureEnabled, } from '../../../common/helper/common.helper'; import { TIMEZONE_PACIFIC } from 'src/common/constants/api.constant'; +import { PaymentTransactionDto } from './dto/common/payment-transaction.dto'; +import { Nullable } from '../../../common/types/common'; import { FeatureFlagValue } from '../../../common/enum/feature-flag-value.enum'; import { PermitData } from 'src/common/interface/permit.template.interface'; import { isValidLoa } from 'src/common/helper/validate-loa.helper'; @@ -257,11 +263,222 @@ export class PaymentService { ); } + /** + * Creates a Refund Transaction in ORBC System, ensuring that payment methods align with user roles and enabled features. + * The method verifies transactions against application status and computes transaction amounts. + * It then creates and saves new transactions and their associated records, handling any CFS payment methods. + * + * @param applicationId - The ID of the application related to the refund transactions. + * @param transactions - An array of transactions of type {@link RefundTransactionDto} to process. + * @param currentUser - The current user object of type {@link IUserJWT}. + * @param nestedQueryRunner - An optional query runner. If not provided, a new one is created. + * @returns {Promise} The created list of transactions of type {@link ReadTransactionDto}. + * @throws UnprocessableEntityException - When the payment method type is invalid for the user or feature is disabled. + * @throws BadRequestException - When the application status is not valid for the transaction. + */ + @LogAsyncMethodExecution() + async createRefundTransactions({ + applicationId, + transactions, + currentUser, + nestedQueryRunner, + }: { + applicationId: string; + transactions: PaymentTransactionDto[]; + currentUser: IUserJWT; + nestedQueryRunner?: Nullable; + }): Promise { + for (const transaction of transactions) { + if ( + !doesUserHaveRole(currentUser.orbcUserRole, IDIR_USER_ROLE_LIST) && + transaction?.paymentMethodTypeCode !== PaymentMethodTypeEnum.WEB && + transaction?.paymentMethodTypeCode !== PaymentMethodTypeEnum.ACCOUNT && + transaction?.paymentMethodTypeCode !== PaymentMethodTypeEnum.NO_PAYMENT + ) { + throwUnprocessableEntityException( + 'Invalid payment method type for the user', + ); + } else if ( + transaction?.paymentMethodTypeCode === PaymentMethodTypeEnum.ACCOUNT && + !(await isFeatureEnabled(this.cacheManager, 'CREDIT-ACCOUNT')) + ) { + throwUnprocessableEntityException('Disabled feature'); + } + + if ( + transaction?.paymentMethodTypeCode === PaymentMethodTypeEnum.POS && + !transaction?.paymentCardTypeCode + ) { + throwBadRequestException('paymentCardTypeCode', [ + `paymentCardTypeCode is required when paymentMethodTypeCode is ${transaction?.paymentMethodTypeCode}`, + ]); + } + + if ( + transaction?.paymentMethodTypeCode !== + PaymentMethodTypeEnum.NO_PAYMENT && + transaction?.transactionAmount === 0 + ) { + throwUnprocessableEntityException( + `paymentMethodTypeCode should be ${PaymentMethodTypeEnum.NO_PAYMENT} when transaction amount is 0`, + ); + } + } + + let readTransactionDto: ReadTransactionDto[]; + const queryRunner = + nestedQueryRunner || this.dataSource.createQueryRunner(); + if (!nestedQueryRunner) { + await queryRunner.connect(); + await queryRunner.startTransaction(); + } + + try { + const existingApplication: Permit = await queryRunner.manager.findOne( + Permit, + { + where: { permitId: applicationId }, + relations: { permitData: true }, + }, + ); + + if ( + !( + this.isVoidorRevoked(existingApplication.permitStatus) || + this.isApplicationInCart(existingApplication.permitStatus) || + isAmendmentApplication(existingApplication) + ) + ) { + throw new BadRequestException( + 'Application in its current status cannot be processed for payment.', + ); + } + + const totalTransactionAmount = transactions?.reduce( + (accumulator, item) => accumulator + item.transactionAmount, + 0, + ); + + await this.validateApplicationAndPayment( + totalTransactionAmount, + TransactionType.REFUND, + [existingApplication], + currentUser, + ); + + let newTransactionList: Transaction[] = []; + + for (const transaction of transactions) { + const transactionOrderNumber = + await this.generateTransactionOrderNumber(); + const newTransaction = new Transaction(); + newTransaction.transactionTypeId = TransactionType.REFUND; + newTransaction.pgTransactionId = transaction.pgTransactionId; + newTransaction.totalTransactionAmount = transaction.transactionAmount; + newTransaction.paymentMethodTypeCode = + transaction.paymentMethodTypeCode; + newTransaction.paymentCardTypeCode = transaction.paymentCardTypeCode; + newTransaction.pgCardType = transaction.paymentCardTypeCode; + newTransaction.pgPaymentMethod = transaction.pgPaymentMethod; + newTransaction.transactionOrderNumber = transactionOrderNumber; + newTransaction.payerName = PPC_FULL_TEXT; + if (transaction.paymentMethodTypeCode === PaymentMethodTypeEnum.WEB) { + newTransaction.pgApproved = 1; + } + setBaseEntityProperties({ + entity: newTransaction, + currentUser, + }); + newTransactionList.push(newTransaction); + } + + newTransactionList = await queryRunner.manager.save(newTransactionList); + + const receiptNumber = await this.generateReceiptNumber(); + let receipt = new Receipt(); + receipt.receiptNumber = receiptNumber; + setBaseEntityProperties({ entity: receipt, currentUser }); + receipt = await queryRunner.manager.save(receipt); + + for (const newTransaction of newTransactionList) { + let newPermitTransactions = new PermitTransaction(); + newPermitTransactions.transaction = newTransaction; + newPermitTransactions.permit = existingApplication; + newPermitTransactions.createdDateTime = new Date(); + newPermitTransactions.createdUser = currentUser.userName; + newPermitTransactions.createdUserDirectory = + currentUser.orbcUserDirectory; + newPermitTransactions.createdUserGuid = currentUser.userGUID; + newPermitTransactions.updatedDateTime = new Date(); + newPermitTransactions.updatedUser = currentUser.userName; + newPermitTransactions.updatedUserDirectory = + currentUser.orbcUserDirectory; + newPermitTransactions.updatedUserGuid = currentUser.userGUID; + newPermitTransactions.transactionAmount = + newTransaction.totalTransactionAmount; + newPermitTransactions = await queryRunner.manager.save( + newPermitTransactions, + ); + + await queryRunner.manager.update( + Transaction, + { transactionId: newTransaction.transactionId }, + { + receipt: receipt, + updatedDateTime: new Date(), + updatedUser: currentUser.userName, + updatedUserDirectory: currentUser.orbcUserDirectory, + updatedUserGuid: currentUser.userGUID, + }, + ); + + if (isCfsPaymentMethodType(newTransaction.paymentMethodTypeCode)) { + const newCfsTransaction: CfsTransactionDetail = + new CfsTransactionDetail(); + newCfsTransaction.transaction = newTransaction; + newCfsTransaction.fileStatus = CfsFileStatus.READY; + await queryRunner.manager.save(newCfsTransaction); + } + } + + if (!nestedQueryRunner) { + await queryRunner.commitTransaction(); + } + + const createdTransaction = await queryRunner.manager.find(Transaction, { + where: { + transactionId: In( + newTransactionList?.map(({ transactionId }) => transactionId), + ), + }, + relations: ['permitTransactions', 'permitTransactions.permit'], + }); + + readTransactionDto = await this.classMapper.mapArrayAsync( + createdTransaction, + Transaction, + ReadTransactionDto, + ); + } catch (error) { + if (!nestedQueryRunner) { + await queryRunner.rollbackTransaction(); + } + this.logger.error(error); + throw error; + } finally { + if (!nestedQueryRunner) { + await queryRunner.release(); + } + } + + return readTransactionDto; + } + /** * Creates a Transaction in ORBC System. * @param currentUser - The current user object of type {@link IUserJWT} * @param createTransactionDto - The createTransactionDto object of type - * {@link CreateTransactionDto} for creating a new Transaction. + * {@link CreateTransactionDto} for creating a new Transaction. * @returns {ReadTransactionDto} The created transaction of type {@link ReadTransactionDto}. */ @LogAsyncMethodExecution() @@ -270,6 +487,10 @@ export class PaymentService { createTransactionDto: CreateTransactionDto, nestedQueryRunner?: QueryRunner, ): Promise { + if (createTransactionDto.transactionTypeId !== TransactionType.PURCHASE) { + throwUnprocessableEntityException('Invalid transaction type'); + } + const featureFlags = await getMapFromCache( this.cacheManager, CacheKey.FEATURE_FLAG_TYPE, @@ -365,8 +586,16 @@ export class PaymentService { await isValidLoa(application, queryRunner, this.classMapper); } } - const totalTransactionAmount = await this.validateApplicationAndPayment( - createTransactionDto, + + let totalTransactionAmount = + createTransactionDto.applicationDetails?.reduce( + (accumulator, item) => accumulator + item.transactionAmount, + 0, + ); + + totalTransactionAmount = await this.validateApplicationAndPayment( + totalTransactionAmount, + createTransactionDto.transactionTypeId, existingApplications, currentUser, ); @@ -478,10 +707,9 @@ export class PaymentService { ) ) { const receiptNumber = await this.generateReceiptNumber(); - //const receipt = new Receipt(); + let receipt = new Receipt(); receipt.receiptNumber = receiptNumber; - //receipt.transaction = createdTransaction; receipt.createdDateTime = new Date(); receipt.createdUser = currentUser.userName; receipt.createdUserDirectory = currentUser.orbcUserDirectory; @@ -490,7 +718,7 @@ export class PaymentService { receipt.updatedUser = currentUser.userName; receipt.updatedUserDirectory = currentUser.orbcUserDirectory; receipt.updatedUserGuid = currentUser.userGUID; - // await queryRunner.manager.save(receipt); + receipt = await queryRunner.manager.save(receipt); await queryRunner.manager.update( @@ -544,15 +772,18 @@ export class PaymentService { * Additionally, for refund transactions, it checks if the total calculated transaction amount is negative as expected; * if not, it throws an error. * - * @param {CreateTransactionDto} createTransactionDto - The DTO containing the transaction details from the request. + * @param {number} totalTransactionAmount - The total transaction amount from the request. + * @param {TransactionType} transactionType - The transactionType. * @param {Permit[]} applications - A list of permits associated with the transaction. - * @param {QueryRunner} nestedQueryRunner - The query runner to use for database operations within the method. + * @param {IUserJWT} currentUser - The current user performing the transaction. + * @param {QueryRunner} queryRunner - The query runner to use for database operations within the method. * @returns {Promise} The total transaction amount calculated from the backend data. * @throws {BadRequestException} When the transaction amount in the request doesn't match with the calculated amount, * or if there's a transaction type and amount mismatch in case of refunds. */ private async validateApplicationAndPayment( - createTransactionDto: CreateTransactionDto, + totalTransactionAmount: number, + transactionType: TransactionType, applications: Permit[], currentUser: IUserJWT, ) { @@ -572,16 +803,12 @@ export class PaymentService { totalTransactionAmountCalculated += await this.permitFeeCalculator(application); } - const totalTransactionAmount = - createTransactionDto.applicationDetails?.reduce( - (accumulator, item) => accumulator + item.transactionAmount, - 0, - ); + if ( !validAmount( totalTransactionAmountCalculated, totalTransactionAmount, - createTransactionDto.transactionTypeId, + transactionType, ) ) throw new BadRequestException('Transaction amount mismatch.'); @@ -738,10 +965,9 @@ export class PaymentService { if (updateTransactionTemp.pgApproved === 1) { const receiptNumber = await this.generateReceiptNumber(); - //const receipt = new Receipt(); + let receipt = new Receipt(); receipt.receiptNumber = receiptNumber; - //receipt.transaction = updatedTransaction; receipt.receiptNumber = receiptNumber; receipt.createdDateTime = new Date(); receipt.createdUser = currentUser.userName; @@ -751,7 +977,7 @@ export class PaymentService { receipt.updatedUser = currentUser.userName; receipt.updatedUserDirectory = currentUser.orbcUserDirectory; receipt.updatedUserGuid = currentUser.userGUID; - //await queryRunner.manager.save(receipt); + receipt = await queryRunner.manager.save(receipt); await queryRunner.manager.update( diff --git a/vehicles/src/modules/permit-application-payment/permit-receipt-document/permit-receipt-document.service.ts b/vehicles/src/modules/permit-application-payment/permit-receipt-document/permit-receipt-document.service.ts index bf8cf432ac..6cfe9e622e 100644 --- a/vehicles/src/modules/permit-application-payment/permit-receipt-document/permit-receipt-document.service.ts +++ b/vehicles/src/modules/permit-application-payment/permit-receipt-document/permit-receipt-document.service.ts @@ -40,6 +40,7 @@ import { validateEmailandFaxList, } from '../../../common/helper/notification.helper'; import { getPermitTemplateName } from '../../../common/helper/template.helper'; +import { TransactionType } from '../../../common/enum/transaction-type.enum'; import { Nullable } from '../../../common/types/common'; @Injectable() @@ -94,6 +95,14 @@ export class PermitReceiptDocumentService { permits: Permit[]; }[] = []; + // Function to check if a permitId already exists in transactionPermitList + // To avoid duplicate entries for Refund to Multiple Payment methods + function hasExistingPermit(permitId: string): boolean { + return transactionPermitList.some((item) => + item.permits.some((p) => p.permitId === permitId), + ); + } + for (const transaction of transactions) { const fetchedApplications = await this.findManyWithSuccessfulTransaction( null, @@ -104,7 +113,10 @@ export class PermitReceiptDocumentService { const unIssuedApplications = fetchedApplications.filter( (application) => !application.permitNumber, ); - if (!unIssuedApplications?.length) { + if ( + !unIssuedApplications?.length && + !fetchedApplications.some((app) => hasExistingPermit(app.permitId)) + ) { transactionPermitList.push({ transactionId: transaction.transactionId, permits: fetchedApplications, @@ -412,13 +424,19 @@ export class PermitReceiptDocumentService { await Promise.allSettled( fetchedPermits?.map(async (fetchedPermit) => { - const permits = fetchedPermit.permits; + let permits = fetchedPermit.permits; const permitIds = permits?.map((permit) => permit.permitId); + permits = await this.findManyWithSuccessfulTransaction( + permitIds, + companyId, + ); + if (permits?.length) { try { const permit = permits?.at(0); const company = permit?.company; const permitTransactions = permit?.permitTransactions; + const transaction = permitTransactions?.at(0)?.transaction; const receipt = transaction?.receipt; if (receipt.receiptDocumentId) { @@ -443,18 +461,70 @@ export class PermitReceiptDocumentService { permit?.permitType, ), permitNumber: permit?.permitNumber, - transactionAmount: formatAmount( + permitFee: formatAmount( transaction?.transactionTypeId, - permit?.permitTransactions?.at(0)?.transactionAmount, + permit?.permitTransactions?.reduce( + (accumulator, item) => + accumulator + item.transactionAmount, + 0, + ), ), }; }), ); + const transactionList = await Promise.all( + permits?.flatMap((permit) => + permit?.permitTransactions?.map(async (permitTransaction) => { + return { + consolidatedPaymentMethod: ( + await getPaymentCodeFromCache( + this.cacheManager, + permitTransaction?.transaction?.paymentMethodTypeCode, + permitTransaction?.transaction?.paymentCardTypeCode, + ) + ).consolidatedPaymentMethod, + pgTransactionId: + permitTransaction?.transaction?.pgTransactionId, + transactionOrderNumber: + permitTransaction?.transaction?.transactionOrderNumber, + transactionAmount: formatAmount( + transaction?.transactionTypeId, + permitTransaction?.transaction?.totalTransactionAmount, + ), + }; + }), + ), + ); + + const uniqueTransactionList = Array.from( + new Map( + transactionList.map((item) => [ + item.transactionOrderNumber, + item, + ]), + ).values(), + ); + + const totalTransactionAmount = permits?.reduce( + (accumulator, item) => + accumulator + + item.permitTransactions.reduce( + (accumulator, item) => accumulator + item.transactionAmount, + 0, + ), + 0, + ); + const dopsRequestData = { templateName: TemplateName.PAYMENT_RECEIPT, generatedDocumentFileName: `Receipt_No_${receiptNumber}`, templateData: { + transactionType: transaction.transactionTypeId, + receiptType: + transaction.transactionTypeId === TransactionType.PURCHASE + ? 'Receipt' + : 'Refund Receipt', receiptNo: receiptNumber, companyName: companyName, companyAlternateName: companyAlternateName, @@ -468,22 +538,12 @@ export class PermitReceiptDocumentService { : constants.SELF_ISSUED, totalTransactionAmount: formatAmount( transaction?.transactionTypeId, - transaction?.totalTransactionAmount, + totalTransactionAmount, ), permitDetails: permitDetails, - //Transaction Details - pgTransactionId: transaction?.pgTransactionId, - transactionOrderNumber: transaction?.transactionOrderNumber, - consolidatedPaymentMethod: ( - await getPaymentCodeFromCache( - this.cacheManager, - transaction?.paymentMethodTypeCode, - transaction?.paymentCardTypeCode, - ) - ).consolidatedPaymentMethod, + transactions: uniqueTransactionList, transactionDate: convertUtcToPt( - permit?.permitTransactions?.at(0)?.transaction - ?.transactionSubmitDate, + transaction?.transactionSubmitDate, 'MMM. D, YYYY, hh:mm a Z', ), }, diff --git a/vehicles/src/modules/permit-application-payment/permit/dto/request/void-permit.dto.ts b/vehicles/src/modules/permit-application-payment/permit/dto/request/void-permit.dto.ts index bcb70c0af9..152de0d2cc 100644 --- a/vehicles/src/modules/permit-application-payment/permit/dto/request/void-permit.dto.ts +++ b/vehicles/src/modules/permit-application-payment/permit/dto/request/void-permit.dto.ts @@ -1,20 +1,20 @@ import { AutoMap } from '@automapper/classes'; import { ApiProperty } from '@nestjs/swagger'; import { + ArrayMinSize, + IsArray, IsEmail, IsEnum, - IsNumber, IsOptional, IsString, Length, - MaxLength, - Min, MinLength, + ValidateNested, } from 'class-validator'; import { ApplicationStatus } from 'src/common/enum/application-status.enum'; -import { PaymentMethodType } from '../../../../../common/enum/payment-method-type.enum'; import { TransactionType } from '../../../../../common/enum/transaction-type.enum'; -import { PaymentCardType } from '../../../../../common/enum/payment-card-type.enum'; +import { PaymentTransactionDto } from '../../../payment/dto/common/payment-transaction.dto'; +import { Type } from 'class-transformer'; export class VoidPermitDto { @AutoMap() @@ -28,23 +28,15 @@ export class VoidPermitDto { @AutoMap() @ApiProperty({ - description: 'Provider Transaction ID.', - example: '10000148', - required: false, - }) - @IsOptional() - @IsString() - @MaxLength(20) - pgTransactionId?: string; - - @AutoMap() - @ApiProperty({ - enum: PaymentMethodType, - example: PaymentMethodType.WEB, - description: 'The identifier of the user selected payment method.', + description: 'The transaction details specific to application/permit.', + required: true, + type: [PaymentTransactionDto], }) - @IsEnum(PaymentMethodType) - paymentMethodTypeCode: PaymentMethodType; + @IsArray() + @ValidateNested({ each: true }) + @ArrayMinSize(1) + @Type(() => PaymentTransactionDto) + transactions: PaymentTransactionDto[]; @AutoMap() @ApiProperty({ @@ -56,48 +48,6 @@ export class VoidPermitDto { @IsEnum(TransactionType) transactionTypeId: TransactionType; - @AutoMap() - @ApiProperty({ - description: 'Payment Transaction Amount.', - example: 30, - }) - @IsNumber() - @Min(0) - transactionAmount: number; - - @AutoMap() - @ApiProperty({ - description: 'Payment Transaction Date.', - example: '2023-07-10T15:49:36.582Z', - required: false, - }) - @IsOptional() - @IsString() - //@MaxLength(27) // TODO Should it be Is Date? - pgTransactionDate?: string; - - @AutoMap() - @ApiProperty({ - example: 'CC', - description: 'Represents the payment method of a transaction.', - required: false, - }) - @IsOptional() - @IsString() - @Length(1, 2) - pgPaymentMethod?: string; - - @AutoMap() - @ApiProperty({ - enum: PaymentCardType, - example: PaymentCardType.VISA, - description: 'Represents the card type used for the transaction.', - required: false, - }) - @IsOptional() - @IsEnum(PaymentCardType) - pgCardType?: PaymentCardType; - @AutoMap() @ApiProperty({ example: 'This permit was voided because of so-and-so reason', diff --git a/vehicles/src/modules/permit-application-payment/permit/permit.service.ts b/vehicles/src/modules/permit-application-payment/permit/permit.service.ts index eb063ce040..6adc9fffd9 100644 --- a/vehicles/src/modules/permit-application-payment/permit/permit.service.ts +++ b/vehicles/src/modules/permit-application-payment/permit/permit.service.ts @@ -60,6 +60,7 @@ import { generateFaxEmail, validateEmailandFaxList, } from '../../../common/helper/notification.helper'; +import { TransactionType } from '../../../common/enum/transaction-type.enum'; @Injectable() export class PermitService { @@ -591,33 +592,46 @@ export class PermitService { }, ); - const createTransactionDto = new CreateTransactionDto(); - createTransactionDto.transactionTypeId = voidPermitDto.transactionTypeId; - createTransactionDto.paymentMethodTypeCode = - voidPermitDto.paymentMethodTypeCode; - createTransactionDto.paymentCardTypeCode = voidPermitDto.pgCardType; - createTransactionDto.pgCardType = voidPermitDto.pgCardType; - createTransactionDto.pgTransactionId = voidPermitDto.pgTransactionId; - createTransactionDto.pgPaymentMethod = voidPermitDto.pgPaymentMethod; - - // Refund for void should automatically set this flag to approved for payment gateway payment methods - // Otherwise, the flag is not applicable - if (voidPermitDto.paymentMethodTypeCode === PaymentMethodType.WEB) { - createTransactionDto.pgApproved = 1; - } - - createTransactionDto.applicationDetails = [ - { + if (voidPermitDto.transactionTypeId === TransactionType.REFUND) { + await this.paymentService.createRefundTransactions({ + currentUser, applicationId: newPermit.permitId, - transactionAmount: voidPermitDto.transactionAmount, - }, - ]; - await this.paymentService.createTransactions( - currentUser, - createTransactionDto, - queryRunner, - ); + transactions: voidPermitDto.transactions, + nestedQueryRunner: queryRunner, + }); + } + if (voidPermitDto.transactionTypeId === TransactionType.PURCHASE) { + const transaction = voidPermitDto?.transactions?.at(0); + const createTransactionDto = new CreateTransactionDto(); + createTransactionDto.transactionTypeId = + voidPermitDto.transactionTypeId; + createTransactionDto.paymentMethodTypeCode = + transaction.paymentMethodTypeCode; + createTransactionDto.paymentCardTypeCode = + transaction.paymentCardTypeCode; + createTransactionDto.pgCardType = transaction.paymentCardTypeCode; + createTransactionDto.pgTransactionId = transaction.pgTransactionId; + createTransactionDto.pgPaymentMethod = transaction.pgPaymentMethod; + + // Refund for void should automatically set this flag to approved for payment gateway payment methods + // Otherwise, the flag is not applicable + if (transaction.paymentMethodTypeCode === PaymentMethodType.WEB) { + createTransactionDto.pgApproved = 1; + } + + createTransactionDto.applicationDetails = [ + { + applicationId: newPermit.permitId, + transactionAmount: transaction.transactionAmount, + }, + ]; + await this.paymentService.createTransactions( + currentUser, + createTransactionDto, + queryRunner, + ); + } await queryRunner.commitTransaction(); success = permitId; voidRevokedPermitId = newPermit.permitId;