From 1c55ac29c5c48628ad52678c83e373ba7542edc9 Mon Sep 17 00:00:00 2001 From: zgong-gov <123983557+zgong-gov@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:35:06 -0700 Subject: [PATCH] ORV2-1346 (#670) Co-authored-by: praju-aot Co-authored-by: gchauhan-aot --- backend/dops/src/enum/template-name.enum.ts | 2 + .../common/enum/application-status.enum.ts | 1 + .../common/enum/payment-method-type.enum.ts | 3 + .../src/common/enum/template-name.enum.ts | 2 + .../src/common/enum/transaction-type.enum.ts | 9 + .../src/common/interface/receipt.interface.ts | 6 - .../common/payment-gateway-transaction.dto.ts | 109 ++++ .../create-application-transaction.dto.ts | 22 + .../request/create-permit-transaction.dto.ts | 18 - .../dto/request/create-transaction.dto.ts | 141 +---- .../read-payment-gateway-transaction.dto.ts | 3 + .../read-application-transaction.dto.ts | 3 + .../dto/response/read-moti-pay-details.dto.ts | 8 - .../read-payment-gateway-transaction.dto.ts | 12 + .../response/read-permit-transaction.dto.ts | 47 -- .../dto/response/read-transaction.dto.ts | 84 +-- .../entities/permit-transaction.entity.ts | 67 ++- .../payment/entities/receipt.entity.ts | 23 +- .../payment/entities/transaction.entity.ts | 152 +++-- .../src/modules/payment/payment.controller.ts | 92 ++- .../src/modules/payment/payment.module.ts | 5 +- .../src/modules/payment/payment.service.ts | 568 +++++++++++------- .../profile/permit-transaction.profile.ts | 20 - .../payment/profile/transaction.profile.ts | 153 ++++- .../modules/permit/application.controller.ts | 45 +- .../src/modules/permit/application.service.ts | 225 ++++--- .../permit/dto/request/issue-permit.dto.ts | 29 +- .../permit/dto/request/void-permit.dto.ts | 69 ++- .../permit/dto/response/permit-history.dto.ts | 37 +- .../dto/response/read-application.dto.ts | 28 + .../permit/dto/response/read-permit.dto.ts | 28 + .../modules/permit/entities/permit.entity.ts | 15 +- .../src/modules/permit/permit.controller.ts | 26 +- .../src/modules/permit/permit.module.ts | 4 +- .../src/modules/permit/permit.service.ts | 312 ++++++---- .../permit/profile/application.profile.ts | 164 ++++- .../modules/permit/profile/permit.profile.ts | 139 +++-- .../sampledata/dops.ORBC_DOCUMENT.Table.sql | 4 +- .../dops.ORBC_DOCUMENT_TEMPLATE.Table.sql | 3 +- database/mssql/scripts/versions/v_4_ddl.sql | 1 + database/mssql/scripts/versions/v_7_ddl.sql | 99 ++- .../components/banners/CompanyBanner.tsx | 11 +- .../tests/helpers/CompanyBanner/prepare.tsx | 3 +- .../common/components/dashboard/Banner.scss | 26 + .../common/components/dashboard/Banner.tsx | 12 +- .../components/dashboard/Dashboard.scss | 20 +- .../common/components/error/ErrorPage.scss | 45 ++ .../src/common/components/error/ErrorPage.tsx | 35 ++ .../components/form/CustomFormComponents.tsx | 14 +- .../table/OnRouteBCTableRowActions.tsx | 2 +- frontend/src/common/helpers/formatDate.ts | 2 + frontend/src/common/pages/NotFound.tsx | 38 +- frontend/src/common/pages/Unauthorized.tsx | 40 +- frontend/src/common/pages/Unexpected.tsx | 43 +- .../common/pages/UniversalUnauthorized.tsx | 37 +- .../components/IDIRPermitSearchRowActions.tsx | 55 +- .../search/components/IDIRSearchResults.tsx | 23 +- .../features/idir/search/table/Columns.tsx | 17 +- .../forms/userManagement/EditUser.tsx | 4 +- .../userManagement/UserAuthRadioGroup.tsx | 6 +- .../manageProfile/pages/AddUserDashboard.tsx | 4 +- .../manageProfile/pages/CompanyInfo.tsx | 5 +- .../types/UserManagementColumns.tsx | 8 +- .../manageProfile/types/userManagement.d.ts | 28 +- .../permits/apiManager/endpoints/endpoints.ts | 1 + .../features/permits/apiManager/permitsAPI.ts | 227 +++++-- .../dashboard/ApplicationDashboard.tsx | 57 +- .../dashboard/ManageApplicationDashboard.tsx | 2 +- .../fixtures/getActiveApplication.ts | 3 +- .../integration/fixtures/getUserDetails.ts | 4 +- .../components/feeSummary/FeeSummary.scss | 41 ++ .../components/feeSummary/FeeSummary.tsx | 57 ++ .../components/form/ApplicationDetails.tsx | 38 +- .../form/tests/ApplicationDetails.test.tsx | 3 +- .../helpers/ApplicationDetails/prepare.tsx | 7 +- .../components/permit-list/Columns.tsx | 11 +- .../components/permit-list/PermitChip.tsx | 64 +- .../components/progressBar/ProgressBar.tsx | 13 +- .../features/permits/constants/constants.ts | 4 +- .../features/permits/helpers/feeSummary.ts | 49 ++ .../helpers/getDefaultApplicationFormData.ts | 30 +- .../src/features/permits/helpers/mappers.ts | 42 +- .../src/features/permits/helpers/payment.ts | 63 +- frontend/src/features/permits/hooks/hooks.ts | 159 ++++- .../hooks/useDefaultApplicationFormData.ts | 1 + .../hooks/usePermitVehicleManagement.ts | 120 ++++ .../permits/pages/Payment/PaymentRedirect.tsx | 89 ++- .../permits/pages/Refund/RefundPage.scss | 147 +++++ .../permits/pages/Refund/RefundPage.tsx | 336 +++++++++++ .../components/TransactionHistoryTable.scss | 52 ++ .../components/TransactionHistoryTable.tsx | 103 ++++ .../pages/Refund/types/RefundFormData.ts | 13 + .../pages/SuccessPage/SuccessPage.scss | 34 +- .../permits/pages/SuccessPage/SuccessPage.tsx | 63 +- .../pages/TermOversize/TermOversize.scss | 46 -- .../pages/TermOversize/TermOversizeForm.tsx | 255 ++------ .../pages/TermOversize/TermOversizePay.scss | 11 + .../pages/TermOversize/TermOversizePay.tsx | 222 ++----- .../pages/TermOversize/TermOversizeReview.tsx | 119 ++-- .../{ => components}/form/ConditionsTable.tsx | 9 +- .../components/form/FormActions.scss | 63 ++ .../components/form/FormActions.tsx | 85 +++ .../{ => components}/form/PermitDetails.tsx | 21 +- .../components/form/PermitForm.scss | 10 + .../components/form/PermitForm.tsx | 84 +++ .../form/VehicleDetails/VehicleDetails.tsx | 32 +- .../customFields/SelectPermitType.tsx | 6 +- .../customFields/SelectUnitOrPlate.tsx | 5 +- .../customFields/SelectVehicleDropdown.scss | 5 + .../customFields/SelectVehicleDropdown.tsx | 14 +- .../customFields/StartApplicationButton.tsx | 33 +- .../form/tests/PermitDetails.test.tsx | 2 +- .../form/tests/helpers/access.ts | 3 +- .../form/tests/helpers/prepare.tsx | 6 +- .../components/pay/ApplicationSummary.scss | 20 + .../components/pay/ApplicationSummary.tsx | 39 ++ .../components/pay/PermitPayFeeSummary.scss | 23 + .../components/pay/PermitPayFeeSummary.tsx | 44 ++ .../review/ConfirmationCheckboxes.scss | 16 + .../review}/ConfirmationCheckboxes.tsx | 25 +- .../components/review/PermitReview.scss | 11 + .../components/review/PermitReview.tsx | 96 +++ .../components/review/ReviewActions.scss | 22 + .../components/review/ReviewActions.tsx | 42 ++ .../review/ReviewConditionsTable.scss | 2 +- .../review/ReviewConditionsTable.tsx | 15 +- .../review/ReviewContactDetails.scss | 2 +- .../review/ReviewContactDetails.tsx | 29 +- .../review/ReviewFeeSummary.scss | 8 +- .../review/ReviewFeeSummary.tsx | 25 +- .../review/ReviewPermitDetails.scss | 2 +- .../review/ReviewPermitDetails.tsx | 27 +- .../review/ReviewVehicleInfo.scss | 2 +- .../review/ReviewVehicleInfo.tsx | 54 +- .../TermOversize/review/FeeSummaryBanner.scss | 37 -- .../TermOversize/review/FeeSummaryBanner.tsx | 53 -- .../tests/TermOversizeReview.test.tsx | 16 +- .../helpers/TermOversizeReview/access.ts | 2 +- .../permits/pages/Void/FinishVoid.tsx | 71 +++ .../permits/pages/Void/VoidPermit.scss | 13 + .../permits/pages/Void/VoidPermit.tsx | 152 +++++ .../pages/Void/components/Breadcrumb.scss | 24 + .../pages/Void/components/Breadcrumb.tsx | 44 ++ .../pages/Void/components/RevokeDialog.scss | 93 +++ .../pages/Void/components/RevokeDialog.tsx | 130 ++++ .../pages/Void/components/VoidPermitForm.scss | 131 ++++ .../pages/Void/components/VoidPermitForm.tsx | 245 ++++++++ .../Void/components/VoidPermitHeader.scss | 34 ++ .../Void/components/VoidPermitHeader.tsx | 100 +++ .../pages/Void/context/VoidPermitContext.ts | 20 + .../permits/pages/Void/helpers/mapper.ts | 34 ++ .../permits/pages/Void/hooks/useVoidPermit.ts | 29 + .../pages/Void/hooks/useVoidPermitForm.ts | 44 ++ .../permits/pages/Void/types/VoidPermit.ts | 32 + .../features/permits/types/PaymentMethod.ts | 202 +++++++ .../features/permits/types/PermitHistory.ts | 14 + .../features/permits/types/PermitStatus.ts | 24 + .../src/features/permits/types/PermitType.ts | 101 ++++ .../features/permits/types/application.d.ts | 10 +- .../src/features/permits/types/payment.d.ts | 64 +- .../src/features/permits/types/permit.d.ts | 24 +- .../src/features/wizard/UserInfoWizard.tsx | 4 +- .../dashboard/CreateProfileSteps.tsx | 3 +- frontend/src/routes/Routes.tsx | 7 + frontend/src/routes/constants.tsx | 2 + 165 files changed, 6137 insertions(+), 2246 deletions(-) create mode 100644 backend/vehicles/src/common/enum/payment-method-type.enum.ts create mode 100644 backend/vehicles/src/common/enum/transaction-type.enum.ts delete mode 100644 backend/vehicles/src/common/interface/receipt.interface.ts create mode 100644 backend/vehicles/src/modules/payment/dto/common/payment-gateway-transaction.dto.ts create mode 100644 backend/vehicles/src/modules/payment/dto/request/create-application-transaction.dto.ts delete mode 100644 backend/vehicles/src/modules/payment/dto/request/create-permit-transaction.dto.ts create mode 100644 backend/vehicles/src/modules/payment/dto/request/read-payment-gateway-transaction.dto.ts create mode 100644 backend/vehicles/src/modules/payment/dto/response/read-application-transaction.dto.ts delete mode 100644 backend/vehicles/src/modules/payment/dto/response/read-moti-pay-details.dto.ts create mode 100644 backend/vehicles/src/modules/payment/dto/response/read-payment-gateway-transaction.dto.ts delete mode 100644 backend/vehicles/src/modules/payment/dto/response/read-permit-transaction.dto.ts delete mode 100644 backend/vehicles/src/modules/payment/profile/permit-transaction.profile.ts create mode 100644 frontend/src/common/components/dashboard/Banner.scss create mode 100644 frontend/src/common/components/error/ErrorPage.scss create mode 100644 frontend/src/common/components/error/ErrorPage.tsx create mode 100644 frontend/src/features/permits/components/feeSummary/FeeSummary.scss create mode 100644 frontend/src/features/permits/components/feeSummary/FeeSummary.tsx create mode 100644 frontend/src/features/permits/helpers/feeSummary.ts create mode 100644 frontend/src/features/permits/hooks/usePermitVehicleManagement.ts create mode 100644 frontend/src/features/permits/pages/Refund/RefundPage.scss create mode 100644 frontend/src/features/permits/pages/Refund/RefundPage.tsx create mode 100644 frontend/src/features/permits/pages/Refund/components/TransactionHistoryTable.scss create mode 100644 frontend/src/features/permits/pages/Refund/components/TransactionHistoryTable.tsx create mode 100644 frontend/src/features/permits/pages/Refund/types/RefundFormData.ts delete mode 100644 frontend/src/features/permits/pages/TermOversize/TermOversize.scss create mode 100644 frontend/src/features/permits/pages/TermOversize/TermOversizePay.scss rename frontend/src/features/permits/pages/TermOversize/{ => components}/form/ConditionsTable.tsx (96%) create mode 100644 frontend/src/features/permits/pages/TermOversize/components/form/FormActions.scss create mode 100644 frontend/src/features/permits/pages/TermOversize/components/form/FormActions.tsx rename frontend/src/features/permits/pages/TermOversize/{ => components}/form/PermitDetails.tsx (87%) create mode 100644 frontend/src/features/permits/pages/TermOversize/components/form/PermitForm.scss create mode 100644 frontend/src/features/permits/pages/TermOversize/components/form/PermitForm.tsx rename frontend/src/features/permits/pages/TermOversize/{ => components}/form/VehicleDetails/VehicleDetails.tsx (92%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/form/VehicleDetails/customFields/SelectPermitType.tsx (78%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/form/VehicleDetails/customFields/SelectUnitOrPlate.tsx (84%) create mode 100644 frontend/src/features/permits/pages/TermOversize/components/form/VehicleDetails/customFields/SelectVehicleDropdown.scss rename frontend/src/features/permits/pages/TermOversize/{ => components}/form/VehicleDetails/customFields/SelectVehicleDropdown.tsx (87%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/form/VehicleDetails/customFields/StartApplicationButton.tsx (69%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/form/tests/PermitDetails.test.tsx (99%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/form/tests/helpers/access.ts (97%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/form/tests/helpers/prepare.tsx (92%) create mode 100644 frontend/src/features/permits/pages/TermOversize/components/pay/ApplicationSummary.scss create mode 100644 frontend/src/features/permits/pages/TermOversize/components/pay/ApplicationSummary.tsx create mode 100644 frontend/src/features/permits/pages/TermOversize/components/pay/PermitPayFeeSummary.scss create mode 100644 frontend/src/features/permits/pages/TermOversize/components/pay/PermitPayFeeSummary.tsx create mode 100644 frontend/src/features/permits/pages/TermOversize/components/review/ConfirmationCheckboxes.scss rename frontend/src/features/permits/pages/TermOversize/{form => components/review}/ConfirmationCheckboxes.tsx (74%) create mode 100644 frontend/src/features/permits/pages/TermOversize/components/review/PermitReview.scss create mode 100644 frontend/src/features/permits/pages/TermOversize/components/review/PermitReview.tsx create mode 100644 frontend/src/features/permits/pages/TermOversize/components/review/ReviewActions.scss create mode 100644 frontend/src/features/permits/pages/TermOversize/components/review/ReviewActions.tsx rename frontend/src/features/permits/pages/TermOversize/{ => components}/review/ReviewConditionsTable.scss (88%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/review/ReviewConditionsTable.tsx (87%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/review/ReviewContactDetails.scss (93%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/review/ReviewContactDetails.tsx (78%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/review/ReviewFeeSummary.scss (68%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/review/ReviewFeeSummary.tsx (51%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/review/ReviewPermitDetails.scss (92%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/review/ReviewPermitDetails.tsx (72%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/review/ReviewVehicleInfo.scss (92%) rename frontend/src/features/permits/pages/TermOversize/{ => components}/review/ReviewVehicleInfo.tsx (74%) delete mode 100644 frontend/src/features/permits/pages/TermOversize/review/FeeSummaryBanner.scss delete mode 100644 frontend/src/features/permits/pages/TermOversize/review/FeeSummaryBanner.tsx create mode 100644 frontend/src/features/permits/pages/Void/FinishVoid.tsx create mode 100644 frontend/src/features/permits/pages/Void/VoidPermit.scss create mode 100644 frontend/src/features/permits/pages/Void/VoidPermit.tsx create mode 100644 frontend/src/features/permits/pages/Void/components/Breadcrumb.scss create mode 100644 frontend/src/features/permits/pages/Void/components/Breadcrumb.tsx create mode 100644 frontend/src/features/permits/pages/Void/components/RevokeDialog.scss create mode 100644 frontend/src/features/permits/pages/Void/components/RevokeDialog.tsx create mode 100644 frontend/src/features/permits/pages/Void/components/VoidPermitForm.scss create mode 100644 frontend/src/features/permits/pages/Void/components/VoidPermitForm.tsx create mode 100644 frontend/src/features/permits/pages/Void/components/VoidPermitHeader.scss create mode 100644 frontend/src/features/permits/pages/Void/components/VoidPermitHeader.tsx create mode 100644 frontend/src/features/permits/pages/Void/context/VoidPermitContext.ts create mode 100644 frontend/src/features/permits/pages/Void/helpers/mapper.ts create mode 100644 frontend/src/features/permits/pages/Void/hooks/useVoidPermit.ts create mode 100644 frontend/src/features/permits/pages/Void/hooks/useVoidPermitForm.ts create mode 100644 frontend/src/features/permits/pages/Void/types/VoidPermit.ts create mode 100644 frontend/src/features/permits/types/PaymentMethod.ts create mode 100644 frontend/src/features/permits/types/PermitHistory.ts create mode 100644 frontend/src/features/permits/types/PermitStatus.ts create mode 100644 frontend/src/features/permits/types/PermitType.ts diff --git a/backend/dops/src/enum/template-name.enum.ts b/backend/dops/src/enum/template-name.enum.ts index ebeec37ae..8c87ffbcd 100644 --- a/backend/dops/src/enum/template-name.enum.ts +++ b/backend/dops/src/enum/template-name.enum.ts @@ -1,4 +1,6 @@ export enum TemplateName { PERMIT_TROS = 'PERMIT_TROS', PAYMENT_RECEIPT = 'PAYMENT_RECEIPT', + PERMIT_TROS_VOID = 'PERMIT_TROS_VOID', + PERMIT_TROS_REVOKED = 'PERMIT_TROS_REVOKED', } diff --git a/backend/vehicles/src/common/enum/application-status.enum.ts b/backend/vehicles/src/common/enum/application-status.enum.ts index 840424e58..28f262d14 100644 --- a/backend/vehicles/src/common/enum/application-status.enum.ts +++ b/backend/vehicles/src/common/enum/application-status.enum.ts @@ -7,6 +7,7 @@ export enum ApplicationStatus { UNDER_REVIEW = 'UNDER_REVIEW', WAITING_APPROVAL = 'WAITING_APPROVAL', WAITING_PAYMENT = 'WAITING_PAYMENT', + PAYMENT_COMPLETE = 'PAYMENT_COMPLETE', ISSUED = 'ISSUED', SUPERSEDED = 'SUPERSEDED', REVOKED = 'REVOKED', diff --git a/backend/vehicles/src/common/enum/payment-method-type.enum.ts b/backend/vehicles/src/common/enum/payment-method-type.enum.ts new file mode 100644 index 000000000..1cb1fa5ce --- /dev/null +++ b/backend/vehicles/src/common/enum/payment-method-type.enum.ts @@ -0,0 +1,3 @@ +export enum PaymentMethodType { + WEB = '1', +} diff --git a/backend/vehicles/src/common/enum/template-name.enum.ts b/backend/vehicles/src/common/enum/template-name.enum.ts index ebeec37ae..8c87ffbcd 100644 --- a/backend/vehicles/src/common/enum/template-name.enum.ts +++ b/backend/vehicles/src/common/enum/template-name.enum.ts @@ -1,4 +1,6 @@ export enum TemplateName { PERMIT_TROS = 'PERMIT_TROS', PAYMENT_RECEIPT = 'PAYMENT_RECEIPT', + PERMIT_TROS_VOID = 'PERMIT_TROS_VOID', + PERMIT_TROS_REVOKED = 'PERMIT_TROS_REVOKED', } diff --git a/backend/vehicles/src/common/enum/transaction-type.enum.ts b/backend/vehicles/src/common/enum/transaction-type.enum.ts new file mode 100644 index 000000000..4fd6ac360 --- /dev/null +++ b/backend/vehicles/src/common/enum/transaction-type.enum.ts @@ -0,0 +1,9 @@ +export enum TransactionType { + PURCHASE = 'P', + REFUND = 'R', + ZERO_AMOUNT = 'Z', + VOID_REFUND = 'VR', + VOID_PURHCASE = 'VC', + PRE_AUTH = 'PA', + PRE_AUTH_COMPLETION = 'PAC', +} diff --git a/backend/vehicles/src/common/interface/receipt.interface.ts b/backend/vehicles/src/common/interface/receipt.interface.ts deleted file mode 100644 index 979f6bcce..000000000 --- a/backend/vehicles/src/common/interface/receipt.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface IReceipt { - transactionOrderNumber: string; - transactionAmount: number; - transactionDate: Date; - paymentMethod: string; -} diff --git a/backend/vehicles/src/modules/payment/dto/common/payment-gateway-transaction.dto.ts b/backend/vehicles/src/modules/payment/dto/common/payment-gateway-transaction.dto.ts new file mode 100644 index 000000000..6c7d36eb9 --- /dev/null +++ b/backend/vehicles/src/modules/payment/dto/common/payment-gateway-transaction.dto.ts @@ -0,0 +1,109 @@ +import { AutoMap } from '@automapper/classes'; +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNumber, + IsOptional, + IsPositive, + IsString, + Length, + Max, + Min, +} from 'class-validator'; + +export class PaymentGatewayTransactionDto { + @AutoMap() + @ApiProperty({ + example: '10000148', + description: + 'Bambora-assigned eight-digit unique id number used to identify an individual transaction.', + }) + @IsOptional() + @IsString() + pgTransactionId: string; + + @AutoMap() + @ApiProperty({ + example: '1', + description: + 'Represents the approval result of a transaction. 0 = Transaction refused, 1 = Transaction approved', + }) + @IsOptional() + @IsNumber() + @IsPositive() + pgApproved: number; + + @AutoMap() + @ApiProperty({ + example: 'TEST', + description: + 'Represents the auth code of a transaction. If the transaction is approved this parameter will contain a unique bank-issued code.', + }) + @IsOptional() + @IsString() + @Length(1, 32) + pgAuthCode: string; + + @AutoMap() + @ApiProperty({ + example: 'VI', + description: 'Represents the type of card used in the transaction.', + }) + @IsOptional() + @IsString() + @Length(1, 2) + pgCardType: string; + + @AutoMap() + @ApiProperty({ + example: '6/23/2023 10:57:28 PM', + description: + 'Represents the date and time that the transaction was processed.', + }) + @IsOptional() + @IsString() + pgTransactionDate: string; + + @AutoMap() + @ApiProperty({ + example: '1', + description: 'Represents the card cvd match status.', + }) + @IsOptional() + @IsNumber() + @IsPositive() + @Min(1) + @Max(6) + pgCvdId: number; + + @AutoMap() + @ApiProperty({ + example: 'CC', + description: 'Represents the payment method of a transaction.', + }) + @IsOptional() + @IsString() + @Length(1, 2) + pgPaymentMethod: string; + + @AutoMap() + @ApiProperty({ + example: 111, + description: + 'References a detailed approved/declined transaction response message.', + }) + @IsOptional() + @IsNumber() + @IsPositive() + pgMessageId: number; + + @AutoMap() + @ApiProperty({ + example: 'Approved', + description: + 'Represents basic approved/declined message for a transaction.', + }) + @IsOptional() + @IsString() + @Length(1, 100) + pgMessageText: string; +} diff --git a/backend/vehicles/src/modules/payment/dto/request/create-application-transaction.dto.ts b/backend/vehicles/src/modules/payment/dto/request/create-application-transaction.dto.ts new file mode 100644 index 000000000..e20feb2f0 --- /dev/null +++ b/backend/vehicles/src/modules/payment/dto/request/create-application-transaction.dto.ts @@ -0,0 +1,22 @@ +import { AutoMap } from '@automapper/classes'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsNumberString, IsPositive } from 'class-validator'; + +export class CreateApplicationTransactionDto { + @AutoMap() + @ApiProperty({ + description: 'Application/Permit Id.', + example: '1', + }) + @IsNumberString() + applicationId: string; + + @AutoMap() + @ApiProperty({ + example: '30.00', + description: 'Represents the amount of the transaction.', + }) + @IsNumber() + @IsPositive() + transactionAmount: number; +} diff --git a/backend/vehicles/src/modules/payment/dto/request/create-permit-transaction.dto.ts b/backend/vehicles/src/modules/payment/dto/request/create-permit-transaction.dto.ts deleted file mode 100644 index 8a4b36fa7..000000000 --- a/backend/vehicles/src/modules/payment/dto/request/create-permit-transaction.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { AutoMap } from '@automapper/classes'; -import { ApiProperty } from '@nestjs/swagger'; - -export class CreatePermitTransactionDto { - @AutoMap() - @ApiProperty({ - example: '1', - description: 'Unique identifier for the permit metadata.', - }) - permitId: string; - - @AutoMap() - @ApiProperty({ - example: '1', - description: 'Represents the ID of a transaction.', - }) - transactionId: number; -} diff --git a/backend/vehicles/src/modules/payment/dto/request/create-transaction.dto.ts b/backend/vehicles/src/modules/payment/dto/request/create-transaction.dto.ts index 5a5e45b7e..71541f529 100644 --- a/backend/vehicles/src/modules/payment/dto/request/create-transaction.dto.ts +++ b/backend/vehicles/src/modules/payment/dto/request/create-transaction.dto.ts @@ -1,134 +1,41 @@ import { AutoMap } from '@automapper/classes'; import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber, IsString, MaxLength } from 'class-validator'; - -export class CreateTransactionDto { - // @AutoMap() - // @ApiProperty({ - // example: '1', - // description: 'Unique identifier for the transaction metadata.', - // }) - // transactionId: number; - - @AutoMap() - @ApiProperty({ - example: 'P', - description: - 'Represents the original value sent to indicate the type of transaction to perform (i.e. P, R, VP, VR, PA, PAC, Q).', - }) - @IsString() - @MaxLength(3) - transactionType: string; - - @AutoMap() - @ApiProperty({ - example: 'T-1687586193681', - description: 'Represents the auth code of a transaction.', - }) - @IsString() - transactionOrderNumber: string; - - @AutoMap() - @ApiProperty({ - example: '10000148', - description: - 'Bambora-assigned eight-digit unique id number used to identify an individual transaction.', - }) - @IsNumber() - providerTransactionId: number; - - @AutoMap() - @ApiProperty({ - example: '30.00', - description: 'Represents the amount of the transaction.', - }) - @IsNumber() - transactionAmount: number; - - @AutoMap() - @ApiProperty({ - example: '1', - description: - 'Represents the approval result of a transaction. 0 = Transaction refused, 1 = Transaction approved', - }) - @IsNumber() - approved: number; +import { ArrayMinSize, IsArray, IsEnum, ValidateNested } from 'class-validator'; +import { TransactionType } from '../../../../common/enum/transaction-type.enum'; +import { PaymentMethodType } from '../../../../common/enum/payment-method-type.enum'; +import { CreateApplicationTransactionDto } from './create-application-transaction.dto'; +import { Type } from 'class-transformer'; +import { PaymentGatewayTransactionDto } from '../common/payment-gateway-transaction.dto'; +export class CreateTransactionDto extends PaymentGatewayTransactionDto { @AutoMap() @ApiProperty({ - example: 'TEST', + enum: TransactionType, + example: TransactionType.PURCHASE, description: - 'Represents the auth code of a transaction. If the transaction is approved this parameter will contain a unique bank-issued code.', + 'Represents the original value sent to indicate the type of transaction to perform.', }) - @IsString() - authCode: string; + @IsEnum(TransactionType) + transactionTypeId: TransactionType; @AutoMap() @ApiProperty({ - example: 'VI', - description: 'Represents the type of card used in the transaction.', - }) - @IsString() - cardType: string; - - @AutoMap() - @ApiProperty({ - example: '6/23/2023 10:57:28 PM', - description: - 'Represents the date and time that the transaction was processed.', - }) - @IsString() - transactionDate: string; - - @AutoMap() - @ApiProperty({ - example: '1', - description: 'Represents the card cvd match status.', - }) - @IsNumber() - cvdId: number; - - @AutoMap() - @ApiProperty({ - example: 'CC', - description: 'Represents the payment method of a transaction.', - }) - @IsString() - paymentMethod: string; - - @AutoMap() - @ApiProperty({ - example: 1, + enum: PaymentMethodType, + example: PaymentMethodType.WEB, description: 'The identifier of the user selected payment method.', }) - @IsNumber() - paymentMethodId: number; - - @AutoMap() - @ApiProperty({ - example: '111', - description: - 'References a detailed approved/declined transaction response message.', - }) - @IsString() - messageId: string; + @IsEnum(PaymentMethodType) + paymentMethodId: PaymentMethodType; @AutoMap() @ApiProperty({ - example: 'Approved', - description: - 'Represents basic approved/declined message for a transaction.', + description: 'The transaction details specific to application/permit.', + required: true, + type: [CreateApplicationTransactionDto], }) - @IsString() - messageText: string; - - // @AutoMap() - // @ApiProperty({ - // description: 'Application Ids.', - // isArray: true, - // type: String, - // example: ['1', '2'], - // }) - // @IsNumberString({}, { each: true }) - // applicationIds: string[]; + @IsArray() + @ValidateNested({ each: true }) + @ArrayMinSize(1) + @Type(() => CreateApplicationTransactionDto) + applicationDetails: CreateApplicationTransactionDto[]; } diff --git a/backend/vehicles/src/modules/payment/dto/request/read-payment-gateway-transaction.dto.ts b/backend/vehicles/src/modules/payment/dto/request/read-payment-gateway-transaction.dto.ts new file mode 100644 index 000000000..9e1a7dfc1 --- /dev/null +++ b/backend/vehicles/src/modules/payment/dto/request/read-payment-gateway-transaction.dto.ts @@ -0,0 +1,3 @@ +import { PaymentGatewayTransactionDto } from '../common/payment-gateway-transaction.dto'; + +export class UpdatePaymentGatewayTransactionDto extends PaymentGatewayTransactionDto {} diff --git a/backend/vehicles/src/modules/payment/dto/response/read-application-transaction.dto.ts b/backend/vehicles/src/modules/payment/dto/response/read-application-transaction.dto.ts new file mode 100644 index 000000000..bdb756ab4 --- /dev/null +++ b/backend/vehicles/src/modules/payment/dto/response/read-application-transaction.dto.ts @@ -0,0 +1,3 @@ +import { CreateApplicationTransactionDto } from '../request/create-application-transaction.dto'; + +export class ReadApplicationTransactionDto extends CreateApplicationTransactionDto {} diff --git a/backend/vehicles/src/modules/payment/dto/response/read-moti-pay-details.dto.ts b/backend/vehicles/src/modules/payment/dto/response/read-moti-pay-details.dto.ts deleted file mode 100644 index 051c95f52..000000000 --- a/backend/vehicles/src/modules/payment/dto/response/read-moti-pay-details.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class MotiPayDetailsDto { - url: string; - transactionOrderNumber: string; - transactionAmount: number; - transactionType: string; - transactionSubmitDate: string; - paymentMethodId: number; -} diff --git a/backend/vehicles/src/modules/payment/dto/response/read-payment-gateway-transaction.dto.ts b/backend/vehicles/src/modules/payment/dto/response/read-payment-gateway-transaction.dto.ts new file mode 100644 index 000000000..cfa69bbf7 --- /dev/null +++ b/backend/vehicles/src/modules/payment/dto/response/read-payment-gateway-transaction.dto.ts @@ -0,0 +1,12 @@ +import { AutoMap } from '@automapper/classes'; +import { PaymentGatewayTransactionDto } from '../common/payment-gateway-transaction.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ReadPaymentGatewayTransactionDto extends PaymentGatewayTransactionDto { + @AutoMap() + @ApiProperty({ + example: '1', + description: 'Unique identifier for the transaction metadata.', + }) + transactionId: string; +} diff --git a/backend/vehicles/src/modules/payment/dto/response/read-permit-transaction.dto.ts b/backend/vehicles/src/modules/payment/dto/response/read-permit-transaction.dto.ts deleted file mode 100644 index dffbce88d..000000000 --- a/backend/vehicles/src/modules/payment/dto/response/read-permit-transaction.dto.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AutoMap } from '@automapper/classes'; -import { ApiProperty } from '@nestjs/swagger'; - -export class ReadPermitTransactionDto { - @AutoMap() - @ApiProperty({ - example: '1', - description: 'Unique identifier for the permit metadata.', - }) - permitId: string; - - @AutoMap() - @ApiProperty({ - example: '1', - description: 'Unique identifier for the transaction metadata.', - }) - transactionId: number; - - @AutoMap() - @ApiProperty({ - example: 'T1687586193681', - description: 'Represents the auth code of a transaction.', - }) - transactionOrderNumber: string; - - // TODO: Multiple permits to be associated with a transaction - // TODO: Use PermitDto - // @AutoMap() - // @ApiProperty({ - // description: 'Permit Ids.', - // isArray: true, - // type: String, - // example: ['1', '2'], - // }) - // permits?: Permit[]; - - // TODO: Multiple transactions - // TODO: Use TransactionDto - // @AutoMap() - // @ApiProperty({ - // description: 'Transaction Ids.', - // isArray: true, - // type: String, - // example: ['1', '2'], - // }) - // transactions?: Transaction[]; -} diff --git a/backend/vehicles/src/modules/payment/dto/response/read-transaction.dto.ts b/backend/vehicles/src/modules/payment/dto/response/read-transaction.dto.ts index 28092f9e4..8c349223e 100644 --- a/backend/vehicles/src/modules/payment/dto/response/read-transaction.dto.ts +++ b/backend/vehicles/src/modules/payment/dto/response/read-transaction.dto.ts @@ -1,7 +1,12 @@ import { AutoMap } from '@automapper/classes'; import { ApiProperty } from '@nestjs/swagger'; +import { TransactionType } from '../../../../common/enum/transaction-type.enum'; +import { PaymentMethodType } from '../../../../common/enum/payment-method-type.enum'; +import { ReadApplicationTransactionDto } from './read-application-transaction.dto'; +import { Type } from 'class-transformer'; +import { PaymentGatewayTransactionDto } from '../common/payment-gateway-transaction.dto'; -export class ReadTransactionDto { +export class ReadTransactionDto extends PaymentGatewayTransactionDto { @AutoMap() @ApiProperty({ example: '1', @@ -11,85 +16,50 @@ export class ReadTransactionDto { @AutoMap() @ApiProperty({ - example: 'P', + enum: TransactionType, + example: TransactionType.PURCHASE, description: - 'Represents the original value sent to indicate the type of transaction to perform (i.e. P, R, VP, VR, PA, PAC, Q).', + 'Represents the original value sent to indicate the type of transaction to perform.', }) - transactionType: string; + transactionTypeId: TransactionType; @AutoMap() @ApiProperty({ - example: 'T-1687586193681', - description: 'Represents the auth code of a transaction.', + enum: TransactionType, + example: PaymentMethodType.WEB, + description: 'The identifier of the user selected payment method.', }) - transactionOrderNumber: string; + paymentMethodId: PaymentMethodType; @AutoMap() @ApiProperty({ example: '30.00', - description: 'Represents the amount of the transaction.', + description: 'Represents the total amount of the transaction.', }) - transactionAmount: number; + totalTransactionAmount: number; @AutoMap() @ApiProperty({ - example: '1', + example: '2023-09-25T16:17:51.110Z', description: - 'Represents the approval result of a transaction. 0 = Transaction refused, 1 = Transaction approved', + 'Represents the date and time that the transaction was submitted.', }) - approved: number; + transactionSubmitDate: string; @AutoMap() @ApiProperty({ - example: 'TEST', - description: - 'Represents the auth code of a transaction. If the transaction is approved this parameter will contain a unique bank-issued code.', - }) - authCode: string; - - @AutoMap() - @ApiProperty({ - example: 'VI', - description: 'Represents the type of card used in the transaction.', - }) - cardType: string; - - @AutoMap() - @ApiProperty({ - example: '6/23/2023 10:57:28 PM', - description: - 'Represents the date and time that the transaction was processed.', - }) - transactionDate: string; - - @AutoMap() - @ApiProperty({ - example: '1', - description: 'Represents the card cvd match status.', - }) - cvdId: number; - - @AutoMap() - @ApiProperty({ - example: 'CC', - description: 'Represents the payment method of a transaction.', - }) - paymentMethod: string; - - @AutoMap() - @ApiProperty({ - example: 1, - description: 'The identifier of the user selected payment method.', + example: 'T-1687586193681', + description: 'Represents the auth code of a transaction.', }) - paymentMethodId: number; + transactionOrderNumber: string; @AutoMap() @ApiProperty({ - example: '111', - description: - 'References a detailed approved/declined transaction response message.', + description: 'The transaction details specific to application/permit.', + type: [ReadApplicationTransactionDto], }) - messageId: string; + @Type(() => ReadApplicationTransactionDto) + applicationDetails: ReadApplicationTransactionDto[]; @AutoMap() @ApiProperty({ @@ -97,5 +67,5 @@ export class ReadTransactionDto { description: 'Represents basic approved/declined message for a transaction.', }) - messageText: string; + url: string; } diff --git a/backend/vehicles/src/modules/payment/entities/permit-transaction.entity.ts b/backend/vehicles/src/modules/payment/entities/permit-transaction.entity.ts index 75e4441b0..20094ba87 100644 --- a/backend/vehicles/src/modules/payment/entities/permit-transaction.entity.ts +++ b/backend/vehicles/src/modules/payment/entities/permit-transaction.entity.ts @@ -1,45 +1,46 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Entity, PrimaryColumn } from 'typeorm'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; import { AutoMap } from '@automapper/classes'; import { Base } from 'src/modules/common/entities/base.entity'; +import { Permit } from '../../permit/entities/permit.entity'; +import { Transaction } from './transaction.entity'; +import { ApiProperty } from '@nestjs/swagger'; @Entity({ name: 'permit.ORBC_PERMIT_TRANSACTION' }) export class PermitTransaction extends Base { + /** + * A unique auto-generated ID for each permit transaction entity. + */ @AutoMap() - @ApiProperty({ - example: '1', - description: 'Unique identifier for the permit metadata.', - }) - @PrimaryColumn({ type: 'bigint', name: 'PERMIT_ID' }) - permitId: number; + @PrimaryGeneratedColumn({ type: 'bigint', name: 'ID' }) + id: string; + + @AutoMap(() => Permit) + @ManyToOne(() => Permit, (permit) => permit.permitTransactions) + @JoinColumn({ name: 'PERMIT_ID' }) + public permit: Permit; + + @AutoMap(() => Transaction) + @ManyToOne(() => Transaction, (transaction) => transaction.permitTransactions) + @JoinColumn({ name: 'TRANSACTION_ID' }) + public transaction: Transaction; @AutoMap() @ApiProperty({ - example: '10000203', - description: 'Represents the ID of a transaction.', + example: '30.00', + description: 'Represents the amount of the transaction.', }) - @PrimaryColumn({ - name: 'TRANSACTION_ID', + @Column({ + type: 'decimal', + precision: 9, + scale: 2, + name: 'TRANSACTION_AMOUNT', + nullable: false, }) - transactionId: string; - - // TODO: Implement many to many relationship for permits and transactions - // Many permits can be associated with a transaction - // Many transactions can be associated with a permit (example: cancelled, paid, refund, etc) - - // @ManyToOne( - // () => Permit, - // permit => permit.permitId, - // {onDelete: 'NO ACTION'} - // ) - // @JoinColumn([{ name: 'PERMIT_ID', referencedColumnName: 'permitId' }]) - // permits?: Permit[]; - - // @ManyToOne( - // () => Transaction, - // transaction => transaction.transactionId, - // {onDelete: 'NO ACTION'} - // ) - // @JoinColumn([{ name: 'TRANSACTION_ID', referencedColumnName: 'transactionId' }]) - // transactions?: Transaction[]; + transactionAmount: number; } diff --git a/backend/vehicles/src/modules/payment/entities/receipt.entity.ts b/backend/vehicles/src/modules/payment/entities/receipt.entity.ts index dc3e1cc60..340713eed 100644 --- a/backend/vehicles/src/modules/payment/entities/receipt.entity.ts +++ b/backend/vehicles/src/modules/payment/entities/receipt.entity.ts @@ -4,7 +4,7 @@ import { Column, PrimaryGeneratedColumn, OneToOne, - JoinTable, + JoinColumn, } from 'typeorm'; import { AutoMap } from '@automapper/classes'; import { Base } from '../../common/entities/base.entity'; @@ -33,17 +33,6 @@ export class Receipt extends Base { }) receiptNumber: string; - @AutoMap() - @ApiProperty({ - example: '10000203', - description: 'Represents the ID of a transaction.', - }) - @Column({ - name: 'TRANSACTION_ID', - nullable: false, - }) - transactionId: string; - @AutoMap() @ApiProperty({ description: 'Receipt Document ID used to retrieve the PDF of the receipt', @@ -54,13 +43,7 @@ export class Receipt extends Base { }) receiptDocumentId: string; - @OneToOne(() => Transaction, (transaction) => transaction.transactionId) - @JoinTable({ - name: 'permit.ORBC_TRANSACTION', - joinColumn: { - name: 'transactionId', - referencedColumnName: 'transactionId', - }, - }) + @OneToOne(() => Transaction, (transaction) => transaction.receipt) + @JoinColumn({ name: 'TRANSACTION_ID' }) transaction: Transaction; } diff --git a/backend/vehicles/src/modules/payment/entities/transaction.entity.ts b/backend/vehicles/src/modules/payment/entities/transaction.entity.ts index d8ae96d47..9dc7d8299 100644 --- a/backend/vehicles/src/modules/payment/entities/transaction.entity.ts +++ b/backend/vehicles/src/modules/payment/entities/transaction.entity.ts @@ -3,14 +3,15 @@ import { Entity, Column, PrimaryGeneratedColumn, - ManyToMany, - JoinTable, OneToOne, + OneToMany, } from 'typeorm'; import { AutoMap } from '@automapper/classes'; import { Base } from '../../common/entities/base.entity'; -import { Permit } from 'src/modules/permit/entities/permit.entity'; import { Receipt } from './receipt.entity'; +import { PaymentMethodType } from '../../../common/enum/payment-method-type.enum'; +import { TransactionType } from '../../../common/enum/transaction-type.enum'; +import { PermitTransaction } from './permit-transaction.entity'; @Entity({ name: 'permit.ORBC_TRANSACTION' }) export class Transaction extends Base { @@ -24,16 +25,60 @@ export class Transaction extends Base { @AutoMap() @ApiProperty({ - example: 'P', + example: TransactionType.PURCHASE, description: 'Represents the original value sent to indicate the type of transaction to perform (i.e. P, R, VP, VR, PA, PAC, Q).', }) @Column({ + type: 'simple-enum', + enum: TransactionType, length: '3', name: 'TRANSACTION_TYPE', nullable: false, }) - transactionType: string; + transactionTypeId: TransactionType; + + @AutoMap() + @ApiProperty({ + example: PaymentMethodType.WEB, + description: 'The identifier of the user selected payment method.', + }) + @Column({ + type: 'simple-enum', + enum: PaymentMethodType, + name: 'PAYMENT_METHOD_TYPE', + nullable: false, + }) + paymentMethodId: PaymentMethodType; + + @AutoMap() + @ApiProperty({ + example: '30.00', + description: 'Represents the total amount of the transaction.', + }) + @Column({ + type: 'decimal', + precision: 9, + scale: 2, + name: 'TOTAL_TRANSACTION_AMOUNT', + nullable: false, + }) + totalTransactionAmount: number; + + @AutoMap() + @ApiProperty({ + example: '2023-07-06T14:49:53.508Z', + description: + 'Represents the date and time that the transaction was submitted (user clicks Pay Now).', + }) + @Column({ + insert: false, + update: false, + default: () => 'GETUTCDATETIME()', + name: 'TRANSACTION_SUBMIT_DATE', + nullable: false, + }) + transactionSubmitDate: Date; // TODO: Max length is 10? @AutoMap() @@ -54,19 +99,8 @@ export class Transaction extends Base { description: 'Bambora-assigned eight-digit unique id number used to identify an individual transaction.', }) - @Column({ type: 'bigint', name: 'PROVIDER_TRANSACTION_ID' }) - providerTransactionId: number; - - @AutoMap() - @ApiProperty({ - example: '30.00', - description: 'Represents the amount of the transaction.', - }) - @Column({ - name: 'TRANSACTION_AMOUNT', - nullable: false, - }) - transactionAmount: number; + @Column({ type: 'bigint', name: 'PG_TRANSACTION_ID' }) + pgTransactionId: string; @AutoMap() @ApiProperty({ @@ -74,8 +108,8 @@ export class Transaction extends Base { description: 'Represents the approval result of a transaction. 0 = Transaction refused, 1 = Transaction approved', }) - @Column({ type: 'int', name: 'TRANSACTION_APPROVED', nullable: false }) - approved: number; + @Column({ type: 'int', name: 'PG_TRANSACTION_APPROVED', nullable: false }) + pgApproved: number; @AutoMap() @ApiProperty({ @@ -85,10 +119,10 @@ export class Transaction extends Base { }) @Column({ length: '32', - name: 'AUTH_CODE', + name: 'PG_AUTH_CODE', nullable: false, }) - authCode: string; + pgAuthCode: string; @AutoMap() @ApiProperty({ @@ -97,22 +131,10 @@ export class Transaction extends Base { }) @Column({ length: '2', - name: 'TRANSACTION_CARD_TYPE', - nullable: false, - }) - cardType: string; - - @AutoMap() - @ApiProperty({ - example: '2023-07-06T14:49:53.508Z', - description: - 'Represents the date and time that the transaction was submitted (user clicks Pay Now).', - }) - @Column({ - name: 'TRANSACTION_SUBMIT_DATE', + name: 'PG_TRANSACTION_CARD_TYPE', nullable: false, }) - transactionSubmitDate: Date; + pgCardType: string; @AutoMap() @ApiProperty({ @@ -123,11 +145,10 @@ export class Transaction extends Base { @Column({ insert: false, update: false, - name: 'TRANSACTION_DATE', + name: 'PG_TRANSACTION_DATE', nullable: false, - type: 'date', }) - transactionDate: Date; + pgTransactionDate: Date; @AutoMap() @ApiProperty({ @@ -135,10 +156,10 @@ export class Transaction extends Base { description: 'Represents the card cvd match status.', }) @Column({ - name: 'CVD_ID', + name: 'PG_CVD_ID', nullable: false, }) - cvdId: number; + pgCvdId: number; @AutoMap() @ApiProperty({ @@ -147,21 +168,10 @@ export class Transaction extends Base { }) @Column({ length: '2', - name: 'PAYMENT_METHOD', - nullable: false, - }) - paymentMethod: string; - - @AutoMap() - @ApiProperty({ - example: 1, - description: 'The identifier of the user selected payment method.', - }) - @Column({ - name: 'PAYMENT_METHOD_TYPE', + name: 'PG_PAYMENT_METHOD', nullable: false, }) - paymentMethodId: number; + pgPaymentMethod: string; @AutoMap() @ApiProperty({ @@ -170,11 +180,11 @@ export class Transaction extends Base { 'References a detailed approved/declined transaction response message.', }) @Column({ - name: 'MESSAGE_ID', + name: 'PG_MESSAGE_ID', nullable: false, type: 'int', }) - messageId: string; + pgMessageId: string; @AutoMap() @ApiProperty({ @@ -184,29 +194,17 @@ export class Transaction extends Base { }) @Column({ length: '100', - name: 'MESSAGE_TEXT', + name: 'PG_MESSAGE_TEXT', nullable: false, }) - messageText: string; + pgMessageText: string; - // TODO: Implement many to many relationship for permits and transactions - // Many permits can be associated with a transaction - // Many transactions can be associated with a permit (example: cancelled, paid, refund, etc) - - @ManyToMany(() => Permit, (permit) => permit.transactions) - @JoinTable({ - name: 'permit.ORBC_PERMIT_TRANSACTION', - joinColumn: { - name: 'TRANSACTION_ID', - referencedColumnName: 'transactionId', - }, - inverseJoinColumn: { - name: 'PERMIT_ID', - referencedColumnName: 'permitId', - }, - }) - permits: Permit[]; - - @OneToOne(() => Receipt, (receipt) => receipt.transactionId) + @OneToOne(() => Receipt, (receipt) => receipt.transaction) receipt: Receipt; + + @OneToMany( + () => PermitTransaction, + (permitTransaction) => permitTransaction.transaction, + ) + public permitTransactions: PermitTransaction[]; } diff --git a/backend/vehicles/src/modules/payment/payment.controller.ts b/backend/vehicles/src/modules/payment/payment.controller.ts index ae349fe4c..cc37388a7 100644 --- a/backend/vehicles/src/modules/payment/payment.controller.ts +++ b/backend/vehicles/src/modules/payment/payment.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post, Query, Req } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Put, Req } from '@nestjs/common'; import { ApiBearerAuth, ApiCreatedResponse, @@ -10,12 +10,13 @@ import { } from '@nestjs/swagger'; import { ExceptionDto } from '../../common/exception/exception.dto'; import { PaymentService } from './payment.service'; -import { MotiPayDetailsDto } from './dto/response/read-moti-pay-details.dto'; import { CreateTransactionDto } from './dto/request/create-transaction.dto'; import { ReadTransactionDto } from './dto/response/read-transaction.dto'; import { IUserJWT } from 'src/common/interface/user-jwt.interface'; import { Request } from 'express'; -import { ReadPermitTransactionDto } from './dto/response/read-permit-transaction.dto'; +import { UpdatePaymentGatewayTransactionDto } from './dto/request/read-payment-gateway-transaction.dto'; +import { ReadPaymentGatewayTransactionDto } from './dto/response/read-payment-gateway-transaction.dto'; +import { getDirectory } from 'src/common/helper/auth.helper'; @ApiBearerAuth() @ApiTags('Payment') @@ -35,69 +36,60 @@ import { ReadPermitTransactionDto } from './dto/response/read-permit-transaction export class PaymentController { constructor(private readonly paymentService: PaymentService) {} - @ApiOkResponse({ - description: 'The MOTI Pay Resource', - }) - @Get() - async forwardTransactionDetails( - @Query('paymentMethodId') paymentMethodId: number, - @Query('transactionSubmitDate') transactionSubmitDate: string, - @Query('transactionAmount') transactionAmount: number, - @Query('permitIds') permitIds: string, - ): Promise { - const permitIdArray: number[] = permitIds.split(',').map(Number); - - const paymentDetails = await this.paymentService.forwardTransactionDetails( - paymentMethodId, - transactionSubmitDate, - transactionAmount, - ); - - const permitTransactions = await this.paymentService.createTransaction( - permitIdArray, - paymentDetails, - ); - - return this.paymentService.generateUrl( - paymentDetails, - permitTransactions.map((permitTransaction) => permitTransaction.permitId), - permitTransactions.map( - (permitTransaction) => permitTransaction.transactionId, - ), - ); - } - @ApiCreatedResponse({ description: 'The Transaction Resource', type: ReadTransactionDto, }) @Post() - async updateTransaction( + async createTransactionDetails( @Req() request: Request, @Body() createTransactionDto: CreateTransactionDto, - ) { + ): Promise { const currentUser = request.user as IUserJWT; + const directory = getDirectory(currentUser); - return await this.paymentService.updateTransaction( + const paymentDetails = await this.paymentService.createTransactions( currentUser, createTransactionDto, + directory, ); + + return paymentDetails; } @ApiOkResponse({ - description: 'The Permit Transaction Resource', - type: ReadPermitTransactionDto, + description: 'The Payment Gateway Transaction Resource', + type: ReadPaymentGatewayTransactionDto, }) - @Get('/:transactionOrderNumber/permit') - async getPermitTransaction( + @Put(':transactionId/payment-gateway') + async updateTransactionDetails( @Req() request: Request, - @Param('transactionOrderNumber') transactionOrderNumber: string, - ): Promise { - const transaction = await this.paymentService.findOneTransaction( - transactionOrderNumber, - ); - return await this.paymentService.findOnePermitTransaction( - transaction.transactionId, + @Param('transactionId') transactionId: string, + @Body() + updatePaymentGatewayTransactionDto: UpdatePaymentGatewayTransactionDto, + ): Promise { + const currentUser = request.user as IUserJWT; + const directory = getDirectory(currentUser); + + const paymentDetails = await this.paymentService.updateTransactions( + currentUser, + transactionId, + updatePaymentGatewayTransactionDto, + directory, ); + + return paymentDetails; + } + + @ApiOkResponse({ + description: 'The Read Transaction Resource', + type: ReadTransactionDto, + }) + @Get(':transactionId') + async findTransaction( + @Req() request: Request, + @Param('transactionId') transactionId: string, + ): Promise { + return await this.paymentService.findTransaction(transactionId); } -} \ No newline at end of file +} diff --git a/backend/vehicles/src/modules/payment/payment.module.ts b/backend/vehicles/src/modules/payment/payment.module.ts index c9f3589cd..840240750 100644 --- a/backend/vehicles/src/modules/payment/payment.module.ts +++ b/backend/vehicles/src/modules/payment/payment.module.ts @@ -4,18 +4,15 @@ import { PaymentController } from './payment.controller'; import { Transaction } from './entities/transaction.entity'; import { TypeOrmModule } from '@nestjs/typeorm'; import { TransactionProfile } from './profile/transaction.profile'; -import { PermitTransactionProfile } from './profile/permit-transaction.profile'; import { PermitTransaction } from './entities/permit-transaction.entity'; -import { PermitModule } from '../permit/permit.module'; import { Receipt } from './entities/receipt.entity'; @Module({ imports: [ TypeOrmModule.forFeature([Transaction, PermitTransaction, Receipt]), - PermitModule, ], controllers: [PaymentController], - providers: [PaymentService, TransactionProfile, PermitTransactionProfile], + providers: [PaymentService, TransactionProfile], exports: [PaymentService], }) export class PaymentModule {} diff --git a/backend/vehicles/src/modules/payment/payment.service.ts b/backend/vehicles/src/modules/payment/payment.service.ts index d14c27db5..a15b68c76 100644 --- a/backend/vehicles/src/modules/payment/payment.service.ts +++ b/backend/vehicles/src/modules/payment/payment.service.ts @@ -1,31 +1,36 @@ -import { HttpException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; import * as CryptoJS from 'crypto-js'; -import { IPayment } from '../../common/interface/payment.interface'; import { CreateTransactionDto } from './dto/request/create-transaction.dto'; import { ReadTransactionDto } from './dto/response/read-transaction.dto'; import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; import { Transaction } from './entities/transaction.entity'; import { InjectRepository } from '@nestjs/typeorm'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource, QueryRunner, Repository, UpdateResult } from 'typeorm'; import { PermitTransaction } from './entities/permit-transaction.entity'; -import { ReadPermitTransactionDto } from './dto/response/read-permit-transaction.dto'; -import { MotiPayDetailsDto } from './dto/response/read-moti-pay-details.dto'; -import { ApplicationService } from '../permit/application.service'; import { IUserJWT } from 'src/common/interface/user-jwt.interface'; -import { IReceipt } from 'src/common/interface/receipt.interface'; import { callDatabaseSequence } from 'src/common/helper/database.helper'; +import { Permit } from '../permit/entities/permit.entity'; +import { ApplicationStatus } from '../../common/enum/application-status.enum'; +import { PaymentMethodType } from '../../common/enum/payment-method-type.enum'; +import { TransactionType } from '../../common/enum/transaction-type.enum'; +import { UpdatePaymentGatewayTransactionDto } from './dto/request/read-payment-gateway-transaction.dto'; +import { ReadPaymentGatewayTransactionDto } from './dto/response/read-payment-gateway-transaction.dto'; +import { Receipt } from './entities/receipt.entity'; +import { Directory } from 'src/common/enum/directory.enum'; @Injectable() export class PaymentService { constructor( - @InjectRepository(PermitTransaction) - private permitTransactionRepository: Repository, private dataSource: DataSource, @InjectRepository(Transaction) private transactionRepository: Repository, @InjectMapper() private readonly classMapper: Mapper, - private applicationService: ApplicationService, ) {} private generateHashExpiry = (currDate?: Date) => { @@ -49,32 +54,31 @@ export class PaymentService { return `${year}${monthPadded}${dayPadded}${hoursPadded}${minutesPadded}`; }; - private queryHash = ( - transactionType: string, - transactionNumber: string, - transactionAmount: string, - permitIds?: number[], - transactionIds?: string[], - ) => { + private queryHash = (transaction: Transaction) => { + const permitIds = transaction.permitTransactions.map( + (permitTransaction) => { + return permitTransaction.permit.permitId; + }, + ); + // Construct the URL with the transaction details for the payment gateway - const redirectUrl = - permitIds && transactionIds - ? `${process.env.MOTIPAY_REDIRECT}` + - encodeURIComponent( - `?permitIds=${permitIds.join( - ',', - )}&transactionIds=${transactionIds.join(',')}`, - ) - : `${process.env.MOTIPAY_REDIRECT}`; + const redirectUrl = permitIds + ? `${process.env.MOTIPAY_REDIRECT}` + + encodeURIComponent( + `?permitIds=${permitIds.join(',')}&transactionId=${ + transaction.transactionId + }`, + ) + : `${process.env.MOTIPAY_REDIRECT}`; // There should be a better way of doing this which is not as rigid - something like // dynamically removing the hashValue param from the actual query string instead of building // it up manually below, but this is sufficient for now. const queryString = `merchant_id=${process.env.MOTIPAY_MERCHANT_ID}` + - `&trnType=${transactionType}` + - `&trnOrderNumber=${transactionNumber}` + - `&trnAmount=${transactionAmount}` + + `&trnType=${transaction.transactionTypeId}` + + `&trnOrderNumber=${transaction.transactionOrderNumber}` + + `&trnAmount=${transaction.totalTransactionAmount}` + `&approvedPage=${redirectUrl}` + `&declinedPage=${redirectUrl}`; @@ -88,18 +92,23 @@ export class PaymentService { return { queryString, motiPayHash, hashExpiry }; }; + generateUrl(transaction: Transaction): string { + // Construct the URL with the transaction details for the payment gateway + const { queryString, motiPayHash } = this.queryHash(transaction); + + const url = + `${process.env.MOTIPAY_BASE_URL}?` + + `${queryString}` + + `&hashValue=${motiPayHash}`; + return url; + } + /** - * Generates a hash and other necessary values for a transaction. + * Generates a transaction Order Number for ORBC and for forwarding to the payment gateway. * - * @param {string} transactionAmount - The amount of the transaction. - * @returns {object} An object containing the transaction number, hash expiry, and hash. + * @returns {string} The Transaction Order Number. */ - private async createHash(transactionAmount: string): Promise { - // Get the current date and time - //const currDate = new Date(); - - // TODO: Generate a unique transaction number based on the current timestamp - + async generateTransactionOrderNumber(): Promise { const seq: number = parseInt( await callDatabaseSequence( 'permit.ORBC_TRANSACTION_NUMBER_SEQ', @@ -109,221 +118,348 @@ export class PaymentService { const trnNum = seq.toString(16); // const trnNum = 'T' + currDate.getTime().toString().substring(4); const currentDate = Date.now(); - const transactionNumber = + const transactionOrderNumber = 'T' + trnNum.padStart(9, '0').toUpperCase() + String(currentDate); - const { motiPayHash, hashExpiry } = this.queryHash( - 'P', - transactionNumber, - transactionAmount, + return transactionOrderNumber; + } + + /** + * Generate Receipt Number + */ + async generateReceiptNumber(): Promise { + const currentDate = new Date(); + const year = currentDate.getFullYear(); + const month = String(currentDate.getMonth() + 1).padStart(2, '0'); + const day = String(currentDate.getDate()).padStart(2, '0'); + const dateString = `${year}${month}${day}`; + const source = dateString; + const seq = await callDatabaseSequence( + 'permit.ORBC_RECEIPT_NUMBER_SEQ', + this.dataSource, ); + const receiptNumber = String(String(source) + '-' + String(seq)); - return { - transactionNumber: transactionNumber, - motipayHashExpiry: hashExpiry, - motipayHash: motiPayHash, - }; + return receiptNumber; } - generateUrl = ( - details: MotiPayDetailsDto, - permitIds: number[], - transactionIds: string[], - ) => { - // Construct the URL with the transaction details for the payment gateway - const { queryString, motiPayHash } = this.queryHash( - details.transactionType, - details.transactionOrderNumber, - `${details.transactionAmount}`, - permitIds, - transactionIds, + private isWebTransactionPurchase( + paymentMethod: PaymentMethodType, + transactionType: TransactionType, + ) { + return ( + paymentMethod == PaymentMethodType.WEB && + transactionType == TransactionType.PURCHASE ); + } - return { - ...details, - url: - `${process.env.MOTIPAY_BASE_URL}?` + - `${queryString}` + - `&hashValue=${motiPayHash}`, - }; - }; + private assertApplicationInProgress( + paymentMethod: PaymentMethodType, + transactionType: TransactionType, + permitStatus: ApplicationStatus, + ) { + if ( + this.isWebTransactionPurchase(paymentMethod, transactionType) && + permitStatus != ApplicationStatus.IN_PROGRESS + ) { + throw new BadRequestException('Application should be in Progress!!'); + } + } - /** - * Generates a URL with transaction details for forwarding the user to the payment gateway. - * - * @param {number} transactionAmount - The amount of the transaction. - * @returns {string} The URL containing transaction details for the payment gateway. - */ - async forwardTransactionDetails( - paymentMethodId: number, - transactionSubmitDate: string, - transactionAmount: number, - ): Promise { - // Generate the hash and other necessary values for the transaction - const hash = await this.createHash(transactionAmount.toString()); - const transactionNumber = hash.transactionNumber; - //let transactionType: string = null; - //transactionType (P) is for Payment, (R) is for refund - // if (transactionAmount >= 0) transactionType = 'P'; - //else transactionType = 'R'; - const transactionType = 'P'; - - return { - url: '', - transactionOrderNumber: transactionNumber, - transactionAmount: transactionAmount, - transactionType: transactionType, - transactionSubmitDate: transactionSubmitDate, - paymentMethodId: paymentMethodId, - }; + private isRefundOrZero(transactionType: TransactionType) { + return ( + transactionType == TransactionType.REFUND || + transactionType == TransactionType.ZERO_AMOUNT + ); } /** - * Updates a transaction in the system based on the provided data. - * - * @param {IUserJWT} currentUser - The current user making the update request (JWT user object). - * @param {CreateTransactionDto} transaction - The data representing the updated transaction (CreateTransactionDto). - * @returns {ReadTransactionDto} A promise that resolves to the updated transaction data (ReadTransactionDto). - * @throws HttpException with status 500 if the transaction update fails. + * 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. + * @returns {ReadTransactionDto} The created transaction of type {@link ReadTransactionDto}. */ - async updateTransaction( + async createTransactions( currentUser: IUserJWT, - transaction: CreateTransactionDto, + createTransactionDto: CreateTransactionDto, + directory: Directory, + nestedQueryRunner?: QueryRunner, ): Promise { - // Retrieve the existing transaction from the database based on the provided transaction order number. - const existingTransaction = await this.findOneTransaction( - transaction.transactionOrderNumber, - ); - - // Find the corresponding permit transaction for the existing transaction. - const existingPermitTransaction = await this.findOnePermitTransaction( - existingTransaction.transactionId, - ); - - // Map the updated transaction data (CreateTransactionDto) to a new Transaction object. - const newTransaction = await this.classMapper.mapAsync( - transaction, - CreateTransactionDto, - Transaction, - ); - // If the updated transaction is approved, issue a permit using the application service. - if (newTransaction.approved) { - // Extract relevant transaction details for issuing the permit. - const applicationId = existingPermitTransaction.permitId.toString(); - const transactionDetails: IReceipt = { - transactionOrderNumber: newTransaction.transactionOrderNumber, - transactionAmount: newTransaction.transactionAmount, - transactionDate: newTransaction.transactionDate, - paymentMethod: newTransaction.paymentMethod, - }; - - // Call the application service to issue the permit. - await this.applicationService.issuePermit( - currentUser, - applicationId, - transactionDetails, + const totalTransactionAmount = + createTransactionDto.applicationDetails?.reduce( + (accumulator, item) => accumulator + item.transactionAmount, + 0, ); + const transactionOrderNumber = await this.generateTransactionOrderNumber(); + let readTransactionDto: ReadTransactionDto; + const queryRunner = + nestedQueryRunner || this.dataSource.createQueryRunner(); + if (!nestedQueryRunner) { + await queryRunner.connect(); + await queryRunner.startTransaction(); } + try { + let newTransaction = await this.classMapper.mapAsync( + createTransactionDto, + CreateTransactionDto, + Transaction, + { + extraArgs: () => ({ + transactionOrderNumber: transactionOrderNumber, + totalTransactionAmount: totalTransactionAmount, + userName: currentUser.userName, + userGUID: currentUser.userGUID, + timestamp: new Date(), + directory: directory, + }), + }, + ); - // Update the existing transaction record in the database with the new transaction data. - const updatedTransaction = await this.transactionRepository.update( - { transactionId: existingTransaction.transactionId }, - newTransaction, - ); + newTransaction = await queryRunner.manager.save(newTransaction); + + for (const application of createTransactionDto.applicationDetails) { + const existingApplication = await queryRunner.manager.findOne(Permit, { + where: { permitId: application.applicationId }, + }); + + this.assertApplicationInProgress( + newTransaction.paymentMethodId, + newTransaction.transactionTypeId, + existingApplication.permitStatus, + ); + + let newPermitTransactions = new PermitTransaction(); + newPermitTransactions.transaction = newTransaction; + newPermitTransactions.permit = existingApplication; + newPermitTransactions.createdDateTime = new Date(); + newPermitTransactions.createdUser = currentUser.userName; + newPermitTransactions.createdUserDirectory = directory; + newPermitTransactions.createdUserGuid = currentUser.userGUID; + newPermitTransactions.updatedDateTime = new Date(); + newPermitTransactions.updatedUser = currentUser.userName; + newPermitTransactions.updatedUserDirectory = directory; + newPermitTransactions.updatedUserGuid = currentUser.userGUID; + newPermitTransactions.transactionAmount = application.transactionAmount; + newPermitTransactions = await queryRunner.manager.save( + newPermitTransactions, + ); + + if ( + this.isWebTransactionPurchase( + newTransaction.paymentMethodId, + newTransaction.transactionTypeId, + ) + ) { + existingApplication.permitStatus = ApplicationStatus.WAITING_PAYMENT; + existingApplication.updatedDateTime = new Date(); + existingApplication.updatedUser = currentUser.userName; + existingApplication.updatedUserDirectory = directory; + existingApplication.updatedUserGuid = currentUser.userGUID; + + await queryRunner.manager.save(existingApplication); + } + } + + const createdTransaction = await queryRunner.manager.findOne( + Transaction, + { + where: { transactionId: newTransaction.transactionId }, + relations: ['permitTransactions', 'permitTransactions.permit'], + }, + ); - // Check if the transaction update was successful, if not, throw an exception. - if (!updatedTransaction.affected) { - throw new HttpException('Error updating transaction', 500); + let url: string = undefined; + if ( + this.isWebTransactionPurchase( + createdTransaction.paymentMethodId, + createdTransaction.transactionTypeId, + ) + ) { + url = this.generateUrl(createdTransaction); + } + + if (this.isRefundOrZero(createdTransaction.transactionTypeId)) { + const receiptNumber = await this.generateReceiptNumber(); + const receipt = new Receipt(); + receipt.receiptNumber = receiptNumber; + receipt.transaction = createdTransaction; + receipt.createdDateTime = new Date(); + receipt.createdUser = currentUser.userName; + receipt.createdUserDirectory = directory; + receipt.createdUserGuid = currentUser.userGUID; + receipt.updatedDateTime = new Date(); + receipt.updatedUser = currentUser.userName; + receipt.updatedUserDirectory = directory; + receipt.updatedUserGuid = currentUser.userGUID; + await queryRunner.manager.save(receipt); + } + + readTransactionDto = await this.classMapper.mapAsync( + createdTransaction, + Transaction, + ReadTransactionDto, + { + extraArgs: () => ({ + url: url, + }), + }, + ); + if (!nestedQueryRunner) { + await queryRunner.commitTransaction(); + } + } catch (err) { + if (!nestedQueryRunner) { + await queryRunner.rollbackTransaction(); + } + throw new InternalServerErrorException(); // Should handle the typeorm Error handling + } finally { + if (!nestedQueryRunner) { + await queryRunner.release(); + } } - // Return the updated transaction data (ReadTransactionDto). - return await this.classMapper.mapAsync( - await this.transactionRepository.findOne({ - where: { transactionId: existingTransaction.transactionId }, - }), - Transaction, - ReadTransactionDto, - ); + return readTransactionDto; } /** - * Creates new transactions and associates them with the provided permit IDs and payment details. - * TODO: Should be one transaction with many permits? - * @param permitIds - An array of permit IDs to associate with the new transactions. - * @param paymentDetails - The payment details to be added to each new transaction. + * Updates details returned by Payment Gateway in ORBC System. + * @param currentUser - The current user object of type {@link IUserJWT} + * @param updatePaymentGatewayTransactionDto - The UpdatePaymentGatewayTransactionDto object of type + * {@link UpdatePaymentGatewayTransactionDto} for updating the payment gateway details. + * @returns {ReadTransactionDto} The updated payment gateway of type {@link ReadPaymentGatewayTransactionDto}. */ - async createTransaction( - permitIds: number[], - paymentDetails: MotiPayDetailsDto, - ) { - const permitTransactions: Pick< - PermitTransaction, - 'permitId' | 'transactionId' - >[] = []; - - // Loop through each permit ID to create a new transaction and associate it with the permit. - for (const id of permitIds) { - // Create a new transaction record in the transaction table with the provided payment details. - await this.transactionRepository - .createQueryBuilder() - .insert() - .values({ - //permits: [permit], - transactionOrderNumber: paymentDetails.transactionOrderNumber, - transactionAmount: paymentDetails.transactionAmount, - transactionType: paymentDetails.transactionType, - transactionSubmitDate: paymentDetails.transactionSubmitDate, - paymentMethodId: paymentDetails.paymentMethodId, - }) - .execute(); - - // Retrieve the newly created transaction from the database based on the order number. - // NOTE: this could potentially cause a problem in the future if multiple transactions have same order number - // since we're using same paymentDetails.transactionOrderNumber for multiple permit ids - const transaction = await this.findOneTransaction( - paymentDetails.transactionOrderNumber, + async updateTransactions( + currentUser: IUserJWT, + transactionId: string, + updatePaymentGatewayTransactionDto: UpdatePaymentGatewayTransactionDto, + directory: Directory, + ): Promise { + let updatedTransaction: Transaction; + let updateResult: UpdateResult; + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + const transactionToUpdate = await queryRunner.manager.findOne( + Transaction, + { + where: { transactionId: transactionId }, + relations: ['permitTransactions', 'permitTransactions.permit'], + }, ); - // Prepare the data to associate the permit with the newly created transaction. - const permitTransaction = { - permitId: id, - transactionId: transaction.transactionId, - }; + if (!transactionToUpdate) { + throw new NotFoundException('TransactionId not found'); + } + + transactionToUpdate.permitTransactions.forEach((permitTransaction) => { + if ( + permitTransaction.permit.permitStatus != + ApplicationStatus.WAITING_PAYMENT + ) { + throw new BadRequestException( + `${permitTransaction.permit.permitId} not in valid status!`, + ); + } + }); + + const updateTransactionTemp = await this.classMapper.mapAsync( + updatePaymentGatewayTransactionDto, + UpdatePaymentGatewayTransactionDto, + Transaction, + { + extraArgs: () => ({ + userName: currentUser.userName, + userGUID: currentUser.userGUID, + timestamp: new Date(), + directory: directory, + }), + }, + ); - // Save the association of the permit with the transaction in the permitTransaction table. - await this.permitTransactionRepository.save(permitTransaction); + updateResult = await queryRunner.manager.update( + Transaction, + { transactionId: transactionId }, + updateTransactionTemp, + ); - permitTransactions.push(permitTransaction); + if (!updateResult?.affected) { + throw new InternalServerErrorException('Error updating transaction'); + } + + if (updateTransactionTemp.pgApproved === 1) { + for (const permitTransaction of transactionToUpdate.permitTransactions) { + updateResult = await queryRunner.manager.update( + Permit, + { permitId: permitTransaction.permit.permitId }, + { + permitStatus: ApplicationStatus.PAYMENT_COMPLETE, + updatedDateTime: new Date(), + updatedUser: currentUser.userName, + updatedUserGuid: currentUser.userGUID, + updatedUserDirectory: directory, + }, + ); + if (!updateResult?.affected) { + throw new InternalServerErrorException( + 'Error updating permit status', + ); + } + } + } + + updatedTransaction = await queryRunner.manager.findOne(Transaction, { + where: { transactionId: transactionId }, + relations: ['permitTransactions', 'permitTransactions.permit'], + }); + + if (updateTransactionTemp.pgApproved === 1) { + const receiptNumber = await this.generateReceiptNumber(); + const receipt = new Receipt(); + receipt.receiptNumber = receiptNumber; + receipt.transaction = updatedTransaction; + receipt.receiptNumber = receiptNumber; + receipt.createdDateTime = new Date(); + receipt.createdUser = currentUser.userName; + receipt.createdUserDirectory = directory; + receipt.createdUserGuid = currentUser.userGUID; + receipt.updatedDateTime = new Date(); + receipt.updatedUser = currentUser.userName; + receipt.updatedUserDirectory = directory; + receipt.updatedUserGuid = currentUser.userGUID; + await queryRunner.manager.save(receipt); + } + + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(); // Should handle the typeorm Error handling + } finally { + await queryRunner.release(); } - return permitTransactions; + const readTransactionDto = await this.classMapper.mapAsync( + updatedTransaction, + Transaction, + ReadPaymentGatewayTransactionDto, + ); + + return readTransactionDto; } - async findOneTransaction( - transactionOrderNumber: string, - ): Promise { + async findTransaction(transactionId: string): Promise { return this.classMapper.mapAsync( - await this.transactionRepository.findOne({ - where: { - transactionOrderNumber: transactionOrderNumber, - }, - }), + await this.findTransactionEntity(transactionId), Transaction, ReadTransactionDto, ); } - async findOnePermitTransaction( - transactionId: string, - ): Promise { - return this.classMapper.mapAsync( - await this.permitTransactionRepository.findOne({ - where: { - transactionId: transactionId, - }, - }), - PermitTransaction, - ReadPermitTransactionDto, - ); + async findTransactionEntity(transactionId: string): Promise { + return await this.transactionRepository.findOne({ + where: { transactionId: transactionId }, + relations: ['permitTransactions', 'permitTransactions.permit'], + }); } -} \ No newline at end of file +} diff --git a/backend/vehicles/src/modules/payment/profile/permit-transaction.profile.ts b/backend/vehicles/src/modules/payment/profile/permit-transaction.profile.ts deleted file mode 100644 index 0fc1a5abf..000000000 --- a/backend/vehicles/src/modules/payment/profile/permit-transaction.profile.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Mapper, createMap } from '@automapper/core'; -import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; -import { Injectable } from '@nestjs/common'; -import { PermitTransaction } from '../entities/permit-transaction.entity'; -import { ReadPermitTransactionDto } from '../dto/response/read-permit-transaction.dto'; -import { CreatePermitTransactionDto } from '../dto/request/create-permit-transaction.dto'; - -@Injectable() -export class PermitTransactionProfile extends AutomapperProfile { - constructor(@InjectMapper() mapper: Mapper) { - super(mapper); - } - - override get profile() { - return (mapper: Mapper) => { - createMap(mapper, PermitTransaction, ReadPermitTransactionDto); - createMap(mapper, CreatePermitTransactionDto, PermitTransaction); - }; - } -} diff --git a/backend/vehicles/src/modules/payment/profile/transaction.profile.ts b/backend/vehicles/src/modules/payment/profile/transaction.profile.ts index 80e639ba7..cf3824f86 100644 --- a/backend/vehicles/src/modules/payment/profile/transaction.profile.ts +++ b/backend/vehicles/src/modules/payment/profile/transaction.profile.ts @@ -1,10 +1,19 @@ -import { Mapper, createMap } from '@automapper/core'; +import { + Mapper, + createMap, + forMember, + mapFrom, + mapWithArguments, +} from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { Transaction } from '../entities/transaction.entity'; import { ReadTransactionDto } from '../dto/response/read-transaction.dto'; import { CreateTransactionDto } from '../dto/request/create-transaction.dto'; -import { ReadPermitTransactionDto } from '../dto/response/read-permit-transaction.dto'; +import { PermitTransaction } from '../entities/permit-transaction.entity'; +import { ReadApplicationTransactionDto } from '../dto/response/read-application-transaction.dto'; +import { UpdatePaymentGatewayTransactionDto } from '../dto/request/read-payment-gateway-transaction.dto'; +import { ReadPaymentGatewayTransactionDto } from '../dto/response/read-payment-gateway-transaction.dto'; @Injectable() export class TransactionProfile extends AutomapperProfile { @@ -14,9 +23,141 @@ export class TransactionProfile extends AutomapperProfile { override get profile() { return (mapper: Mapper) => { - createMap(mapper, Transaction, ReadTransactionDto); - createMap(mapper, CreateTransactionDto, Transaction); - createMap(mapper, Transaction, ReadPermitTransactionDto); + createMap( + mapper, + PermitTransaction, + ReadApplicationTransactionDto, + forMember( + (d) => d.applicationId, + mapFrom((s) => { + return s.permit?.permitId; + }), + ), + ); + + createMap( + mapper, + Transaction, + ReadTransactionDto, + forMember( + (d) => d.url, + mapWithArguments((source, { url }) => { + return url; + }), + ), + forMember( + (d) => d.applicationDetails, + mapFrom((src) => + this.mapper.mapArray( + src.permitTransactions, + PermitTransaction, + ReadApplicationTransactionDto, + ), + ), + ), + ); + + createMap( + mapper, + CreateTransactionDto, + Transaction, + forMember( + (d) => d.createdUserGuid, + mapWithArguments((source, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (d) => d.createdUser, + mapWithArguments((source, { userName }) => { + return userName; + }), + ), + forMember( + (d) => d.createdUserDirectory, + mapWithArguments((source, { directory }) => { + return directory; + }), + ), + + forMember( + (d) => d.createdDateTime, + mapWithArguments((source, { timestamp }) => { + return timestamp; + }), + ), + + forMember( + (d) => d.updatedUserGuid, + mapWithArguments((source, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (d) => d.updatedUser, + mapWithArguments((source, { userName }) => { + return userName; + }), + ), + forMember( + (d) => d.updatedUserDirectory, + mapWithArguments((source, { directory }) => { + return directory; + }), + ), + + forMember( + (d) => d.updatedDateTime, + mapWithArguments((source, { timestamp }) => { + return timestamp; + }), + ), + forMember( + (d) => d.transactionOrderNumber, + mapWithArguments((source, { transactionOrderNumber }) => { + return transactionOrderNumber; + }), + ), + forMember( + (d) => d.totalTransactionAmount, + mapWithArguments((source, { totalTransactionAmount }) => { + return totalTransactionAmount; + }), + ), + ); + + createMap( + mapper, + UpdatePaymentGatewayTransactionDto, + Transaction, + forMember( + (transaction) => transaction.updatedUserGuid, + mapWithArguments((source, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (transaction) => transaction.updatedUser, + mapWithArguments((source, { userName }) => { + return userName; + }), + ), + forMember( + (transaction) => transaction.updatedUserDirectory, + mapWithArguments((source, { directory }) => { + return directory; + }), + ), + + forMember( + (transaction) => transaction.updatedDateTime, + mapWithArguments((source, { timestamp }) => { + return timestamp; + }), + ), + ); + + createMap(mapper, Transaction, ReadPaymentGatewayTransactionDto); }; } -} \ No newline at end of file +} diff --git a/backend/vehicles/src/modules/permit/application.controller.ts b/backend/vehicles/src/modules/permit/application.controller.ts index ae3d27fe2..418fd89e0 100644 --- a/backend/vehicles/src/modules/permit/application.controller.ts +++ b/backend/vehicles/src/modules/permit/application.controller.ts @@ -32,6 +32,8 @@ import { UpdateApplicationStatusDto } from './dto/request/update-application-sta import { ResultDto } from './dto/response/result.dto'; import { Roles } from 'src/common/decorator/roles.decorator'; import { Role } from 'src/common/enum/roles.enum'; +import { IssuePermitDto } from './dto/request/issue-permit.dto'; +import { getDirectory } from 'src/common/helper/auth.helper'; @ApiBearerAuth() @ApiTags('Permit Application') @@ -66,7 +68,12 @@ export class ApplicationController { @Body() createApplication: CreateApplicationDto, ): Promise { const currentUser = request.user as IUserJWT; - return await this.applicationService.create(createApplication, currentUser); + const directory = getDirectory(currentUser); + return await this.applicationService.create( + createApplication, + currentUser, + directory, + ); } /** @@ -138,9 +145,13 @@ export class ApplicationController { @Param('applicationNumber') applicationNumber: string, @Body() updateApplicationDto: UpdateApplicationDto, ): Promise { + const currentUser = request.user as IUserJWT; + const directory = getDirectory(currentUser); const application = await this.applicationService.update( applicationNumber, updateApplicationDto, + currentUser, + directory, ); if (!application) { @@ -166,14 +177,44 @@ export class ApplicationController { @Body() updateApplicationStatusDto: UpdateApplicationStatusDto, ): Promise { const currentUser = request.user as IUserJWT; // TODO: consider security with passing JWT token to DMS microservice + const directory = getDirectory(currentUser); const result = await this.applicationService.updateApplicationStatus( updateApplicationStatusDto.applicationIds, updateApplicationStatusDto.applicationStatus, currentUser, + directory, ); if (!result) { throw new DataNotFoundException(); } return result; } -} \ No newline at end of file + + /** + * A POST method defined with the @Post() decorator and a route of /:applicationId/issue + * that issues a ermit for given @param applicationId.. + * @param request + * @param issuePermitDto + * @returns The id of new voided/revoked permit a in response object {@link ResultDto} + * + */ + @Post('/issue') + async issuePermit( + @Req() request: Request, + @Body() issuePermitDto: IssuePermitDto, + ): Promise { + const currentUser = request.user as IUserJWT; + const directory = getDirectory(currentUser); + /**Bulk issuance would require changes in issuePermit service method with + * respect to Document generation etc. At the moment, it is not handled and + * only single permit Id must be passed. + * + */ + const result = await this.applicationService.issuePermit( + currentUser, + issuePermitDto.applicationIds[0], + directory, + ); + return result; + } +} diff --git a/backend/vehicles/src/modules/permit/application.service.ts b/backend/vehicles/src/modules/permit/application.service.ts index 416302e5c..e64c5f7a7 100644 --- a/backend/vehicles/src/modules/permit/application.service.ts +++ b/backend/vehicles/src/modules/permit/application.service.ts @@ -1,11 +1,13 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { + BadRequestException, ForbiddenException, HttpException, Inject, Injectable, InternalServerErrorException, + NotFoundException, forwardRef, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; @@ -40,12 +42,12 @@ import { CacheKey } from '../../common/enum/cache-key.enum'; import { DopsService } from '../common/dops.service'; import { DopsGeneratedDocument } from '../../common/interface/dops-generated-document.interface'; import { TemplateName } from '../../common/enum/template-name.enum'; -import { IReceipt } from 'src/common/interface/receipt.interface'; import { IFile } from '../../common/interface/file.interface'; import { ReadTransactionDto } from '../payment/dto/response/read-transaction.dto'; import { Transaction } from '../payment/entities/transaction.entity'; import { Receipt } from '../payment/entities/receipt.entity'; import { convertUtcToPt } from '../../common/helper/date-time.helper'; +import { Directory } from 'src/common/enum/directory.enum'; @Injectable() export class ApplicationService { @@ -80,6 +82,7 @@ export class ApplicationService { async create( createApplicationDto: CreateApplicationDto, currentUser: IUserJWT, + directory: Directory, ): Promise { const id = createApplicationDto.permitId; //If permit id exists assign it to null to create new application. @@ -121,6 +124,14 @@ export class ApplicationService { createApplicationDto, CreateApplicationDto, Permit, + { + extraArgs: () => ({ + userName: currentUser.userName, + userGUID: currentUser.userGUID, + timestamp: new Date(), + directory: directory, + }), + }, ); const savedPermitEntity = await this.permitRepository.save( permitApplication, @@ -153,6 +164,22 @@ export class ApplicationService { }); } + private async findOneWithSuccessfulTransaction( + applicationId: string, + ): Promise { + return await this.permitRepository + .createQueryBuilder('permit') + .innerJoinAndSelect('permit.permitData', 'permitData') + .innerJoinAndSelect('permit.permitTransactions', 'permitTransactions') + .innerJoinAndSelect('permitTransactions.transaction', 'transaction') + .innerJoinAndSelect('transaction.receipt', 'receipt') + .where('permit.permitId = :permitId', { + permitId: applicationId, + }) + .andWhere('receipt.receiptNumber IS NOT NULL') + .getOne(); + } + /* Get single application By Permit ID*/ async findApplication(permitId: string): Promise { const application = await this.findOne(permitId); @@ -241,6 +268,8 @@ export class ApplicationService { async update( applicationNumber: string, updateApplicationDto: UpdateApplicationDto, + currentUser: IUserJWT, + directory: Directory, ): Promise { const existingApplication = await this.findByApplicationNumber( applicationNumber, @@ -254,14 +283,15 @@ export class ApplicationService { extraArgs: () => ({ permitId: existingApplication.permitId, permitDataId: existingApplication.permitData.permitDataId, + userName: currentUser.userName, + userGUID: currentUser.userGUID, + timestamp: new Date(), + directory: directory, }), }, ); - const applicationData: Permit = { - ...newApplication, - updatedDateTime: new Date(), - }; + const applicationData: Permit = newApplication; await this.permitRepository.save(applicationData); return this.classMapper.mapAsync( await this.findByApplicationNumber(applicationNumber), @@ -283,6 +313,7 @@ export class ApplicationService { applicationIds: string[], applicationStatus: ApplicationStatus, currentUser: IUserJWT, + directory: Directory, ): Promise { let permitApprovalSource: PermitApprovalSourceEnum = null; if (applicationIds.length === 1) { @@ -315,6 +346,10 @@ export class ApplicationService { ...(permitApprovalSource && { permitApprovalSource: permitApprovalSource, }), + updatedUser: currentUser.userName, + updatedDateTime: new Date(), + updatedUserDirectory: directory, + updatedUserGuid: currentUser.userGUID, }) .whereInIds(applicationIds) .returning(['permitId']) @@ -340,39 +375,53 @@ export class ApplicationService { /** * This function is responsible for issuing a permit based on a given application. * It performs various operations, including generating a permit number, calling the PDF generation service, and updating the permit record in the database. - * @param currentUser // TODO: protect endpoint + * @param currentUser * @param applicationId applicationId to identify the application to be issued. It is the same as permitId. * @returns a resultDto that describes if the transaction was successful or if it failed */ async issuePermit( currentUser: IUserJWT, applicationId: string, - transactionDetails?: IReceipt, + directory: Directory, ) { let success = ''; let failure = ''; - const tempPermit = await this.findOne(applicationId); + const fetchedApplication = await this.findOneWithSuccessfulTransaction( + applicationId, + ); // Check if a PDF document already exists for the permit. // It's important that a PDF does not get overwritten. // Once its created, it is a permanent legal document. - if ( - tempPermit.documentId || - tempPermit.permitStatus === ApplicationStatus.ISSUED - ) { + if (!fetchedApplication) { + throw new NotFoundException('Application not found for issuance!'); + } + if (fetchedApplication.documentId) { throw new HttpException('Document already exists', 409); + } else if ( + fetchedApplication.permitStatus != ApplicationStatus.PAYMENT_COMPLETE + ) { + throw new BadRequestException( + 'Application must be ready for issuance with payment complete status!', + ); } - const permitNumber = await this.generatePermitNumber(applicationId); - tempPermit.permitNumber = permitNumber; - tempPermit.permitStatus = ApplicationStatus.ISSUED; + const permitNumber = await this.generatePermitNumber(applicationId, null); + //Generate receipt number for the permit to be created in database. + const receiptNumber = + fetchedApplication.permitTransactions[0].transaction.receipt + .receiptNumber; + fetchedApplication.permitNumber = permitNumber; + fetchedApplication.permitStatus = ApplicationStatus.ISSUED; - const companyInfo = await this.companyService.findOne(tempPermit.companyId); + const companyInfo = await this.companyService.findOne( + fetchedApplication.companyId, + ); - const fullNames = await this.getFullNamesFromCache(tempPermit); + const fullNames = await this.getFullNamesFromCache(fetchedApplication); // Provide the permit json data required to populate the .docx template that is used to generate a PDF const permitDataForTemplate = formatTemplateData( - tempPermit, + fetchedApplication, fullNames, companyInfo, ); @@ -398,19 +447,27 @@ export class ApplicationService { companyInfo.companyId, ); - //Generate receipt number for the permit to be created in database. - const receiptNo = await this.generateReceiptNumber(); dopsRequestData = { templateName: TemplateName.PAYMENT_RECEIPT, - generatedDocumentFileName: `Receipt_No_${receiptNo}`, + generatedDocumentFileName: `Receipt_No_${receiptNumber}`, templateData: { ...permitDataForTemplate, - ...transactionDetails, + // transaction details still needs to be reworked to support multiple permits + transactionOrderNumber: + fetchedApplication.permitTransactions[0].transaction + .transactionOrderNumber, + transactionAmount: + fetchedApplication.permitTransactions[0].transaction + .totalTransactionAmount, + paymentMethod: + fetchedApplication.permitTransactions[0].transaction + .pgPaymentMethod, transactionDate: convertUtcToPt( - transactionDetails.transactionDate, + fetchedApplication.permitTransactions[0].transaction + .transactionSubmitDate, 'MMM. D, YYYY, hh:mm a Z', ), - receiptNo, + receiptNo: receiptNumber, }, }; @@ -425,34 +482,54 @@ export class ApplicationService { generatedReceiptDocumentPromise, ]); - const permitEntity = await this.permitRepository.findOne({ - where: [{ applicationNumber: tempPermit.applicationNumber }], - relations: { - permitData: true, + await queryRunner.manager.update( + Permit, + { permitId: fetchedApplication.permitId }, + { + permitStatus: fetchedApplication.permitStatus, + permitNumber: fetchedApplication.permitNumber, + documentId: generatedDocuments.at(0).dmsId, + updatedDateTime: new Date(), + updatedUser: currentUser.userName, + updatedUserDirectory: directory, + updatedUserGuid: currentUser.userGUID, + }, + ); + + await queryRunner.manager.update( + Receipt, + { + receiptId: + fetchedApplication.permitTransactions[0].transaction.receipt + .receiptId, + }, + { + receiptDocumentId: generatedDocuments.at(1).dmsId, + updatedDateTime: new Date(), + updatedUser: currentUser.userName, + updatedUserDirectory: directory, + updatedUserGuid: currentUser.userGUID, }, - }); - permitEntity.permitStatus = ApplicationStatus.ISSUED; - permitEntity.permitNumber = permitNumber; - permitEntity.documentId = generatedDocuments.at(0).dmsId; - await queryRunner.manager.save(permitEntity); - const receiptEntity: Receipt = new Receipt(); - const transaction = await this.findOneTransactionByOrderNumber( - transactionDetails.transactionOrderNumber, ); - receiptEntity.transactionId = transaction.transactionId; - receiptEntity.receiptNumber = receiptNo; - receiptEntity.receiptDocumentId = generatedDocuments.at(1).dmsId; - await queryRunner.manager.save(receiptEntity); + // In case of amendment move the parent permit to SUPERSEDED Status. if ( - tempPermit.previousRevision != null || - tempPermit.previousRevision != undefined + fetchedApplication.previousRevision != null || + fetchedApplication.previousRevision != undefined ) { - const parentPermit = await this.findOne( - String(tempPermit.previousRevision), + await queryRunner.manager.update( + Permit, + { + permitId: fetchedApplication.previousRevision, + }, + { + permitStatus: ApplicationStatus.SUPERSEDED, + updatedDateTime: new Date(), + updatedUser: currentUser.userName, + updatedUserDirectory: directory, + updatedUserGuid: currentUser.userGUID, + }, ); - parentPermit.permitStatus = ApplicationStatus.SUPERSEDED; - await queryRunner.manager.save(parentPermit); } await queryRunner.commitTransaction(); success = applicationId; @@ -463,13 +540,13 @@ export class ApplicationService { const attachments: AttachementEmailData[] = [ { - filename: tempPermit.permitNumber + '.pdf', + filename: fetchedApplication.permitNumber + '.pdf', contentType: 'application/pdf', encoding: 'base64', content: generatedDocuments.at(0).buffer.toString('base64'), }, { - filename: `Receipt_No_${receiptNo}.pdf`, + filename: `Receipt_No_${receiptNumber}.pdf`, contentType: 'application/pdf', encoding: 'base64', content: generatedDocuments.at(1).buffer.toString('base64'), @@ -654,24 +731,27 @@ export class ApplicationService { } /** - * Generate permit number for a permit application. + * Generate permit number for a permit application. only one (i.e. permitId or oldPermitId) should be present at a time. * @param permitId + * @param oldPermitId * @returns permitNumber */ - async generatePermitNumber(permitId: string): Promise { - const permit = await this.findOne(permitId); - let approvalSourceId: number; - let rnd; + async generatePermitNumber( + permitId: string, + oldPermitId: string, + ): Promise { + const id = !permitId ? oldPermitId : permitId; + const permit = await this.findOne(id); let seq: string; - const approvalSource = await this.permitApprovalSourceRepository.find({ + const approvalSource = await this.permitApprovalSourceRepository.findOne({ where: { id: permit.permitApprovalSource }, }); - if (!approvalSourceId) { - approvalSourceId = 9; - } else { - approvalSourceId = approvalSource[0].code; - } - if (permit.revision == 0) { + let approvalSourceId: number; + if (approvalSource.code != undefined || approvalSource.code != null) + approvalSourceId = approvalSource.code; + else approvalSourceId = 9; + let rnd: number | string; + if (permitId) { seq = await callDatabaseSequence( 'permit.ORBC_PERMIT_NUMBER_SEQ', this.dataSource, @@ -680,8 +760,8 @@ export class ApplicationService { const { randomInt } = await import('crypto'); rnd = randomInt(100, 1000); } else { - seq = permit.applicationNumber.substring(3, 15); - rnd = 'A' + String(permit.revision).padStart(2, '0'); + seq = permit.permitNumber.substring(3, 15); + rnd = 'A' + String(permit.revision + 1).padStart(2, '0'); } const permitNumber = 'P' + String(approvalSourceId) + '-' + String(seq) + '-' + String(rnd); @@ -702,25 +782,6 @@ export class ApplicationService { ); } - /** - * Generate Receipt Number - */ - async generateReceiptNumber(): Promise { - const currentDate = new Date(); - const year = currentDate.getFullYear(); - const month = String(currentDate.getMonth() + 1).padStart(2, '0'); - const day = String(currentDate.getDate()).padStart(2, '0'); - const dateString = `${year}${month}${day}`; - const source = dateString; - const seq = await callDatabaseSequence( - 'permit.ORBC_RECEIPT_NUMBER_SEQ', - this.dataSource, - ); - const receiptNumber = String(String(source) + '-' + String(seq)); - - return receiptNumber; - } - async checkApplicationInProgress(originalPermitId: string): Promise { const count = await this.permitRepository .createQueryBuilder('permit') @@ -740,4 +801,4 @@ export class ApplicationService { .getCount(); return count; } -} \ No newline at end of file +} diff --git a/backend/vehicles/src/modules/permit/dto/request/issue-permit.dto.ts b/backend/vehicles/src/modules/permit/dto/request/issue-permit.dto.ts index bd1c7bba7..da08de0f5 100644 --- a/backend/vehicles/src/modules/permit/dto/request/issue-permit.dto.ts +++ b/backend/vehicles/src/modules/permit/dto/request/issue-permit.dto.ts @@ -1,10 +1,33 @@ +import { AutoMap } from '@automapper/classes'; import { ApiProperty } from '@nestjs/swagger'; +import { + IsNumberString, + IsNumber, + IsOptional, + ArrayMinSize, +} from 'class-validator'; export class IssuePermitDto { + @AutoMap() @ApiProperty({ - description: 'Id of the application.', - example: 1, + description: + 'Application Ids. Note: Bulk issuance is not yet implemented even though we capture multiple Application Ids', + isArray: true, + type: String, + example: ['1'], + }) + @IsNumberString({}, { each: true }) + @ArrayMinSize(1) + applicationIds: string[]; + + @AutoMap() + @IsNumber() + @ApiProperty({ + description: 'Id of the company requesting the permit.', + example: 74, required: false, }) - applicationId: string; + @IsOptional() + @IsNumber() + companyId: number; } diff --git a/backend/vehicles/src/modules/permit/dto/request/void-permit.dto.ts b/backend/vehicles/src/modules/permit/dto/request/void-permit.dto.ts index ef36555da..8bd3ce994 100644 --- a/backend/vehicles/src/modules/permit/dto/request/void-permit.dto.ts +++ b/backend/vehicles/src/modules/permit/dto/request/void-permit.dto.ts @@ -1,11 +1,21 @@ import { AutoMap } from '@automapper/classes'; import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber } from 'class-validator'; +import { + IsEnum, + IsNumber, + IsOptional, + IsString, + Length, + Min, + MinLength, +} from 'class-validator'; import { ApplicationStatus } from 'src/common/enum/application-status.enum'; +import { PaymentMethodType } from '../../../../common/enum/payment-method-type.enum'; export class VoidPermitDto { @AutoMap() @ApiProperty({ + enum: ApplicationStatus, description: 'Revoke or void status for permit.', example: ApplicationStatus.REVOKED, required: false, @@ -15,33 +25,68 @@ export class VoidPermitDto { @AutoMap() @ApiProperty({ description: 'Permit Transaction ID.', - example: 'T000000A0W', + example: '10000148', required: false, }) - transactionOrderNumber: string; + @IsOptional() + pgTransactionId: string; @AutoMap() @ApiProperty({ - description: 'Permit Transaction Date.', - example: '2023-07-10T15:49:36.582Z', - required: false, + enum: PaymentMethodType, + example: PaymentMethodType.WEB, + description: 'The identifier of the user selected payment method.', }) - transactionDate: Date; + @IsEnum(PaymentMethodType) + paymentMethodId: PaymentMethodType; @AutoMap() - @IsNumber() @ApiProperty({ - description: 'Permit Transaction Amount.', + description: 'Payment Transaction Amount.', example: 30, - required: false, }) + @IsNumber() + @Min(0) transactionAmount: number; @AutoMap() @ApiProperty({ - description: 'Permit Transaction Method.', + description: 'Payment Transaction Date.', + example: '2023-07-10T15:49:36.582Z', + required: false, + }) + @IsOptional() + @IsString() + 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({ + example: 'VI', + description: 'Represents the card type used for the transaction.', required: false, }) - paymentMethod: string; + @IsOptional() + @IsString() + @Length(1, 2) + pgCardType: string; + + @AutoMap() + @ApiProperty({ + example: 'This permit was voided because of so-and-so reason', + description: 'Comment/Reason for voiding or revoking a permit.', + }) + @IsString() + @MinLength(1) + comment: string; } diff --git a/backend/vehicles/src/modules/permit/dto/response/permit-history.dto.ts b/backend/vehicles/src/modules/permit/dto/response/permit-history.dto.ts index 60e2f9785..42c44de72 100644 --- a/backend/vehicles/src/modules/permit/dto/response/permit-history.dto.ts +++ b/backend/vehicles/src/modules/permit/dto/response/permit-history.dto.ts @@ -1,5 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { ApiProperty } from '@nestjs/swagger'; +import { TransactionType } from '../../../../common/enum/transaction-type.enum'; export class PermitHistoryDto { @AutoMap() @@ -9,12 +10,22 @@ export class PermitHistoryDto { 'Unique formatted permit number, recorded once the permit is approved and issued.', }) permitNumber: string; + @AutoMap() @ApiProperty({ - example: '', - description: '', + example: 'This permit was amended because of so-and-so reason.', + description: + 'Any comment/reason that was made for modification of the permit', }) comment: string; + + @AutoMap() + @ApiProperty({ + example: 'This permit was amended because of so-and-so reason.', + description: 'The username of user that amended/voided the permit', + }) + commentUsername: string; + @AutoMap() @ApiProperty({ example: '30.00', @@ -35,12 +46,28 @@ export class PermitHistoryDto { description: 'Bambora-assigned eight-digit unique id number used to identify an individual transaction.', }) - providerTransactionId: number; + pgTransactionId: string; @AutoMap() @ApiProperty({ example: 'CC', - description: 'Represents the payment method of a transaction.', + description: 'Represents the payment method of a transaction from Bambora.', + }) + pgPaymentMethod: string; + + @AutoMap() + @ApiProperty({ + example: 'VI', + description: 'Represents the card type used by a transaction.', + }) + pgCardType: string; + + @AutoMap() + @ApiProperty({ + enum: TransactionType, + example: TransactionType.PURCHASE, + description: + 'Represents the original value sent to indicate the type of transaction to perform.', }) - paymentMethod: string; + transactionTypeId: TransactionType; } diff --git a/backend/vehicles/src/modules/permit/dto/response/read-application.dto.ts b/backend/vehicles/src/modules/permit/dto/response/read-application.dto.ts index 2caacc6d1..37bae0d57 100644 --- a/backend/vehicles/src/modules/permit/dto/response/read-application.dto.ts +++ b/backend/vehicles/src/modules/permit/dto/response/read-application.dto.ts @@ -14,6 +14,34 @@ export class ReadApplicationDto { }) permitId: string; + @AutoMap() + @ApiProperty({ + example: '1', + description: 'Id of the original permit for a revision', + }) + originalPermitId: string; + + @AutoMap() + @ApiProperty({ + example: '1', + description: 'Revision number for a permit.', + }) + revision: number; + + @AutoMap() + @ApiProperty({ + example: '1', + description: 'Previous permit id for a revised permit.', + }) + previousRevision: number; + + @AutoMap() + @ApiProperty({ + example: 'This permit was amended because of so-and-so reason', + description: 'Comment/Reason for modifying a permit.', + }) + comment: string; + @AutoMap() @ApiProperty({ description: 'Satus of Permit/Permit Application', diff --git a/backend/vehicles/src/modules/permit/dto/response/read-permit.dto.ts b/backend/vehicles/src/modules/permit/dto/response/read-permit.dto.ts index 9d6e16240..b88ae96e6 100644 --- a/backend/vehicles/src/modules/permit/dto/response/read-permit.dto.ts +++ b/backend/vehicles/src/modules/permit/dto/response/read-permit.dto.ts @@ -22,6 +22,34 @@ export class ReadPermitDto { }) permitId: number; + @AutoMap() + @ApiProperty({ + example: '1', + description: 'Id of the original permit for a revision', + }) + originalPermitId: string; + + @AutoMap() + @ApiProperty({ + example: '1', + description: 'Revision number for a permit.', + }) + revision: number; + + @AutoMap() + @ApiProperty({ + example: '1', + description: 'Previous permit id for a revised permit.', + }) + previousRevision: number; + + @AutoMap() + @ApiProperty({ + example: 'This permit was amended because of so-and-so reason', + description: 'Comment/Reason for modifying a permit.', + }) + comment: string; + @AutoMap() @ApiProperty({ enum: PermitType, diff --git a/backend/vehicles/src/modules/permit/entities/permit.entity.ts b/backend/vehicles/src/modules/permit/entities/permit.entity.ts index e4ddd92d9..a6432fb5b 100644 --- a/backend/vehicles/src/modules/permit/entities/permit.entity.ts +++ b/backend/vehicles/src/modules/permit/entities/permit.entity.ts @@ -4,7 +4,7 @@ import { Column, PrimaryGeneratedColumn, OneToOne, - ManyToMany, + OneToMany, } from 'typeorm'; import { AutoMap } from '@automapper/classes'; import { Base } from '../../common/entities/base.entity'; @@ -13,7 +13,7 @@ import { PermitData } from './permit-data.entity'; import { PermitApplicationOrigin } from '../../../common/enum/permit-application-origin.enum'; import { PermitApprovalSource } from '../../../common/enum/permit-approval-source.enum'; import { ApplicationStatus } from 'src/common/enum/application-status.enum'; -import { Transaction } from 'src/modules/payment/entities/transaction.entity'; +import { PermitTransaction } from '../../payment/entities/permit-transaction.entity'; @Entity({ name: 'permit.ORBC_PERMIT' }) export class Permit extends Base { @@ -178,8 +178,8 @@ export class Permit extends Base { @AutoMap() @ApiProperty({ - example: 'A2-00000002-120', - description: 'Unique formatted permit application number.', + example: 'This permit was amended because of so-and-so reason', + description: 'Comment/Reason for modifying a permit.', }) @Column({ length: '3000', @@ -188,6 +188,9 @@ export class Permit extends Base { }) comment: string; - @ManyToMany(() => Transaction, (transaction) => transaction.permits) - transactions?: Transaction[]; + @OneToMany( + () => PermitTransaction, + (permitTransaction) => permitTransaction.permit, + ) + public permitTransactions: PermitTransaction[]; } diff --git a/backend/vehicles/src/modules/permit/permit.controller.ts b/backend/vehicles/src/modules/permit/permit.controller.ts index 61e944613..a05ec86b7 100644 --- a/backend/vehicles/src/modules/permit/permit.controller.ts +++ b/backend/vehicles/src/modules/permit/permit.controller.ts @@ -39,10 +39,10 @@ import { } from 'src/common/interface/pagination.interface'; import { PaginationDto } from 'src/common/class/pagination'; import { LessThenPipe } from 'src/common/class/customs.transform'; -import { Permit } from './entities/permit.entity'; import { PermitHistoryDto } from './dto/response/permit-history.dto'; import { ResultDto } from './dto/response/result.dto'; import { VoidPermitDto } from './dto/request/void-permit.dto'; +import { getDirectory } from 'src/common/helper/auth.helper'; @ApiBearerAuth() @ApiTags('Permit') @@ -72,7 +72,9 @@ export class PermitController { @Req() request: Request, @Body() createPermitDto: CreatePermitDto, ): Promise { - return this.permitService.create(createPermitDto); + const currentUser = request.user as IUserJWT; + const directory = getDirectory(currentUser); + return this.permitService.create(createPermitDto, currentUser, directory); } @ApiOkResponse({ @@ -90,7 +92,7 @@ export class PermitController { @ApiOkResponse({ description: 'The Permit Resource to get revision and payment history.', - type: Permit, + type: PermitHistoryDto, isArray: true, }) @Public() @@ -229,6 +231,19 @@ export class PermitController { res.status(200); } + @AuthOnly() + @ApiOkResponse({ + description: 'The Permit Resource', + type: ReadPermitDto, + isArray: true, + }) + @Get('/:permitId') + async getByPermitId( + @Param('permitId') permitId: string, + ): Promise { + return this.permitService.findByPermitId(permitId); + } + /** * A POST method defined with the @Post() decorator and a route of /:permitId/void * that Voids or revokes a permit for given @param permitId by changing it's status to VOIDED|REVOKED. @@ -245,13 +260,14 @@ export class PermitController { @Body() voidPermitDto: VoidPermitDto, ): Promise { - console.log(voidPermitDto); const currentUser = request.user as IUserJWT; + const directory = getDirectory(currentUser); const permit = await this.permitService.voidPermit( permitId, voidPermitDto, currentUser, + directory, ); return permit; } -} \ No newline at end of file +} diff --git a/backend/vehicles/src/modules/permit/permit.module.ts b/backend/vehicles/src/modules/permit/permit.module.ts index 09cf60d94..9b46ac22b 100644 --- a/backend/vehicles/src/modules/permit/permit.module.ts +++ b/backend/vehicles/src/modules/permit/permit.module.ts @@ -15,6 +15,7 @@ import { PermitApprovalSource } from './entities/permit-approval-source.entity'; import { CompanyModule } from '../company-user-management/company/company.module'; import { Receipt } from '../payment/entities/receipt.entity'; import { Transaction } from '../payment/entities/transaction.entity'; +import { PaymentModule } from '../payment/payment.module'; @Module({ imports: [ @@ -28,8 +29,9 @@ import { Transaction } from '../payment/entities/transaction.entity'; Receipt, ]), CompanyModule, + PaymentModule, ], - controllers: [PermitController, ApplicationController], + controllers: [ApplicationController, PermitController], providers: [ PermitService, ApplicationService, diff --git a/backend/vehicles/src/modules/permit/permit.service.ts b/backend/vehicles/src/modules/permit/permit.service.ts index 1956cc0d8..62e947308 100644 --- a/backend/vehicles/src/modules/permit/permit.service.ts +++ b/backend/vehicles/src/modules/permit/permit.service.ts @@ -29,7 +29,6 @@ import { paginate } from 'src/common/helper/paginate'; import { PermitHistoryDto } from './dto/response/permit-history.dto'; import { ApplicationStatus } from 'src/common/enum/application-status.enum'; import { ApplicationService } from './application.service'; -import { IReceipt } from 'src/common/interface/receipt.interface'; import { formatTemplateData } from './helpers/formatTemplateData.helper'; import { CompanyService } from '../company-user-management/company/company.service'; import { DopsGeneratedDocument } from 'src/common/interface/dops-generated-document.interface'; @@ -42,6 +41,13 @@ import { EmailService } from '../email/email.service'; import { EmailTemplate } from 'src/common/enum/email-template.enum'; import { ResultDto } from './dto/response/result.dto'; import { VoidPermitDto } from './dto/request/void-permit.dto'; +import { PaymentService } from '../payment/payment.service'; +import { CreateTransactionDto } from '../payment/dto/request/create-transaction.dto'; +import { TransactionType } from '../../common/enum/transaction-type.enum'; +import { Transaction } from '../payment/entities/transaction.entity'; +import { Directory } from 'src/common/enum/directory.enum'; +import { PermitData } from './entities/permit-data.entity'; +import { Base } from '../common/entities/base.entity'; @Injectable() export class PermitService { @@ -51,21 +57,32 @@ export class PermitService { private permitRepository: Repository, @InjectRepository(PermitType) private permitTypeRepository: Repository, - @InjectRepository(Receipt) - private receiptRepository: Repository, private dataSource: DataSource, private readonly dopsService: DopsService, private companyService: CompanyService, private readonly emailService: EmailService, @Inject(forwardRef(() => ApplicationService)) private readonly applicationService: ApplicationService, + private paymentService: PaymentService, ) {} - async create(createPermitDto: CreatePermitDto): Promise { + async create( + createPermitDto: CreatePermitDto, + currentUser: IUserJWT, + directory: Directory, + ): Promise { const permitEntity = await this.classMapper.mapAsync( createPermitDto, CreatePermitDto, Permit, + { + extraArgs: () => ({ + userName: currentUser.userName, + directory: directory, + userGUID: currentUser.userGUID, + timestamp: new Date(), + }), + }, ); const savedPermitEntity = await this.permitRepository.save(permitEntity); @@ -92,13 +109,14 @@ export class PermitService { }); } - private async findOneWithTransactions(permitId: string): Promise { - return this.permitRepository.findOne({ - where: { permitId: permitId }, - relations: { - transactions: true, - }, - }); + /** + * Find single permit with associated data by permit id. + * @param permitId permit id + * @returns permit with data + */ + public async findByPermitId(permitId: string): Promise { + const permit = await this.findOne(permitId); + return this.classMapper.mapAsync(permit, Permit, ReadPermitDto); } /** @@ -256,38 +274,6 @@ export class PermitService { return readPermitDtoItems; } - async findReceipt(permit: Permit): Promise { - if (!permit.transactions || permit.transactions.length === 0) { - throw new Error('No transactions associated with this permit'); - } - - // Find the latest transaction for the permit, but not necessarily an approved transaction - let latestTransaction = permit.transactions[0]; - let latestSubmitDate = latestTransaction.transactionSubmitDate; - permit.transactions.forEach((transaction) => { - if ( - new Date(transaction.transactionSubmitDate) >= - new Date(latestSubmitDate) - ) { - latestSubmitDate = transaction.transactionSubmitDate; - latestTransaction = transaction; - } - }); - - const receipt = await this.receiptRepository.findOne({ - where: { - transactionId: latestTransaction.transactionId, - }, - }); - - if (!receipt) { - throw new Error( - "No receipt generated for this permit's latest transaction", - ); - } - return receipt; - } - /** * Finds a receipt PDF document associated with a specific permit ID. * @param currentUser - The current User Details. @@ -298,19 +284,29 @@ export class PermitService { currentUser: IUserJWT, permitId: string, res?: Response, - ): Promise { - const permit = await this.findOneWithTransactions(permitId); - const receipt = await this.findReceipt(permit); + ): Promise { + const permit = await this.permitRepository + .createQueryBuilder('permit') + .innerJoinAndSelect('permit.permitTransactions', 'permitTransactions') + .innerJoinAndSelect('permitTransactions.transaction', 'transaction') + .innerJoinAndSelect('transaction.receipt', 'receipt') + .where('permit.permitId = :permitId', { + permitId: permitId, + }) + .andWhere('receipt.receiptNumber IS NOT NULL') + .getOne(); + + if (!permit) { + throw new NotFoundException('Receipt Not Found!'); + } - const file: ReadFileDto = null; await this.dopsService.download( currentUser, - receipt.receiptDocumentId, + permit.permitTransactions[0].transaction.receipt.receiptDocumentId, FileDownloadModes.PROXY, res, permit.companyId, ); - return file; } public async findPermitHistory( @@ -318,13 +314,29 @@ export class PermitService { ): Promise { const permits = await this.permitRepository .createQueryBuilder('permit') - .innerJoinAndSelect('permit.transactions', 'transaction') + .innerJoinAndSelect('permit.permitTransactions', 'permitTransactions') + .innerJoinAndSelect('permitTransactions.transaction', 'transaction') .where('permit.permitNumber IS NOT NULL') .andWhere('permit.originalPermitId = :originalPermitId', { originalPermitId: originalPermitId, }) + .orderBy('transaction.transactionSubmitDate', 'DESC') .getMany(); - return this.classMapper.mapArrayAsync(permits, Permit, PermitHistoryDto); + + return permits.flatMap((permit) => + permit.permitTransactions.map((permitTransaction) => ({ + permitNumber: permit.permitNumber, + comment: permit.comment, + transactionOrderNumber: + permitTransaction.transaction.transactionOrderNumber, + transactionAmount: permitTransaction.transactionAmount, + transactionTypeId: permitTransaction.transaction.transactionTypeId, + pgPaymentMethod: permitTransaction.transaction.pgPaymentMethod, + pgTransactionId: permitTransaction.transaction.pgTransactionId, + pgCardType: permitTransaction.transaction.pgCardType, + commentUsername: permit.createdUser, + })), + ) as PermitHistoryDto[]; } /** @@ -337,13 +349,8 @@ export class PermitService { permitId: string, voidPermitDto: VoidPermitDto, currentUser: IUserJWT, + directory: Directory, ): Promise { - const transactionDetails: IReceipt = { - transactionAmount: voidPermitDto.transactionAmount, - transactionDate: voidPermitDto.transactionDate, - transactionOrderNumber: voidPermitDto.transactionOrderNumber, - paymentMethod: voidPermitDto.paymentMethod, - }; const permit = await this.findOne(permitId); /** * If permit not found raise error. @@ -374,25 +381,112 @@ export class PermitService { currentUser.identity_provider, permitId, ); + const permitNumber = await this.applicationService.generatePermitNumber( + null, + permitId, + ); let success = ''; let failure = ''; - const companyInfo = await this.companyService.findOne(permit.companyId); - - const fullNames = await this.applicationService.getFullNamesFromCache( - permit, - ); - const permitDataForTemplate = formatTemplateData( - permit, - fullNames, - companyInfo, - ); const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { + const userMetadata: Base = { + createdDateTime: new Date(), + createdUser: currentUser.userName, + createdUserDirectory: directory, + createdUserGuid: currentUser.userGUID, + updatedDateTime: new Date(), + updatedUser: currentUser.userName, + updatedUserDirectory: directory, + updatedUserGuid: currentUser.userGUID, + }; + + // to create new permit + let newPermit = new Permit(); + newPermit = Object.assign(newPermit, permit); + newPermit.permitId = null; + newPermit.permitNumber = permitNumber; + newPermit.applicationNumber = applicationNumber; + newPermit.permitStatus = voidPermitDto.status; + newPermit.revision = permit.revision + 1; + newPermit.previousRevision = +permitId; + newPermit.comment = voidPermitDto.comment; + newPermit = Object.assign(newPermit, userMetadata); + + let permitData = new PermitData(); + permitData.permitData = permit.permitData.permitData; + permitData = Object.assign(permitData, userMetadata); + newPermit.permitData = permitData; + + /* Create application to generate permit id. + this permit id will be used to generate permit number based this id's application number.*/ + newPermit = await queryRunner.manager.save(newPermit); + + //Update old permit status to SUPERSEDED. + await queryRunner.manager.update( + Permit, + { + permitId: newPermit.previousRevision, + }, + { + permitStatus: ApplicationStatus.SUPERSEDED, + updatedDateTime: new Date(), + updatedUser: currentUser.userName, + updatedUserDirectory: directory, + updatedUserGuid: currentUser.userGUID, + }, + ); + + const createTransactionDto = new CreateTransactionDto(); + createTransactionDto.pgTransactionId = voidPermitDto.pgTransactionId; + createTransactionDto.pgPaymentMethod = voidPermitDto.pgPaymentMethod; + createTransactionDto.pgCardType = voidPermitDto.pgCardType; + createTransactionDto.paymentMethodId = voidPermitDto.paymentMethodId; + createTransactionDto.transactionTypeId = + voidPermitDto.transactionAmount === 0 + ? TransactionType.ZERO_AMOUNT + : TransactionType.REFUND; + createTransactionDto.applicationDetails = [ + { + applicationId: newPermit.permitId, + transactionAmount: voidPermitDto.transactionAmount, + }, + ]; + const transactionDto = await this.paymentService.createTransactions( + currentUser, + createTransactionDto, + directory, + queryRunner, + ); + + const fetchedTransaction = await queryRunner.manager.findOne( + Transaction, + { + where: { transactionId: transactionDto.transactionId }, + relations: ['receipt'], + }, + ); + + const companyInfo = await this.companyService.findOne( + newPermit.companyId, + ); + + const fullNames = await this.applicationService.getFullNamesFromCache( + newPermit, + ); + const permitDataForTemplate = formatTemplateData( + newPermit, + fullNames, + companyInfo, + ); + let dopsRequestData: DopsGeneratedDocument = { - templateName: TemplateName.PERMIT_TROS, + templateName: + voidPermitDto.status == ApplicationStatus.VOIDED + ? TemplateName.PERMIT_TROS_VOID + : TemplateName.PERMIT_TROS_REVOKED, generatedDocumentFileName: permitDataForTemplate.permitNumber, templateData: permitDataForTemplate, documentsToMerge: permitDataForTemplate.permitData.commodities.map( @@ -411,19 +505,19 @@ export class PermitService { companyInfo.companyId, ); - //Generate receipt number for the permit to be created in database. - const receiptNo = await this.applicationService.generateReceiptNumber(); dopsRequestData = { templateName: TemplateName.PAYMENT_RECEIPT, - generatedDocumentFileName: `Receipt_No_${receiptNo}`, + generatedDocumentFileName: `Receipt_No_${fetchedTransaction.receipt.receiptNumber}`, templateData: { ...permitDataForTemplate, - ...transactionDetails, + transactionOrderNumber: fetchedTransaction.transactionOrderNumber, + transactionAmount: fetchedTransaction.totalTransactionAmount, + paymentMethod: fetchedTransaction.pgPaymentMethod, transactionDate: convertUtcToPt( - new Date(), + fetchedTransaction.transactionSubmitDate, 'MMM. D, YYYY, hh:mm a Z', ), - receiptNo, + receiptNo: fetchedTransaction.receipt.receiptNumber, }, }; @@ -438,47 +532,37 @@ export class PermitService { generatedPermitDocumentPromise, generatedReceiptDocumentPromise, ]); - permit.permitStatus = ApplicationStatus.SUPERSEDED; - // to create new permit - let newPermit = permit; - newPermit.permitId = null; - newPermit.permitStatus = voidPermitDto.status; - newPermit.revision = permit.revision + 1; - newPermit.previousRevision = +permitId; - newPermit.documentId = generatedDocuments.at(0).dmsId; - newPermit.applicationNumber = applicationNumber; - /* Create application to generate permit id. - this permit id will be used to generate permit number based this id's application number.*/ - newPermit = await queryRunner.manager.save(newPermit); + // Update Document Id on new permit + await queryRunner.manager.update( + Permit, + { + permitId: newPermit.permitId, + }, + { + documentId: generatedDocuments.at(0).dmsId, + updatedDateTime: new Date(), + updatedUser: currentUser.userName, + updatedUserDirectory: directory, + updatedUserGuid: currentUser.userGUID, + }, + ); - const receiptEntity: Receipt = new Receipt(); - const transaction = - await this.applicationService.findOneTransactionByOrderNumber( - transactionDetails.transactionOrderNumber, - ); - receiptEntity.transactionId = transaction.transactionId; - receiptEntity.receiptNumber = receiptNo; - receiptEntity.receiptDocumentId = generatedDocuments.at(1).dmsId; - await queryRunner.manager.save(receiptEntity); - /* const permitNumber = await this.applicationService.generatePermitNumber( - newPermit.permitId, + // Update Document Id on new receipt + await queryRunner.manager.update( + Receipt, + { + receiptId: fetchedTransaction.receipt.receiptId, + }, + { + receiptDocumentId: generatedDocuments.at(1).dmsId, + updatedDateTime: new Date(), + updatedUser: currentUser.userName, + updatedUserDirectory: directory, + updatedUserGuid: currentUser.userGUID, + }, ); - newPermit.permitNumber = permitNumber;*/ - // Save new permit - await queryRunner.manager - .createQueryBuilder() - .update('Permit') - .set({ permitStatus: voidPermitDto.status }) - .where('permitId = :permitId', { permitId: newPermit.permitId }) - .execute(); - //Update old permit status to SUPERSEDED. - await queryRunner.manager - .createQueryBuilder() - .update('Permit') - .set({ permitStatus: ApplicationStatus.SUPERSEDED }) - .where('permitId = :permitId', { permitId: permitId }) - .execute(); + await queryRunner.commitTransaction(); success = permit.permitId; @@ -495,7 +579,7 @@ export class PermitService { content: generatedDocuments.at(0).buffer.toString('base64'), }, { - filename: `Receipt_No_${receiptNo}.pdf`, + filename: `Receipt_No_${fetchedTransaction.receipt.receiptNumber}.pdf`, contentType: 'application/pdf', encoding: 'base64', content: generatedDocuments.at(1).buffer.toString('base64'), @@ -529,4 +613,4 @@ export class PermitService { }; return resultDto; } -} \ No newline at end of file +} diff --git a/backend/vehicles/src/modules/permit/profile/application.profile.ts b/backend/vehicles/src/modules/permit/profile/application.profile.ts index a4dfca79c..206b85564 100644 --- a/backend/vehicles/src/modules/permit/profile/application.profile.ts +++ b/backend/vehicles/src/modules/permit/profile/application.profile.ts @@ -25,7 +25,111 @@ export class ApplicationProfile extends AutomapperProfile { CreateApplicationDto, Permit, forMember( - (d) => d.permitData?.permitData, + (permit) => permit.createdUserGuid, + mapWithArguments((_, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (permit) => permit.createdUser, + mapWithArguments((_, { userName }) => { + return userName; + }), + ), + forMember( + (permit) => permit.createdUserDirectory, + mapWithArguments((_, { directory }) => { + return directory; + }), + ), + + forMember( + (permit) => permit.createdDateTime, + mapWithArguments((_, { timestamp }) => { + return timestamp; + }), + ), + + forMember( + (permit) => permit.updatedUserGuid, + mapWithArguments((_, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (permit) => permit.updatedUser, + mapWithArguments((_, { userName }) => { + return userName; + }), + ), + forMember( + (permit) => permit.updatedUserDirectory, + mapWithArguments((_, { directory }) => { + return directory; + }), + ), + + forMember( + (permit) => permit.updatedDateTime, + mapWithArguments((_, { timestamp }) => { + return timestamp; + }), + ), + + forMember( + (permit) => permit.permitData.createdUserGuid, + mapWithArguments((_, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (permit) => permit.permitData.createdUser, + mapWithArguments((_, { userName }) => { + return userName; + }), + ), + forMember( + (permit) => permit.permitData.createdUserDirectory, + mapWithArguments((_, { directory }) => { + return directory; + }), + ), + + forMember( + (permit) => permit.permitData.createdDateTime, + mapWithArguments((_, { timestamp }) => { + return timestamp; + }), + ), + + forMember( + (permit) => permit.permitData.updatedUserGuid, + mapWithArguments((_, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (permit) => permit.permitData.updatedUser, + mapWithArguments((_, { userName }) => { + return userName; + }), + ), + forMember( + (permit) => permit.permitData.updatedUserDirectory, + mapWithArguments((_, { directory }) => { + return directory; + }), + ), + + forMember( + (permit) => permit.permitData.updatedDateTime, + mapWithArguments((_, { timestamp }) => { + return timestamp; + }), + ), + + forMember( + (permit) => permit.permitData?.permitData, mapFrom((s) => { return s.permitData ? JSON.stringify(s.permitData) : undefined; }), @@ -50,6 +154,56 @@ export class ApplicationProfile extends AutomapperProfile { mapper, UpdateApplicationDto, Permit, + forMember( + (d) => d.updatedUserGuid, + mapWithArguments((updateApplicationDto, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (d) => d.updatedUser, + mapWithArguments((updateApplicationDto, { userName }) => { + return userName; + }), + ), + forMember( + (d) => d.updatedUserDirectory, + mapWithArguments((updateApplicationDto, { directory }) => { + return directory; + }), + ), + + forMember( + (d) => d.updatedDateTime, + mapWithArguments((updateApplicationDto, { timestamp }) => { + return timestamp; + }), + ), + forMember( + (d) => d.permitData.updatedUserGuid, + mapWithArguments((updateApplicationDto, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (d) => d.permitData.updatedUser, + mapWithArguments((updateApplicationDto, { userName }) => { + return userName; + }), + ), + forMember( + (d) => d.permitData.updatedUserDirectory, + mapWithArguments((updateApplicationDto, { directory }) => { + return directory; + }), + ), + + forMember( + (d) => d.permitData.updatedDateTime, + mapWithArguments((updateApplicationDto, { timestamp }) => { + return timestamp; + }), + ), forMember( (d) => d.permitData?.permitData, mapFrom((s) => { @@ -58,23 +212,23 @@ export class ApplicationProfile extends AutomapperProfile { ), forMember( (d) => d.permitId, - mapWithArguments((source, { permitId }) => { + mapWithArguments((updateApplicationDto, { permitId }) => { return permitId; }), ), forMember( (d) => d.previousRevision, - mapWithArguments((source, { previousRevision }) => { + mapWithArguments((updateApplicationDto, { previousRevision }) => { return previousRevision; }), ), forMember( (d) => d.permitData.permitDataId, - mapWithArguments((source, { permitDataId }) => { + mapWithArguments((updateApplicationDto, { permitDataId }) => { return permitDataId; }), ), ); }; } -} \ No newline at end of file +} diff --git a/backend/vehicles/src/modules/permit/profile/permit.profile.ts b/backend/vehicles/src/modules/permit/profile/permit.profile.ts index eb664cf4e..79e3a822a 100644 --- a/backend/vehicles/src/modules/permit/profile/permit.profile.ts +++ b/backend/vehicles/src/modules/permit/profile/permit.profile.ts @@ -10,7 +10,6 @@ import { Injectable } from '@nestjs/common'; import { Permit } from '../entities/permit.entity'; import { CreatePermitDto } from '../dto/request/create-permit.dto'; import { ReadPermitDto } from '../dto/response/read-permit.dto'; -import { PermitHistoryDto } from '../dto/response/permit-history.dto'; @Injectable() export class PermitProfile extends AutomapperProfile { @@ -25,68 +24,130 @@ export class PermitProfile extends AutomapperProfile { CreatePermitDto, Permit, forMember( - (d) => d.permitData?.permitData, - mapFrom((s) => { - return s.permitData ? JSON.stringify(s.permitData) : undefined; + (d) => d.createdUserGuid, + mapWithArguments((createPermitDto, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (d) => d.createdUser, + mapWithArguments((createPermitDto, { userName }) => { + return userName; + }), + ), + forMember( + (d) => d.createdUserDirectory, + mapWithArguments((createPermitDto, { directory }) => { + return directory; }), ), - ); - createMap( - mapper, - Permit, - ReadPermitDto, forMember( - (d) => d.permitData, - mapFrom((s) => { - return s.permitData?.permitData - ? (JSON.parse(s.permitData?.permitData) as JSON) - : undefined; + (d) => d.createdDateTime, + mapWithArguments((createPermitDto, { timestamp }) => { + return timestamp; }), ), - ); - // ToDo: revisit the maper for multiple transaction. - createMap( - mapper, - Permit, - PermitHistoryDto, + + forMember( + (d) => d.updatedUserGuid, + mapWithArguments((createPermitDto, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (d) => d.updatedUser, + mapWithArguments((createPermitDto, { userName }) => { + return userName; + }), + ), + forMember( + (d) => d.updatedUserDirectory, + mapWithArguments((createPermitDto, { directory }) => { + return directory; + }), + ), + forMember( - (d) => d.paymentMethod, - mapWithArguments((s) => { - return s.transactions[0].paymentMethod; + (d) => d.updatedDateTime, + mapWithArguments((createPermitDto, { timestamp }) => { + return timestamp; + }), + ), + + forMember( + (d) => d.permitData.createdUserGuid, + mapWithArguments((createPermitDto, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (d) => d.permitData.createdUser, + mapWithArguments((createPermitDto, { userName }) => { + return userName; + }), + ), + forMember( + (d) => d.permitData.createdUserDirectory, + mapWithArguments((createPermitDto, { directory }) => { + return directory; + }), + ), + + forMember( + (d) => d.permitData.createdDateTime, + mapWithArguments((createPermitDto, { timestamp }) => { + return timestamp; + }), + ), + + forMember( + (d) => d.permitData.updatedUserGuid, + mapWithArguments((createPermitDto, { userGUID }) => { + return userGUID; }), ), forMember( - (d) => d.providerTransactionId, - mapWithArguments((s) => { - return s.transactions[0].providerTransactionId; + (d) => d.permitData.updatedUser, + mapWithArguments((createPermitDto, { userName }) => { + return userName; }), ), forMember( - (d) => d.transactionAmount, - mapWithArguments((s) => { - return s.transactions[0].transactionAmount; + (d) => d.permitData.updatedUserDirectory, + mapWithArguments((createPermitDto, { directory }) => { + return directory; }), ), + forMember( - (d) => d.transactionOrderNumber, - mapWithArguments((s) => { - return s.transactions[0].transactionOrderNumber; + (d) => d.permitData.updatedDateTime, + mapWithArguments((createPermitDto, { timestamp }) => { + return timestamp; }), ), + forMember( - (d) => d.permitNumber, - mapWithArguments((s) => { - return s.permitNumber; + (d) => d.permitData?.permitData, + mapFrom((s) => { + return s.permitData ? JSON.stringify(s.permitData) : undefined; }), ), + ); + + createMap( + mapper, + Permit, + ReadPermitDto, forMember( - (d) => d.comment, - mapWithArguments((s) => { - return s.comment; + (d) => d.permitData, + mapFrom((s) => { + return s.permitData?.permitData + ? (JSON.parse(s.permitData?.permitData) as JSON) + : undefined; }), ), ); }; } -} \ No newline at end of file +} diff --git a/database/mssql/scripts/sampledata/dops.ORBC_DOCUMENT.Table.sql b/database/mssql/scripts/sampledata/dops.ORBC_DOCUMENT.Table.sql index 2b96a805b..7d564e7cc 100644 --- a/database/mssql/scripts/sampledata/dops.ORBC_DOCUMENT.Table.sql +++ b/database/mssql/scripts/sampledata/dops.ORBC_DOCUMENT.Table.sql @@ -3,9 +3,9 @@ GO SET IDENTITY_INSERT [dops].[ORBC_DOCUMENT] ON INSERT [dops].[ORBC_DOCUMENT] ([ID], [S3_OBJECT_ID], [S3_VERSION_ID], [S3_LOCATION], [OBJECT_MIME_TYPE], [FILE_NAME], [DMS_VERSION_ID], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (1, N'bcc0644f-9076-41e0-9841-4ee23e109e7a', NULL, N'https://moti-int.objectstore.gov.bc.ca/tran_api_orbc_docs_dev/tran_api_orbc_docs_dev%40moti-int.objectstore.gov.bc.ca/bcc0644f-9076-41e0-9841-4ee23e109e7a', N'application/vnd.openxmlformats-officedocument.wordprocessingml.document',N'tros-template-v1.docx',1, 1, N'dops', GETUTCDATE(), N'dops', GETUTCDATE()) -GO - INSERT [dops].[ORBC_DOCUMENT] ([ID], [S3_OBJECT_ID], [S3_VERSION_ID], [S3_LOCATION], [OBJECT_MIME_TYPE], [FILE_NAME], [DMS_VERSION_ID], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (2, N'de4229ce-4d4a-4129-8a6b-1f7a469ab667', NULL, N'https://moti-int.objectstore.gov.bc.ca/tran_api_orbc_docs_dev/tran_api_orbc_docs_dev%40moti-int.objectstore.gov.bc.ca/de4229ce-4d4a-4129-8a6b-1f7a469ab667', N'application/vnd.openxmlformats-officedocument.wordprocessingml.document',N'Payment Receipt Template.docx',1, 1, N'dops', GETUTCDATE(), N'dops', GETUTCDATE()) +INSERT [dops].[ORBC_DOCUMENT] ([ID], [S3_OBJECT_ID], [S3_VERSION_ID], [S3_LOCATION], [OBJECT_MIME_TYPE], [FILE_NAME], [DMS_VERSION_ID], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (3, N'D33C4B65-955F-4BBF-88B0-D59787E62A79', NULL, N'https://moti-int.objectstore.gov.bc.ca/tran_api_orbc_docs_dev/tran_api_orbc_docs_dev%40moti-int.objectstore.gov.bc.ca/d33c4b65-955f-4bbf-88b0-d59787e62a79', N'application/vnd.openxmlformats-officedocument.wordprocessingml.document',N'tros-template-void-template-v1.docx',1, 1, N'dops', GETUTCDATE(), N'dops', GETUTCDATE()) +INSERT [dops].[ORBC_DOCUMENT] ([ID], [S3_OBJECT_ID], [S3_VERSION_ID], [S3_LOCATION], [OBJECT_MIME_TYPE], [FILE_NAME], [DMS_VERSION_ID], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (4, N'95E8660A-74FC-411B-ACBD-5D43E447A5B4', NULL, N'https://moti-int.objectstore.gov.bc.ca/tran_api_orbc_docs_dev/tran_api_orbc_docs_dev%40moti-int.objectstore.gov.bc.ca/95e8660a-74fc-411b-acbd-5d43e447a5b4', N'application/vnd.openxmlformats-officedocument.wordprocessingml.document',N'tros-template-revoked-template-v1.docx',1, 1, N'dops', GETUTCDATE(), N'dops', GETUTCDATE()) GO SET IDENTITY_INSERT [dops].[ORBC_DOCUMENT] OFF diff --git a/database/mssql/scripts/sampledata/dops.ORBC_DOCUMENT_TEMPLATE.Table.sql b/database/mssql/scripts/sampledata/dops.ORBC_DOCUMENT_TEMPLATE.Table.sql index 4d3fd9bc1..f4eb8cae7 100644 --- a/database/mssql/scripts/sampledata/dops.ORBC_DOCUMENT_TEMPLATE.Table.sql +++ b/database/mssql/scripts/sampledata/dops.ORBC_DOCUMENT_TEMPLATE.Table.sql @@ -3,8 +3,9 @@ GO SET IDENTITY_INSERT [dops].[ORBC_DOCUMENT_TEMPLATE] ON INSERT [dops].[ORBC_DOCUMENT_TEMPLATE] ([TEMPLATE_ID], [TEMPLATE_NAME], [TEMPLATE_VERSION], [DOCUMENT_ID], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (1, N'PERMIT_TROS', 1, 1, 1, N'dops', GETUTCDATE(), N'dops', GETUTCDATE()) -GO INSERT [dops].[ORBC_DOCUMENT_TEMPLATE] ([TEMPLATE_ID], [TEMPLATE_NAME], [TEMPLATE_VERSION], [DOCUMENT_ID], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (2, N'PAYMENT_RECEIPT', 1, 2, 1, N'dops', GETUTCDATE(), N'dops', GETUTCDATE()) +INSERT [dops].[ORBC_DOCUMENT_TEMPLATE] ([TEMPLATE_ID], [TEMPLATE_NAME], [TEMPLATE_VERSION], [DOCUMENT_ID], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (3, N'PERMIT_TROS_VOID', 1, 3, 1, N'dops', GETUTCDATE(), N'dops', GETUTCDATE()) +INSERT [dops].[ORBC_DOCUMENT_TEMPLATE] ([TEMPLATE_ID], [TEMPLATE_NAME], [TEMPLATE_VERSION], [DOCUMENT_ID], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (4, N'PERMIT_TROS_REVOKED', 1, 4, 1, N'dops', GETUTCDATE(), N'dops', GETUTCDATE()) GO SET IDENTITY_INSERT [dops].[ORBC_DOCUMENT_TEMPLATE] OFF diff --git a/database/mssql/scripts/versions/v_4_ddl.sql b/database/mssql/scripts/versions/v_4_ddl.sql index db233d046..6622c80aa 100644 --- a/database/mssql/scripts/versions/v_4_ddl.sql +++ b/database/mssql/scripts/versions/v_4_ddl.sql @@ -273,6 +273,7 @@ INSERT [permit].[ORBC_PERMIT_STATUS_TYPE] ([PERMIT_STATUS_TYPE], [NAME], [DESCRI INSERT [permit].[ORBC_PERMIT_STATUS_TYPE] ([PERMIT_STATUS_TYPE], [NAME], [DESCRIPTION], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (N'VOIDED', N'Voided', NULL, NULL, N'dbo', GETUTCDATE(), N'dbo', GETUTCDATE()) INSERT [permit].[ORBC_PERMIT_STATUS_TYPE] ([PERMIT_STATUS_TYPE], [NAME], [DESCRIPTION], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (N'WAITING_APPROVAL', N'Waiting Approval', NULL, NULL, N'dbo', GETUTCDATE(), N'dbo', GETUTCDATE()) INSERT [permit].[ORBC_PERMIT_STATUS_TYPE] ([PERMIT_STATUS_TYPE], [NAME], [DESCRIPTION], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (N'WAITING_PAYMENT', N'Waiting Payment', NULL, NULL, N'dbo', GETUTCDATE(), N'dbo', GETUTCDATE()) +INSERT [permit].[ORBC_PERMIT_STATUS_TYPE] ([PERMIT_STATUS_TYPE], [NAME], [DESCRIPTION], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (N'PAYMENT_COMPLETE', N'Waiting Payment', NULL, NULL, N'dbo', GETUTCDATE(), N'dbo', GETUTCDATE()) INSERT [permit].[ORBC_PERMIT_TYPE] ([PERMIT_TYPE], [NAME], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (N'EPTOP', N'Extra-Provincial Temporary Operating', NULL, N'dbo', CAST(N'2023-08-08T21:30:23.6400000' AS DateTime2), N'dbo', CAST(N'2023-08-08T21:30:23.6400000' AS DateTime2)) INSERT [permit].[ORBC_PERMIT_TYPE] ([PERMIT_TYPE], [NAME], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (N'HC', N'Highway Crossing', NULL, N'dbo', CAST(N'2023-08-08T21:30:19.8133333' AS DateTime2), N'dbo', CAST(N'2023-08-08T21:30:19.8133333' AS DateTime2)) INSERT [permit].[ORBC_PERMIT_TYPE] ([PERMIT_TYPE], [NAME], [CONCURRENCY_CONTROL_NUMBER], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (N'LCV', N'Long Combination Vehicle', NULL, N'dbo', CAST(N'2023-08-08T21:29:58.5933333' AS DateTime2), N'dbo', CAST(N'2023-08-08T21:29:58.5933333' AS DateTime2)) diff --git a/database/mssql/scripts/versions/v_7_ddl.sql b/database/mssql/scripts/versions/v_7_ddl.sql index 0fc8dfa57..be28b2adb 100644 --- a/database/mssql/scripts/versions/v_7_ddl.sql +++ b/database/mssql/scripts/versions/v_7_ddl.sql @@ -5,10 +5,55 @@ GO SET NOCOUNT ON GO + +CREATE TABLE [permit].[ORBC_TRANSACTION_TYPE]( + [TRANSACTION_TYPE] [varchar](3) NOT NULL, + [DESCRIPTION] [varchar] (50) NULL, + [CONCURRENCY_CONTROL_NUMBER] [int] NULL, + [DB_CREATE_USERID] [varchar](63) NOT NULL, + [DB_CREATE_TIMESTAMP] [datetime2](7) NOT NULL, + [DB_LAST_UPDATE_USERID] [varchar](63) NOT NULL, + [DB_LAST_UPDATE_TIMESTAMP] [datetime2](7) NOT NULL, + CONSTRAINT [PK_ORBC_TRANSACTION_TYPE] PRIMARY KEY CLUSTERED +( + [TRANSACTION_TYPE] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO + +ALTER TABLE [permit].[ORBC_TRANSACTION_TYPE] ADD CONSTRAINT [ORBC_TRANSACTION_TYPE_DB_CREATE_USERID_DEF] DEFAULT (user_name()) FOR [DB_CREATE_USERID] +ALTER TABLE [permit].[ORBC_TRANSACTION_TYPE] ADD CONSTRAINT [ORBC_TRANSACTION_TYPE_DB_CREATE_TIMESTAMP_DEF] DEFAULT (getutcdate()) FOR [DB_CREATE_TIMESTAMP] +ALTER TABLE [permit].[ORBC_TRANSACTION_TYPE] ADD CONSTRAINT [ORBC_TRANSACTION_TYPE_DB_LAST_UPDATE_USERID_DEF] DEFAULT (user_name()) FOR [DB_LAST_UPDATE_USERID] +ALTER TABLE [permit].[ORBC_TRANSACTION_TYPE] ADD CONSTRAINT [ORBC_TRANSACTION_TYPE_DB_LAST_UPDATE_TIMESTAMP_DEF] DEFAULT (getutcdate()) FOR [DB_LAST_UPDATE_TIMESTAMP] +GO + +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The Transaction Type mirroring Bambora values' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1Name=N'ORBC_TRANSACTION_TYPE', @level2type=N'COLUMN',@level2name=N'TRANSACTION_TYPE' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The description of Transaction Type.' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1Name=N'ORBC_TRANSACTION_TYPE', @level2type=N'COLUMN',@level2name=N'DESCRIPTION' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Application code is responsible for retrieving the row and then incrementing the value of the CONCURRENCY_CONTROL_NUMBER column by one prior to issuing an update. If this is done then the update will succeed, provided that the row was not updated by any other transactions in the period between the read and the update operations.' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1Name=N'ORBC_TRANSACTION_TYPE', @level2type=N'COLUMN',@level2name=N'CONCURRENCY_CONTROL_NUMBER' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The user or proxy account that created or last updated the record.' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1Name=N'ORBC_TRANSACTION_TYPE', @level2type=N'COLUMN',@level2name=N'DB_LAST_UPDATE_USERID' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The date and time the record was created.' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1Name=N'ORBC_TRANSACTION_TYPE', @level2type=N'COLUMN',@level2name=N'DB_CREATE_TIMESTAMP' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The user or proxy account that created the record.' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1Name=N'ORBC_TRANSACTION_TYPE', @level2type=N'COLUMN',@level2name=N'DB_CREATE_USERID' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The date and time the record was created or last updated.' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1Name=N'ORBC_TRANSACTION_TYPE', @level2type=N'COLUMN',@level2name=N'DB_LAST_UPDATE_TIMESTAMP' +GO + +INSERT [permit].[ORBC_TRANSACTION_TYPE] ([TRANSACTION_TYPE], [DESCRIPTION], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (N'P', N'Payment Transaction', N'dbo', GETUTCDATE(), N'dbo', GETUTCDATE()) +INSERT [permit].[ORBC_TRANSACTION_TYPE] ([TRANSACTION_TYPE], [DESCRIPTION], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (N'R', N'Refund Transaction', N'dbo', GETUTCDATE(), N'dbo', GETUTCDATE()) +INSERT [permit].[ORBC_TRANSACTION_TYPE] ([TRANSACTION_TYPE], [DESCRIPTION], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (N'Z', N'Zero Amount Transaction', N'dbo', GETUTCDATE(), N'dbo', GETUTCDATE()) +GO + CREATE TABLE [permit].[ORBC_PAYMENT_METHOD_TYPE]( [PAYMENT_METHOD_TYPE] [int] IDENTITY(1,1) NOT NULL, [NAME] [varchar] (20) NOT NULL, [DESCRIPTION] [varchar] (50) NULL, + [APP_CREATE_TIMESTAMP] [datetime2](7) DEFAULT (getutcdate()), + [APP_CREATE_USERID] [nvarchar](30) DEFAULT (user_name()), + [APP_CREATE_USER_GUID] [char](32) NULL, + [APP_CREATE_USER_DIRECTORY] [nvarchar](30) DEFAULT (user_name()), + [APP_LAST_UPDATE_TIMESTAMP] [datetime2](7) DEFAULT (getutcdate()), + [APP_LAST_UPDATE_USERID] [nvarchar](30) DEFAULT (user_name()), + [APP_LAST_UPDATE_USER_GUID] [char](32) NULL, + [APP_LAST_UPDATE_USER_DIRECTORY] [nvarchar](30) DEFAULT (user_name()), + [CONCURRENCY_CONTROL_NUMBER] [int] NULL, [DB_CREATE_USERID] [varchar](63) NULL, [DB_CREATE_TIMESTAMP] [datetime2](7) NULL, [DB_LAST_UPDATE_USERID] [varchar](63) NULL, @@ -21,26 +66,27 @@ CREATE TABLE [permit].[ORBC_PAYMENT_METHOD_TYPE]( GO SET IDENTITY_INSERT [permit].[ORBC_PAYMENT_METHOD_TYPE] ON -INSERT [permit].[ORBC_PAYMENT_METHOD_TYPE] ([PAYMENT_METHOD_TYPE], [NAME], [DESCRIPTION], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (1, N'MOTI Pay', N'MOTI Pay with credit card', N'dbo', GETUTCDATE(), N'dbo', GETUTCDATE()) +INSERT [permit].[ORBC_PAYMENT_METHOD_TYPE] ([PAYMENT_METHOD_TYPE], [NAME], [DESCRIPTION], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (1, N'Web', N'Web', N'dbo', GETUTCDATE(), N'dbo', GETUTCDATE()) +INSERT [permit].[ORBC_PAYMENT_METHOD_TYPE] ([PAYMENT_METHOD_TYPE], [NAME], [DESCRIPTION], [DB_CREATE_USERID], [DB_CREATE_TIMESTAMP], [DB_LAST_UPDATE_USERID], [DB_LAST_UPDATE_TIMESTAMP]) VALUES (2, N'Icepay', N'Ice Pay', N'dbo', GETUTCDATE(), N'dbo', GETUTCDATE()) GO SET IDENTITY_INSERT [permit].[ORBC_PAYMENT_METHOD_TYPE] OFF CREATE TABLE [permit].[ORBC_TRANSACTION]( [TRANSACTION_ID] [bigint] IDENTITY(20000000,1) NOT NULL, [TRANSACTION_TYPE] [varchar] (3) NOT NULL, + [PAYMENT_METHOD_TYPE] [int] NOT NULL, + [TOTAL_TRANSACTION_AMOUNT] [decimal] (9, 2) NOT NULL, + [TRANSACTION_SUBMIT_DATE] [datetime2](7) NOT NULL, [TRANSACTION_ORDER_NUMBER] [varchar](30) NOT NULL, - [PROVIDER_TRANSACTION_ID] [bigint] NULL, - [TRANSACTION_AMOUNT] [decimal] (9, 2) NULL, - [TRANSACTION_APPROVED] [tinyint] NULL CHECK (TRANSACTION_APPROVED BETWEEN 0 AND 1), - [AUTH_CODE] [varchar] (32) NULL, - [TRANSACTION_CARD_TYPE] [nvarchar](2) NULL, - [TRANSACTION_SUBMIT_DATE] [datetime2](7) NULL, - [TRANSACTION_DATE] [datetime2](7) NULL, - [CVD_ID] [tinyint] NULL CHECK (CVD_ID BETWEEN 1 AND 6), - [PAYMENT_METHOD] [varchar] (2) NULL, - [PAYMENT_METHOD_TYPE] [int] NULL, - [MESSAGE_ID] [int] NULL, - [MESSAGE_TEXT] [varchar](100) NULL, + [PG_TRANSACTION_ID] [bigint] NULL, + [PG_TRANSACTION_APPROVED] [tinyint] NULL CHECK (PG_TRANSACTION_APPROVED BETWEEN 0 AND 1), + [PG_AUTH_CODE] [varchar] (32) NULL, + [PG_TRANSACTION_CARD_TYPE] [nvarchar](2) NULL, + [PG_TRANSACTION_DATE] [datetime2](7) NULL, + [PG_CVD_ID] [tinyint] NULL CHECK (PG_CVD_ID BETWEEN 1 AND 6), + [PG_PAYMENT_METHOD] [varchar] (2) NULL, + [PG_MESSAGE_ID] [int] NULL, + [PG_MESSAGE_TEXT] [varchar](100) NULL, [APP_CREATE_TIMESTAMP] [datetime2](7) DEFAULT (getutcdate()), [APP_CREATE_USERID] [nvarchar](30) DEFAULT (user_name()), [APP_CREATE_USER_GUID] [char](32) NULL, @@ -65,7 +111,7 @@ CREATE TABLE [permit].[ORBC_RECEIPT]( [RECEIPT_ID] [bigint] IDENTITY(1,1) NOT NULL, [RECEIPT_NUMBER] [varchar](19) NOT NULL, [TRANSACTION_ID] [bigint] NOT NULL, - [RECEIPT_DOCUMENT_ID] [varchar](10) NOT NULL, + [RECEIPT_DOCUMENT_ID] [varchar](10) NULL, [APP_CREATE_TIMESTAMP] [datetime2](7) DEFAULT (getutcdate()), [APP_CREATE_USERID] [nvarchar](30) DEFAULT (user_name()), [APP_CREATE_USER_GUID] [char](32) NULL, @@ -86,6 +132,7 @@ CREATE TABLE [permit].[ORBC_RECEIPT]( ) ON [PRIMARY] GO +ALTER TABLE [permit].[ORBC_TRANSACTION] ADD CONSTRAINT [ORBC_TRANSACTION_TRX_SUBMIT_DATE_DEF] DEFAULT (getutcdate()) FOR [TRANSACTION_SUBMIT_DATE] ALTER TABLE [permit].[ORBC_TRANSACTION] ADD CONSTRAINT [ORBC_TRANSACTION_DB_CREATE_USERID_DEF] DEFAULT (user_name()) FOR [DB_CREATE_USERID] ALTER TABLE [permit].[ORBC_TRANSACTION] ADD CONSTRAINT [ORBC_TRANSACTION_DB_CREATE_TIMESTAMP_DEF] DEFAULT (getutcdate()) FOR [DB_CREATE_TIMESTAMP] ALTER TABLE [permit].[ORBC_TRANSACTION] ADD CONSTRAINT [ORBC_TRANSACTION_DB_LAST_UPDATE_USERID_DEF] DEFAULT (user_name()) FOR [DB_LAST_UPDATE_USERID] @@ -96,6 +143,7 @@ CREATE TABLE [permit].[ORBC_PERMIT_TRANSACTION]( [ID] [bigint] IDENTITY(1,1) NOT NULL, [PERMIT_ID] [bigint] NOT NULL, [TRANSACTION_ID] [bigint] NOT NULL, + [TRANSACTION_AMOUNT] [decimal] (9, 2) NOT NULL, [APP_CREATE_TIMESTAMP] [datetime2](7) DEFAULT (getutcdate()), [APP_CREATE_USERID] [nvarchar](30) DEFAULT (user_name()), [APP_CREATE_USER_GUID] [char](32) NULL, @@ -126,6 +174,8 @@ ALTER TABLE [permit].[ORBC_RECEIPT] ADD CONSTRAINT [ORBC_RECEIPT_DB_LAST_UPDATE ALTER TABLE [permit].[ORBC_RECEIPT] ADD CONSTRAINT [ORBC_RECEIPT_DB_LAST_UPDATE_TIMESTAMP_DEF] DEFAULT (getutcdate()) FOR [DB_LAST_UPDATE_TIMESTAMP] ALTER TABLE [permit].[ORBC_TRANSACTION] WITH CHECK ADD CONSTRAINT [ORBC_TRANSACTION_PAYMENT_METHOD_FK] FOREIGN KEY([PAYMENT_METHOD_TYPE]) REFERENCES [permit].[ORBC_PAYMENT_METHOD_TYPE] ([PAYMENT_METHOD_TYPE]) +ALTER TABLE [permit].[ORBC_TRANSACTION] WITH CHECK ADD CONSTRAINT [ORBC_TRANSACTION_TYPE_FK] FOREIGN KEY([TRANSACTION_TYPE]) +REFERENCES [permit].[ORBC_TRANSACTION_TYPE] ([TRANSACTION_TYPE]) ALTER TABLE [permit].[ORBC_PERMIT_TRANSACTION] WITH CHECK ADD CONSTRAINT [ORBC_PERMIT_TRANSACTION_PERMIT_ID_FK] FOREIGN KEY([PERMIT_ID]) REFERENCES [permit].[ORBC_PERMIT] ([ID]) ALTER TABLE [permit].[ORBC_PERMIT_TRANSACTION] WITH CHECK ADD CONSTRAINT [ORBC_PERMIT_TRANSACTION_TRANSACTION_ID_FK] FOREIGN KEY([TRANSACTION_ID]) @@ -140,20 +190,21 @@ EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Payment method EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Primary key for the transaction metadata record' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TRANSACTION_ID' EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The original value sent to indicate the type of transaction to perform' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TRANSACTION_TYPE' EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The value of trnOrderNumber submitted in the transaction request' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TRANSACTION_ORDER_NUMBER' -EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Bambora-assigned eight-digit unique id number used to identify an individual transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PROVIDER_TRANSACTION_ID' -EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The amount of the transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TRANSACTION_AMOUNT' -EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Transaction approved or refused identifier' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TRANSACTION_APPROVED' -EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'If the transaction is approved this parameter will contain a unique bank-issued code' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'AUTH_CODE' -EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The type of card used in the transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TRANSACTION_CARD_TYPE' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Bambora-assigned eight-digit unique id number used to identify an individual transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PG_TRANSACTION_ID' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The total amount of the transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TOTAL_TRANSACTION_AMOUNT' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Transaction approved or refused identifier' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PG_TRANSACTION_APPROVED' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'If the transaction is approved this parameter will contain a unique bank-issued code' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PG_AUTH_CODE' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The type of card used in the transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PG_TRANSACTION_CARD_TYPE' EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The date and time that user submitted the transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TRANSACTION_SUBMIT_DATE' -EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The date and time that the transaction was processed' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TRANSACTION_DATE' -EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Card verification match ID' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'CVD_ID' -EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'2 characters that Bambora sends back references interac online transaction or credit card transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PAYMENT_METHOD' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The date and time that the transaction was processed' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PG_TRANSACTION_DATE' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Card verification match ID' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PG_CVD_ID' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'2 characters that Bambora sends back references interac online transaction or credit card transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PG_PAYMENT_METHOD' EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Payment method identifier of the user selected payment method' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PAYMENT_METHOD_TYPE' -EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The message id references a detailed approved/declined transaction response message' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'MESSAGE_ID' -EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'A basic approved/declined message which may be displayed to the customer on a confirmation page' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'MESSAGE_TEXT' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The message id references a detailed approved/declined transaction response message' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PG_MESSAGE_ID' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'A basic approved/declined message which may be displayed to the customer on a confirmation page' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PG_MESSAGE_TEXT' EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Permit ID relates to a transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_PERMIT_TRANSACTION', @level2type=N'COLUMN',@level2name=N'PERMIT_ID' EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Transaction ID relates to a permit' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_PERMIT_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TRANSACTION_ID' +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'The amount of the transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_PERMIT_TRANSACTION', @level2type=N'COLUMN',@level2name=N'TRANSACTION_AMOUNT' EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Receipt ID for a payment transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_RECEIPT', @level2type=N'COLUMN',@level2name=N'RECEIPT_ID' EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Receipt number for a payment transaction' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_RECEIPT', @level2type=N'COLUMN',@level2name=N'RECEIPT_NUMBER' EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Transaction ID of the payment receipt' , @level0type=N'SCHEMA',@level0name=N'permit', @level1type=N'TABLE',@level1name=N'ORBC_RECEIPT', @level2type=N'COLUMN',@level2name=N'TRANSACTION_ID' diff --git a/frontend/src/common/components/banners/CompanyBanner.tsx b/frontend/src/common/components/banners/CompanyBanner.tsx index 5f17610fc..fc69d22c2 100644 --- a/frontend/src/common/components/banners/CompanyBanner.tsx +++ b/frontend/src/common/components/banners/CompanyBanner.tsx @@ -1,13 +1,14 @@ import { Typography } from "@mui/material"; import "./CompanyBanner.scss"; -import { CompanyProfile } from "../../../features/manageProfile/types/manageProfile"; import { getDefaultRequiredVal } from "../../helpers/util"; export const CompanyBanner = ({ - companyInfo, + companyName, + clientNumber, }: { - companyInfo?: CompanyProfile; + companyName?: string; + clientNumber?: string; }) => { return (
@@ -22,7 +23,7 @@ export const CompanyBanner = ({ variant="h4" data-testid="company-banner-name" > - {getDefaultRequiredVal("", companyInfo?.legalName)} + {getDefaultRequiredVal("", companyName)}
@@ -36,7 +37,7 @@ export const CompanyBanner = ({ variant="h4" data-testid="company-banner-client" > - {getDefaultRequiredVal("", companyInfo?.clientNumber)} + {getDefaultRequiredVal("", clientNumber)}
diff --git a/frontend/src/common/components/banners/tests/helpers/CompanyBanner/prepare.tsx b/frontend/src/common/components/banners/tests/helpers/CompanyBanner/prepare.tsx index b8452ab88..7fb22bd21 100644 --- a/frontend/src/common/components/banners/tests/helpers/CompanyBanner/prepare.tsx +++ b/frontend/src/common/components/banners/tests/helpers/CompanyBanner/prepare.tsx @@ -30,7 +30,8 @@ export const defaultCompanyInfo = { export const renderTestComponent = (companyInfo?: CompanyProfile) => { return render( ); }; diff --git a/frontend/src/common/components/dashboard/Banner.scss b/frontend/src/common/components/dashboard/Banner.scss new file mode 100644 index 000000000..1c65cd885 --- /dev/null +++ b/frontend/src/common/components/dashboard/Banner.scss @@ -0,0 +1,26 @@ +.layout-banner { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 1.5rem; + padding-bottom: 0.25rem; + + &--extend { + padding-top: 2.5rem; + padding-bottom: 2.5rem; + } + + &__text-section { + display: flex; + justify-content: space-between; + + .banner-button { + display: flex; + align-items: center; + } + } + + .banner-subtext { + margin-top: -1rem; + } +} diff --git a/frontend/src/common/components/dashboard/Banner.tsx b/frontend/src/common/components/dashboard/Banner.tsx index 103196924..5066fd5fe 100644 --- a/frontend/src/common/components/dashboard/Banner.tsx +++ b/frontend/src/common/components/dashboard/Banner.tsx @@ -1,6 +1,7 @@ -import { Box, Grid } from "@mui/material"; -import "./Dashboard.scss"; import { ReactNode } from "react"; +import { Box, Grid } from "@mui/material"; + +import "./Banner.scss"; /** * The Banner component is a common component that is used to display a banner in a dashboard @@ -23,17 +24,16 @@ export const Banner = ({ extendHeight?: boolean; }) => (

{bannerText}

- + {bannerButton ? bannerButton : null}
diff --git a/frontend/src/common/components/dashboard/Dashboard.scss b/frontend/src/common/components/dashboard/Dashboard.scss index bdb9a7db2..6bbda543b 100644 --- a/frontend/src/common/components/dashboard/Dashboard.scss +++ b/frontend/src/common/components/dashboard/Dashboard.scss @@ -1,33 +1,21 @@ .tabpanel-container { - padding: 0px 60px; + padding: 0 3.75rem; overflow: hidden; background-color: white; min-height: calc(100vh - 306px); height: 100%; } -.layout-banner { - display: flex; - justify-content: space-between; - align-items: center; - padding-top: 25px; - padding-bottom: 5px; -} - .layout-box { - padding: 0px 60px 0px 60px; + padding: 0 3.75rem; } @media screen and (max-width: 768px) { .tabpanel-container { - padding: 0px 20px; + padding: 0 1.25rem; } .layout-box { - padding: 0px 20px; + padding: 0 1.25rem; } } - -.banner-subtext { - margin-top: -16px; -} diff --git a/frontend/src/common/components/error/ErrorPage.scss b/frontend/src/common/components/error/ErrorPage.scss new file mode 100644 index 000000000..6050d61e3 --- /dev/null +++ b/frontend/src/common/components/error/ErrorPage.scss @@ -0,0 +1,45 @@ +@import "../../../themes/orbcStyles.scss"; + +.error-page { + padding: 1.5rem; + padding-bottom: 0; + margin: 0 auto; + width: 100%; + max-width: 1200px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + height: calc(100vh - 179px); + + &__wrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + margin: 2em 0; + max-width: 640px; + } + + &__img { + margin: 1em 0; + } + + &__title { + font-size: 1.5rem; + font-weight: bold; + color: $bc-black; + margin: 1em 0; + } + + &__msg { + color: $bc-black; + margin: 1em 0; + text-align: center; + + .msg { + margin: 1.25rem 1rem; + } + } +} diff --git a/frontend/src/common/components/error/ErrorPage.tsx b/frontend/src/common/components/error/ErrorPage.tsx new file mode 100644 index 000000000..ec725c149 --- /dev/null +++ b/frontend/src/common/components/error/ErrorPage.tsx @@ -0,0 +1,35 @@ +import { Divider } from "@mui/material"; + +import "./ErrorPage.scss"; + +export const ErrorPage = ({ + errorTitle, + msgNode, +}: { + errorTitle: string; + msgNode: React.ReactNode; +}) => { + return ( +
+
+ Error Screen Graphic +
+ {errorTitle} +
+
+ +

+ {msgNode} +

+ +
+
+
+ ); +}; diff --git a/frontend/src/common/components/form/CustomFormComponents.tsx b/frontend/src/common/components/form/CustomFormComponents.tsx index c8e865499..4a663b7c8 100644 --- a/frontend/src/common/components/form/CustomFormComponents.tsx +++ b/frontend/src/common/components/form/CustomFormComponents.tsx @@ -179,8 +179,14 @@ export const CustomFormComponent = ({ rules={rules} render={({ fieldState: { invalid } }) => ( <> - + @@ -196,7 +202,11 @@ export const CustomFormComponent = ({ {renderSubFormComponent(invalid)} {invalid && ( - + {i18options?.inValidMessage_i18 ? t(i18options?.inValidMessage_i18, { fieldName: label, diff --git a/frontend/src/common/components/table/OnRouteBCTableRowActions.tsx b/frontend/src/common/components/table/OnRouteBCTableRowActions.tsx index bd702f5c2..322f4133e 100644 --- a/frontend/src/common/components/table/OnRouteBCTableRowActions.tsx +++ b/frontend/src/common/components/table/OnRouteBCTableRowActions.tsx @@ -36,7 +36,7 @@ export const OnRouteBCTableRowActions = ({ const handleClick = useCallback((event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }, []); - const handleClose = (_event: React.MouseEvent) => { + const handleClose = () => { setAnchorEl(null); }; diff --git a/frontend/src/common/helpers/formatDate.ts b/frontend/src/common/helpers/formatDate.ts index db6923e4e..286eba06b 100644 --- a/frontend/src/common/helpers/formatDate.ts +++ b/frontend/src/common/helpers/formatDate.ts @@ -16,8 +16,10 @@ export const DATE_FORMATS = { SHORT: "LL", LONG: "MMM. DD, YYYY, hh:mm a z", DATEONLY_SHORT_NAME: "MMM D, YYYY", + DATEONLY_ABBR_MONTH: "MMM. D, YYYY", DATETIME_LONG_TZ: "MMM D, YYYY, h:mm A z", DATEONLY_SLASH: "MM/DD/YYYY", + ISO8601: "YYYY-MM-DDTHH:mm:ss.SSS[Z]", }; /** diff --git a/frontend/src/common/pages/NotFound.tsx b/frontend/src/common/pages/NotFound.tsx index 3b98a0fb7..adb050c99 100644 --- a/frontend/src/common/pages/NotFound.tsx +++ b/frontend/src/common/pages/NotFound.tsx @@ -1,32 +1,16 @@ -import { Box, Container, Divider, Typography } from "@mui/material"; +import { Link } from "react-router-dom"; + +import { ErrorPage } from "../components/error/ErrorPage"; export const NotFound = () => { return ( - - - - - Profile Set-up Successful - - - Page not found - - - -

- - Please visit onRouteBC. - -

- -
-
-
-
+ + Please visit onRouteBC. + + )} + /> ); }; diff --git a/frontend/src/common/pages/Unauthorized.tsx b/frontend/src/common/pages/Unauthorized.tsx index ff598c305..f3d50a9a8 100644 --- a/frontend/src/common/pages/Unauthorized.tsx +++ b/frontend/src/common/pages/Unauthorized.tsx @@ -1,36 +1,14 @@ -import { Box, Container, Typography, Divider} from "@mui/material"; +import { ErrorPage } from "../components/error/ErrorPage"; export const Unauthorized = () => { return ( - - - - - - Profile Set-up Successful - - - Unauthorized access - - - - -
- -

- You do not have the necessary authorization to view this page. Please contact your administrator. -

- -
-
-
-
-
-
+ + You do not have the necessary authorization to view this page. Please contact your administrator. + + )} + /> ); }; diff --git a/frontend/src/common/pages/Unexpected.tsx b/frontend/src/common/pages/Unexpected.tsx index c936d866b..c6d312600 100644 --- a/frontend/src/common/pages/Unexpected.tsx +++ b/frontend/src/common/pages/Unexpected.tsx @@ -1,37 +1,16 @@ -import { Box, Container, Typography, Divider} from "@mui/material"; +import { Link } from "react-router-dom"; + +import { ErrorPage } from "../components/error/ErrorPage"; export const Unexpected = () => { return ( - - - - - - Profile Set-up Successful - - - Unexpected error - - - - -
- -

- Please refresh to continue. If the error persists -

contact us.

-

- -
-
-
-
-
-
+ + Please refresh to continue. If the error persists, contact us. + + )} + /> ); }; diff --git a/frontend/src/common/pages/UniversalUnauthorized.tsx b/frontend/src/common/pages/UniversalUnauthorized.tsx index d77062668..b429ab898 100644 --- a/frontend/src/common/pages/UniversalUnauthorized.tsx +++ b/frontend/src/common/pages/UniversalUnauthorized.tsx @@ -1,33 +1,14 @@ -import { Box, Container, Typography, Divider} from "@mui/material"; +import { ErrorPage } from "../components/error/ErrorPage"; export const UniversalUnauthorized = () => { return ( - - - - - - Unauthorized - - - Unauthorized access - - - -

- - You do not have the necessary authorization to view this page. - -

- -
-
-
-
+ + You do not have the necessary authorization to view this page. + + )} + /> ); }; diff --git a/frontend/src/features/idir/search/components/IDIRPermitSearchRowActions.tsx b/frontend/src/features/idir/search/components/IDIRPermitSearchRowActions.tsx index 228d18b5e..a00f96ac2 100644 --- a/frontend/src/features/idir/search/components/IDIRPermitSearchRowActions.tsx +++ b/frontend/src/features/idir/search/components/IDIRPermitSearchRowActions.tsx @@ -2,10 +2,41 @@ import { useState } from "react"; import { OnRouteBCTableRowActions } from "../../../../common/components/table/OnRouteBCTableRowActions"; import PermitResendDialog from "./PermitResendDialog"; import { viewReceiptPdf } from "../../../permits/helpers/permitPDFHelper"; +import { useNavigate } from "react-router-dom"; +import * as routes from "../../../../routes/constants"; +import { USER_AUTH_GROUP } from "../../../manageProfile/types/userManagement.d"; -const ACTIVE_OPTIONS = ["Amend", "View Receipt", "Resend", "Void"]; -const EXPIRED_OPTIONS = ["View Receipt", "Resend"]; -const MINIMAL_OPTIONS = ["View Receipt"]; +interface PermitAction { + actionName: string; + isAuthorized: (isExpired: boolean, userAuthGroup?: string) => boolean; +} + +const PERMIT_ACTIONS: PermitAction[] = [ + { + actionName: "Amend", + isAuthorized: (isExpired: boolean, userAuthGroup?: string) => + !isExpired && ( + userAuthGroup === USER_AUTH_GROUP.PPCCLERK || userAuthGroup === USER_AUTH_GROUP.SYSADMIN + ), + }, + { + actionName: "View Receipt", + isAuthorized: (_: boolean, userAuthGroup?: string) => + userAuthGroup === USER_AUTH_GROUP.PPCCLERK + || userAuthGroup === USER_AUTH_GROUP.SYSADMIN + || userAuthGroup === USER_AUTH_GROUP.EOFFICER, + }, + { + actionName: "Resend", + isAuthorized: (_: boolean, userAuthGroup?: string) => + userAuthGroup === USER_AUTH_GROUP.PPCCLERK || userAuthGroup === USER_AUTH_GROUP.SYSADMIN, + }, + { + actionName: "Void", + isAuthorized: (isExpired: boolean, userAuthGroup?: string) => + !isExpired && userAuthGroup === USER_AUTH_GROUP.SYSADMIN, + }, +]; /** * Returns options for the row actions. @@ -13,8 +44,9 @@ const MINIMAL_OPTIONS = ["View Receipt"]; * @returns string[] */ const getOptions = (isExpired: boolean, userAuthGroup?: string): string[] => { - if (userAuthGroup === "EOFFICER") return MINIMAL_OPTIONS; - return isExpired ? EXPIRED_OPTIONS : ACTIVE_OPTIONS; + return PERMIT_ACTIONS + .filter(action => action.isAuthorized(isExpired, userAuthGroup)) + .map(action => action.actionName); }; /** @@ -22,7 +54,7 @@ const getOptions = (isExpired: boolean, userAuthGroup?: string): string[] => { */ export const IDIRPermitSearchRowActions = ({ permitId, - isExpired, + isPermitInactive, permitNumber, email, fax, @@ -33,9 +65,9 @@ export const IDIRPermitSearchRowActions = ({ */ permitId: number; /** - * Has the permit expired? + * Is the permit inactive (voided/superseded/revoked) or expired? */ - isExpired: boolean; + isPermitInactive: boolean; /** * The permit number */ @@ -49,11 +81,12 @@ export const IDIRPermitSearchRowActions = ({ */ fax?: string; /** - * The auth group for the current user (eg. PPC_CLERK or EOFFICER) + * The auth group for the current user (eg. PPCCLERK or EOFFICER) */ userAuthGroup?: string; }) => { const [isResendOpen, setIsResendOpen] = useState(false); + const navigate = useNavigate(); /** * Function to handle user selection from the options. @@ -65,6 +98,8 @@ export const IDIRPermitSearchRowActions = ({ setIsResendOpen(() => true); } else if (selectedOption === "View Receipt") { viewReceiptPdf(permitId.toString()); + } else if (selectedOption === "Void") { + navigate(`/${routes.PERMITS}/${permitId}/${routes.PERMIT_VOID}`); } }; @@ -72,7 +107,7 @@ export const IDIRPermitSearchRowActions = ({ <> { if (!userAuthGroup) return false; // Check if the user has PPC role to confirm - return userAuthGroup === "PPC_CLERK" || userAuthGroup === "EOFFICER"; + const allowableAuthGroups = [ + USER_AUTH_GROUP.PPCCLERK, + USER_AUTH_GROUP.EOFFICER, + USER_AUTH_GROUP.SYSADMIN, + ] as string[]; + return allowableAuthGroups.includes(userAuthGroup); }; /* @@ -46,8 +53,8 @@ export const IDIRSearchResults = memo( }) => { const { searchValue, searchByFilter, searchEntity } = searchParams; const { idirUserDetails } = useContext(OnRouteBCContext); - const [isActiveRecordsOnly, setIsActiveRecordsOnly] = - useState(false); + const [isActiveRecordsOnly, setIsActiveRecordsOnly] = useState(false); + const { data, isLoading, isError } = useQuery( ["search-entity", searchValue, searchByFilter, searchEntity], () => @@ -75,7 +82,8 @@ export const IDIRSearchResults = memo( if (isActiveRecordsOnly) { // Returns unexpired permits return initialData.filter( - ({ permitData: { expiryDate } }) => !hasPermitExpired(expiryDate) + ({ permitStatus, permitData: { expiryDate } }) => + !hasPermitExpired(expiryDate) && !isPermitInactive(permitStatus) ); } return initialData; @@ -137,14 +145,15 @@ export const IDIRSearchResults = memo( table: MRT_TableInstance; row: MRT_Row; }) => { - const isExpired = hasPermitExpired( + const isInactive = hasPermitExpired( row.original.permitData.expiryDate - ); + ) || isPermitInactive(row.original.permitStatus); + if (shouldShowRowActions(idirUserDetails?.userAuthGroup)) { return ( [] = [ permitStatus, permitData: { expiryDate }, } = permit; - let permitChip = undefined; - if (permitStatus === "REVOKED" || permitChip == "VOIDED") { - permitChip = ( - - ); - } else if (hasPermitExpired(expiryDate)) { - permitChip = ; - } + return ( <> [] = [ > {props.cell.getValue()} - {permitChip} + {hasPermitExpired(expiryDate) ? ( + + ) : ( + + )} ); }, diff --git a/frontend/src/features/manageProfile/components/forms/userManagement/EditUser.tsx b/frontend/src/features/manageProfile/components/forms/userManagement/EditUser.tsx index b6f3cc73e..db25d4727 100644 --- a/frontend/src/features/manageProfile/components/forms/userManagement/EditUser.tsx +++ b/frontend/src/features/manageProfile/components/forms/userManagement/EditUser.tsx @@ -30,7 +30,7 @@ import { BC_COLOURS } from "../../../../../themes/bcGovStyles"; import { updateUserInfo } from "../../../apiManager/manageProfileAPI"; import { BCEID_PROFILE_TABS } from "../../../types/manageProfile.d"; import { - BCeIDAuthGroup, + BCEID_AUTH_GROUP, ReadCompanyUser, } from "../../../types/userManagement.d"; import UserGroupsAndPermissionsModal from "../../user-management/UserGroupsAndPermissionsModal"; @@ -60,7 +60,7 @@ export const EditUserForm = memo( countryCode: getDefaultRequiredVal("", userInfo?.countryCode), provinceCode: getDefaultRequiredVal("", userInfo?.provinceCode), city: getDefaultRequiredVal("", userInfo?.city), - userAuthGroup: BCeIDAuthGroup.ORGADMIN, + userAuthGroup: BCEID_AUTH_GROUP.ORGADMIN, }, }); const { handleSubmit } = formMethods; diff --git a/frontend/src/features/manageProfile/components/forms/userManagement/UserAuthRadioGroup.tsx b/frontend/src/features/manageProfile/components/forms/userManagement/UserAuthRadioGroup.tsx index ff63e5c44..335fbaa5d 100644 --- a/frontend/src/features/manageProfile/components/forms/userManagement/UserAuthRadioGroup.tsx +++ b/frontend/src/features/manageProfile/components/forms/userManagement/UserAuthRadioGroup.tsx @@ -6,7 +6,7 @@ import { RadioGroup, } from "@mui/material"; import { CustomInputHTMLAttributes } from "../../../../../common/types/formElements"; -import { BCeIDAuthGroup } from "../../../types/userManagement.d"; +import { BCEID_AUTH_GROUP } from "../../../types/userManagement.d"; import "../myInfo/MyInfoForm.scss"; import { ControllerFieldState, @@ -33,7 +33,7 @@ export const UserAuthRadioGroup = ({ aria-labelledby="radio-buttons-group-label" > { const formMethods = useForm({ defaultValues: { - userAuthGroup: BCeIDAuthGroup.CVCLIENT, + userAuthGroup: BCEID_AUTH_GROUP.CVCLIENT, }, reValidateMode: "onBlur", }); diff --git a/frontend/src/features/manageProfile/pages/CompanyInfo.tsx b/frontend/src/features/manageProfile/pages/CompanyInfo.tsx index f8b3bce1e..5b3ee608e 100644 --- a/frontend/src/features/manageProfile/pages/CompanyInfo.tsx +++ b/frontend/src/features/manageProfile/pages/CompanyInfo.tsx @@ -31,7 +31,10 @@ export const CompanyInfo = ({ return ( <> {isEditting ?
: null} - + {isEditting ? ( { if (!userAuthGroup) return ""; switch (userAuthGroup) { - case BCeIDAuthGroup.CVCLIENT: + case BCEID_AUTH_GROUP.CVCLIENT: return "Permit Applicant"; - case BCeIDAuthGroup.ORGADMIN: - case BCeIDAuthGroup.PUBLIC: + case BCEID_AUTH_GROUP.ORGADMIN: + case BCEID_AUTH_GROUP.PUBLIC: default: return "Administrator"; } diff --git a/frontend/src/features/manageProfile/types/userManagement.d.ts b/frontend/src/features/manageProfile/types/userManagement.d.ts index 35de5f975..d84b6f286 100644 --- a/frontend/src/features/manageProfile/types/userManagement.d.ts +++ b/frontend/src/features/manageProfile/types/userManagement.d.ts @@ -1,13 +1,31 @@ import { UserInformation } from "./manageProfile"; +/** + * All user auth groups + */ +export const USER_AUTH_GROUP = { + ANONYMOUS: "ANONYMOUS", + CVCLIENT: "CVCLIENT", + IDIRBASIC: "IDIRBASIC", + ORGADMIN: "ORGADMIN", + PPCCLERK: "PPCCLERK", + PUBLIC: "PUBLIC", + SYSADMIN: "SYSADMIN", + EOFFICER: "EOFFICER", +} as const; + +export type UserAuthGroup = typeof USER_AUTH_GROUP[keyof typeof USER_AUTH_GROUP]; + /** * The types of user auth groups for BCeID users. */ -export enum BCeIDAuthGroup { - PUBLIC = "PUBLIC", - CVCLIENT = "CVCLIENT", - ORGADMIN = "ORGADMIN", -} +export const BCEID_AUTH_GROUP = { + PUBLIC: USER_AUTH_GROUP.PUBLIC, + CVCLIENT: USER_AUTH_GROUP.CVCLIENT, + ORGADMIN: USER_AUTH_GROUP.ORGADMIN, +} as const; + +export type BCeIDAuthGroup = typeof BCEID_AUTH_GROUP[keyof typeof BCEID_AUTH_GROUP]; /** * The types of user statuses for BCeID users. diff --git a/frontend/src/features/permits/apiManager/endpoints/endpoints.ts b/frontend/src/features/permits/apiManager/endpoints/endpoints.ts index 3f5e61af6..9b2eb1c89 100644 --- a/frontend/src/features/permits/apiManager/endpoints/endpoints.ts +++ b/frontend/src/features/permits/apiManager/endpoints/endpoints.ts @@ -3,6 +3,7 @@ import { VEHICLES_URL } from "../../../../common/apiManager/endpoints/endpoints" export const PERMITS_API = { BASE: `${VEHICLES_URL}/permits`, SUBMIT_TERM_OVERSIZE_PERMIT: `${VEHICLES_URL}/permits/applications`, + ISSUE_PERMIT: `${VEHICLES_URL}/permits/applications/issue`, }; export const APPLICATION_UPDATE_STATUS_API = `${VEHICLES_URL}/permits/applications/status`; diff --git a/frontend/src/features/permits/apiManager/permitsAPI.ts b/frontend/src/features/permits/apiManager/permitsAPI.ts index 8a181a2a3..a2d7fe553 100644 --- a/frontend/src/features/permits/apiManager/permitsAPI.ts +++ b/frontend/src/features/permits/apiManager/permitsAPI.ts @@ -1,3 +1,18 @@ +import { DATE_FORMATS, toLocal } from "../../../common/helpers/formatDate"; +import { mapApplicationToApplicationRequestData } from "../helpers/mappers"; +import { VEHICLES_URL } from "../../../common/apiManager/endpoints/endpoints"; +import { IssuePermitsResponse, ReadPermitDto } from "../types/permit"; +import { PaginatedResponse } from "../../../common/types/common"; +import { PERMIT_STATUSES } from "../types/PermitStatus"; +import { PermitHistory } from "../types/PermitHistory"; +import { getPermitTypeName } from "../types/PermitType"; +import { + CompleteTransactionRequestData, + CompleteTransactionResponseData, + StartTransactionRequestData, + StartTransactionResponseData, +} from "../types/payment"; + import { getCompanyIdFromSession, httpGETRequest, @@ -8,33 +23,23 @@ import { } from "../../../common/apiManager/httpRequestHandler"; import { + applyWhenNotNullable, getDefaultRequiredVal, replaceEmptyValuesWithNull, } from "../../../common/helpers/util"; + import { Application, ApplicationResponse, PermitApplicationInProgress, } from "../types/application"; -import { DATE_FORMATS, toLocal } from "../../../common/helpers/formatDate"; + import { APPLICATION_UPDATE_STATUS_API, PAYMENT_API, PERMITS_API, } from "./endpoints/endpoints"; -import { mapApplicationToApplicationRequestData } from "../helpers/mappers"; -import { PermitTransaction, Transaction } from "../types/payment"; -import { VEHICLES_URL } from "../../../common/apiManager/endpoints/endpoints"; -import { ReadPermitDto } from "../types/permit"; -import { PaginatedResponse } from "../../../common/types/common"; - -/** - * A record containing permit keys and full forms. - */ -const permitAbbreviations: Record = { - TROS: "Term Oversize", - STOS: "Single Trip Oversize", -}; +import { RevokePermitRequestData, VoidPermitRequestData, VoidPermitResponseData } from "../pages/Void/types/VoidPermit"; /** * Submits a new term oversize application. @@ -93,7 +98,7 @@ export const getApplicationsInProgress = async (): Promise< ).map((application) => { return { ...application, - permitType: permitAbbreviations[application.permitType], + permitType: getPermitTypeName(application.permitType) as string, createdDateTime: toLocal( application.createdDateTime, DATE_FORMATS.DATETIME_LONG_TZ @@ -120,19 +125,25 @@ export const getApplicationsInProgress = async (): Promise< }; /** - * Fetch in-progress application by its permit id. + * Fetch application by its permit id. * @param permitId permit id of the application to fetch - * @returns ApplicationResponse data as response, or undefined if fetch failed + * @returns ApplicationResponse data as response, or null if fetch failed */ -export const getApplicationInProgressById = ( - permitId: string | undefined -): Promise => { - const companyId = getCompanyIdFromSession(); - let url = `${VEHICLES_URL}/permits/applications/${permitId}`; - if (companyId) { - url += `?companyId=${companyId}`; +export const getApplicationByPermitId = async ( + permitId?: string +): Promise => { + try { + const companyId = getCompanyIdFromSession(); + let url = `${VEHICLES_URL}/permits/applications/${permitId}`; + if (companyId) { + url += `?companyId=${companyId}`; + } + + const response = await httpGETRequest(url); + return response.data; + } catch (err) { + return null; } - return httpGETRequest(url).then((response) => response.data); }; /** @@ -141,7 +152,7 @@ export const getApplicationInProgressById = ( * @returns A Promise with the API response. */ export const deleteApplications = async (applicationIds: Array) => { - const requestBody = { applicationIds, applicationStatus: "CANCELLED" }; + const requestBody = { applicationIds, applicationStatus: PERMIT_STATUSES.CANCELLED }; return await httpPOSTRequest( `${APPLICATION_UPDATE_STATUS_API}`, replaceEmptyValuesWithNull(requestBody) @@ -211,32 +222,104 @@ export const downloadReceiptPdf = async (permitId: string) => { }; /** - * Generates a URL for making a payment transaction with Moti Pay. - * @param {number} transactionAmount - The amount of the transaction. - * @returns {Promise} - A Promise that resolves to the transaction URL. + * Start making a payment transaction with Moti Pay. + * @param {StartTransactionRequestData} requestData - Payment information that is to be submitted. + * @returns {Promise} - A Promise that resolves to the submitted transaction with URL. */ -export const getMotiPayTransactionUrl = async ( - paymentMethodId: number, - transactionSubmitDate: string, - transactionAmount: number, - permitIds: string[] -): Promise => { - const url = - `${PAYMENT_API}?` + - `paymentMethodId=${paymentMethodId}` + - `&transactionSubmitDate=${transactionSubmitDate}` + - `&transactionAmount=${transactionAmount}` + - `&permitIds=${permitIds.toString()}`; - return httpGETRequest(url).then((response) => { - return response.data.url; - }); +export const startTransaction = async ( + requestData: StartTransactionRequestData +): Promise => { + try { + const response = await httpPOSTRequest(PAYMENT_API, replaceEmptyValuesWithNull(requestData)); + if (response.status !== 201) { + return null; + } + return response.data as StartTransactionResponseData; + } catch (err) { + console.error(err); + return null; + } }; -export const postTransaction = async ( - transactionDetails: Transaction -): Promise => { - const url = `${PAYMENT_API}`; - return await httpPOSTRequest(url, transactionDetails); +/** + * Completes the transaction after payment is successful. + * @param transactionId - The id for the transaction to be completed + * @param transactionDetails - The complete transaction details to be submitted after payment + * @returns Promise that resolves to a successful transaction. + */ +export const completeTransaction = async ( + transactionId: string, + transactionDetails: CompleteTransactionRequestData +): Promise => { + try { + const response = await httpPUTRequest( + `${PAYMENT_API}/${transactionId}/payment-gateway`, + transactionDetails + ); + if (response.status !== 200) { + return null; + } + return response.data as CompleteTransactionResponseData; + } catch (err) { + console.error(err); + return null; + } +}; + +/** + * Issues the permits indicated by the application/permit ids. + * @param ids Application/permit ids for the permits to be issued. + * @returns Successful and failed permit ids that were issued. + */ +export const issuePermits = async ( + ids: string[], +): Promise => { + try { + const companyId = getCompanyIdFromSession(); + const response = await httpPOSTRequest( + PERMITS_API.ISSUE_PERMIT, + replaceEmptyValuesWithNull({ + applicationIds: [...ids], + companyId: applyWhenNotNullable((companyId) => Number(companyId), companyId), + }) + ); + + if (response.status !== 201) { + return { + success: [], + failure: [...ids], + }; + } + return response.data as IssuePermitsResponse; + } catch (err) { + console.error(err); + return { + success: [], + failure: [...ids], + }; + } +}; + +/** + * Get permit by permit id + * @param permitId Permit id of the permit to be retrieved. + * @returns Permit information if found, or undefined + */ +export const getPermit = async (permitId?: string): Promise => { + if (!permitId) return null; + const companyId = getDefaultRequiredVal("", getCompanyIdFromSession()); + let permitsURL = `${VEHICLES_URL}/permits/${permitId}`; + const queryParams = []; + if (companyId) { + queryParams.push(`companyId=${companyId}`); + } + if (queryParams.length > 0) { + permitsURL += `?${queryParams.join("&")}`; + } + + const response = await httpGETRequest(permitsURL); + if (!response.data) return null; + return response.data as ReadPermitDto; }; /** @@ -296,16 +379,52 @@ export const getPermits = async ({ return permits; }; -export const getPermitTransaction = async (transactionOrderNumber: string) => { +export const getPermitHistory = async (originalPermitId?: string) => { try { + if (!originalPermitId) return []; + const response = await httpGETRequest( - `${PAYMENT_API}/${transactionOrderNumber}/permit` + `${VEHICLES_URL}/permits/history?originalId=${originalPermitId}` ); + if (response.status === 200) { - return response.data as PermitTransaction; + return response.data as PermitHistory[]; + } + return []; + } catch (err) { + return []; + } +}; + +/** + * Void or revoke a permit. + * @param permitId Id of the permit to void or revoke. + * @param voidData Void or revoke data to be sent to backend. + * @returns Response data containing successfully voided/revoked permit ids, as well as failed ones. + */ +export const voidPermit = async (voidPermitParams: { + permitId: string, + voidData: VoidPermitRequestData | RevokePermitRequestData +}) => { + const { permitId, voidData } = voidPermitParams; + try { + const response = await httpPOSTRequest( + `${PERMITS_API.BASE}/${permitId}/void`, + replaceEmptyValuesWithNull(voidData) + ); + + if (response.status === 201) { + return response.data as VoidPermitResponseData; } - return undefined; + return { + success: [], + failure: [permitId], + }; } catch (err) { - return undefined; + console.error(err); + return { + success: [], + failure: [permitId], + }; } }; diff --git a/frontend/src/features/permits/components/dashboard/ApplicationDashboard.tsx b/frontend/src/features/permits/components/dashboard/ApplicationDashboard.tsx index f0d316f8e..e8384559a 100644 --- a/frontend/src/features/permits/components/dashboard/ApplicationDashboard.tsx +++ b/frontend/src/features/permits/components/dashboard/ApplicationDashboard.tsx @@ -1,4 +1,7 @@ import { Box } from "@mui/material"; +import { AxiosError } from "axios"; +import { useParams } from "react-router-dom"; + import "../../../../common/components/dashboard/Dashboard.scss"; import { Banner } from "../../../../common/components/dashboard/Banner"; import { TermOversizeForm } from "../../pages/TermOversize/TermOversizeForm"; @@ -8,17 +11,28 @@ import { TermOversizeReview } from "../../pages/TermOversize/TermOversizeReview" import { useMultiStepForm } from "../../hooks/useMultiStepForm"; import { useCompanyInfoQuery } from "../../../manageProfile/apiManager/hooks"; import { Loading } from "../../../../common/pages/Loading"; -import { AxiosError } from "axios"; import { Unauthorized } from "../../../../common/pages/Unauthorized"; import { ErrorFallback } from "../../../../common/pages/ErrorFallback"; -import { useParams } from "react-router-dom"; import { useApplicationDetailsQuery } from "../../hooks/hooks"; -export enum ApplicationStep { - Form = "Form", - Review = "Review", - Pay = "Pay", -} +export const APPLICATION_STEPS = { + Form: "Form", + Review: "Review", + Pay: "Pay", +} as const; + +export type ApplicationStep = typeof APPLICATION_STEPS[keyof typeof APPLICATION_STEPS]; + +const displayHeaderText = (stepKey: ApplicationStep) => { + switch (stepKey) { + case APPLICATION_STEPS.Form: + return "Permit Application"; + case APPLICATION_STEPS.Review: + return "Review and Confirm Details"; + case APPLICATION_STEPS.Pay: + return "Pay for Permit"; + } +}; export const ApplicationDashboard = () => { const companyInfoQuery = useCompanyInfoQuery(); @@ -32,35 +46,17 @@ export const ApplicationDashboard = () => { } = useApplicationDetailsQuery(applicationNumber); const { - //steps, currentStepIndex, step, - //isFirstStep, - //isLastStep, back, next, goTo, } = useMultiStepForm([ - - , - , - , - + , + , + , ]); - const displayHeaderText = () => { - switch (step.key) { - case ApplicationStep.Form: - return "Permit Application"; - case ApplicationStep.Review: - return "Review and Confirm Details"; - case ApplicationStep.Pay: - return "Pay for Permit"; - default: - return ""; - } - }; - if (companyInfoQuery.isLoading) { return ; } @@ -100,7 +96,10 @@ export const ApplicationDashboard = () => { borderColor: "divider", }} > - + {step} diff --git a/frontend/src/features/permits/components/dashboard/ManageApplicationDashboard.tsx b/frontend/src/features/permits/components/dashboard/ManageApplicationDashboard.tsx index 8f0a1cbf3..16dd8fb70 100644 --- a/frontend/src/features/permits/components/dashboard/ManageApplicationDashboard.tsx +++ b/frontend/src/features/permits/components/dashboard/ManageApplicationDashboard.tsx @@ -7,7 +7,7 @@ import { Loading } from "../../../../common/pages/Loading"; import { ErrorFallback } from "../../../../common/pages/ErrorFallback"; import { List } from "../list/List"; import { getApplicationsInProgress } from "../../../../features/permits/apiManager/permitsAPI"; -import { StartApplicationButton } from "../../../../features/permits/pages/TermOversize/form/VehicleDetails/customFields/StartApplicationButton"; +import { StartApplicationButton } from "../../pages/TermOversize/components/form/VehicleDetails/customFields/StartApplicationButton"; import { ActivePermitList } from "../permit-list/ActivePermitList"; import { ExpiredPermitList } from "../permit-list/ExpiredPermitList"; import { FIVE_MINUTES } from "../../../../common/constants/constants"; diff --git a/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getActiveApplication.ts b/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getActiveApplication.ts index 7b1169017..36cf2a254 100644 --- a/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getActiveApplication.ts +++ b/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getActiveApplication.ts @@ -7,6 +7,7 @@ import { getDefaultUserDetails } from "./getUserDetails"; import { getDefaultPowerUnits } from "./getVehicleInfo"; import { getDefaultCompanyInfo } from "./getCompanyInfo"; import { TROS_COMMODITIES } from "../../../../../constants/termOversizeConstants"; +import { PERMIT_TYPES } from "../../../../../types/PermitType"; const activeApplicationSource = factory({ application: { @@ -132,7 +133,7 @@ export const getDefaultApplication = () => { return { companyId, userGuid: "AB1CD2EFAB34567CD89012E345FA678B", - permitType: "TROS", + permitType: PERMIT_TYPES.TROS, permitData: { startDate, permitDuration: 30, diff --git a/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getUserDetails.ts b/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getUserDetails.ts index 39d8080d7..acabcdeb2 100644 --- a/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getUserDetails.ts +++ b/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getUserDetails.ts @@ -1,3 +1,5 @@ +import { USER_AUTH_GROUP } from "../../../../../../manageProfile/types/userManagement.d"; + export const getDefaultUserDetails = () => ({ companyId: 74, userDetails: { @@ -10,7 +12,7 @@ export const getDefaultUserDetails = () => ({ phone2Extension: "234", email: "my.company@mycompany.co", fax: "604-123-4569", - userAuthGroup: "ORGADMIN" + userAuthGroup: USER_AUTH_GROUP.ORGADMIN, }, }); diff --git a/frontend/src/features/permits/components/feeSummary/FeeSummary.scss b/frontend/src/features/permits/components/feeSummary/FeeSummary.scss new file mode 100644 index 000000000..f97e48cb1 --- /dev/null +++ b/frontend/src/features/permits/components/feeSummary/FeeSummary.scss @@ -0,0 +1,41 @@ +@import "../../../../themes/orbcStyles.scss"; + +.fee-summary { + display: flex; + flex-direction: column; + background-color: $banner-grey; + color: $bc-primary-blue; + padding: 1em; + margin-top: 2em; + + &__title { + font-weight: 600; + font-size: 1.2rem; + } + + &__table { + display: flex; + flex-direction: column; + + .table-row { + display: flex; + flex-direction: row; + justify-content: space-between; + font-size: 1rem; + border-bottom: 1px solid $bc-border-grey; + padding: 1em 0; + + &--header { + font-weight: 600; + padding: 2em 0 1em 0; + } + + &--total { + border-bottom: none; + font-weight: 600; + font-size: 1.2rem; + padding: 1em 0 0 0; + } + } + } +} \ No newline at end of file diff --git a/frontend/src/features/permits/components/feeSummary/FeeSummary.tsx b/frontend/src/features/permits/components/feeSummary/FeeSummary.tsx new file mode 100644 index 000000000..07a2944cf --- /dev/null +++ b/frontend/src/features/permits/components/feeSummary/FeeSummary.tsx @@ -0,0 +1,57 @@ +import { feeSummaryDisplayText } from "../../helpers/feeSummary"; +import { permitTypeDisplayText } from "../../types/PermitType"; +import "./FeeSummary.scss"; + +export const FeeSummary = ({ + permitType, + feeSummary, + permitDuration, +}: { + permitType?: string; + feeSummary?: string; + permitDuration?: number; +}) => { + const feeDisplayText = feeSummaryDisplayText( + feeSummary, + permitDuration + ); + + return ( +
+
+ Fee Summary +
+
+
+
Description
+
Amount
+
+
+
+ {permitTypeDisplayText(permitType)} +
+
+ {feeDisplayText} +
+
+
+
+ Total (CAD) +
+
+ {feeDisplayText} +
+
+
+
+ ); +}; diff --git a/frontend/src/features/permits/components/form/ApplicationDetails.tsx b/frontend/src/features/permits/components/form/ApplicationDetails.tsx index 9c60508de..1484d302e 100644 --- a/frontend/src/features/permits/components/form/ApplicationDetails.tsx +++ b/frontend/src/features/permits/components/form/ApplicationDetails.tsx @@ -2,30 +2,35 @@ import { Box, Typography } from "@mui/material"; import { Dayjs } from "dayjs"; import { CompanyBanner } from "../../../../common/components/banners/CompanyBanner"; -import { useCompanyInfoQuery } from "../../../manageProfile/apiManager/hooks"; import { DATE_FORMATS, dayjsToLocalStr } from "../../../../common/helpers/formatDate"; import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../../common/helpers/util"; import { CompanyInformation } from "./CompanyInformation"; import "./ApplicationDetails.scss"; -import { permitTypeDisplayText } from "../../helpers/mappers"; -import { PermitType } from "../../types/application"; +import { permitTypeDisplayText } from "../../types/PermitType"; +import { CompanyProfile } from "../../../manageProfile/types/manageProfile"; export const ApplicationDetails = ({ permitType, - applicationNumber, + infoNumberType = "application", + infoNumber, createdDateTime, updatedDateTime, + companyInfo, }: { - permitType?: string, - applicationNumber?: string, - createdDateTime?: Dayjs, - updatedDateTime?: Dayjs, + permitType?: string; + infoNumberType?: "application" | "permit"; + infoNumber?: string; + createdDateTime?: Dayjs; + updatedDateTime?: Dayjs; + companyInfo?: CompanyProfile; }) => { - const companyInfoQuery = useCompanyInfoQuery(); const applicationName = permitTypeDisplayText( - getDefaultRequiredVal("", permitType) as PermitType + getDefaultRequiredVal("", permitType) ); + const validInfoNumber = () => infoNumber && infoNumber !== ""; + const isPermitNumber = () => infoNumberType === "permit"; + return ( <>
@@ -36,7 +41,7 @@ export const ApplicationDetails = ({ > {applicationName} - {(applicationNumber && applicationNumber !== "") ? ( + {validInfoNumber() ? ( - Application #: + {isPermitNumber() ? "Permit #:" : "Application #:"} - {applicationNumber} + {infoNumber} @@ -101,8 +106,11 @@ export const ApplicationDetails = ({ )}
- - + + ); }; diff --git a/frontend/src/features/permits/components/form/tests/ApplicationDetails.test.tsx b/frontend/src/features/permits/components/form/tests/ApplicationDetails.test.tsx index 700cab16f..2e747f83d 100644 --- a/frontend/src/features/permits/components/form/tests/ApplicationDetails.test.tsx +++ b/frontend/src/features/permits/components/form/tests/ApplicationDetails.test.tsx @@ -1,4 +1,5 @@ import { DATE_FORMATS, dayjsToLocalStr } from "../../../../../common/helpers/formatDate"; +import { permitTypeDisplayText } from "../../../types/PermitType"; import { closeMockServer, createdAt, @@ -9,13 +10,13 @@ import { resetMockServer, updatedAt, } from "./helpers/ApplicationDetails/prepare"; + import { applicationNumber, createdDate, title, updatedDate, } from "./helpers/ApplicationDetails/access"; -import { permitTypeDisplayText } from "../../../helpers/mappers"; beforeAll(() => { listenToMockServer(); diff --git a/frontend/src/features/permits/components/form/tests/helpers/ApplicationDetails/prepare.tsx b/frontend/src/features/permits/components/form/tests/helpers/ApplicationDetails/prepare.tsx index f86dcf66b..653e05a8d 100644 --- a/frontend/src/features/permits/components/form/tests/helpers/ApplicationDetails/prepare.tsx +++ b/frontend/src/features/permits/components/form/tests/helpers/ApplicationDetails/prepare.tsx @@ -5,6 +5,7 @@ import { MANAGE_PROFILE_API } from "../../../../../../manageProfile/apiManager/e import { renderWithClient } from "../../../../../../../common/helpers/testHelper"; import { ApplicationDetails } from "../../../ApplicationDetails"; import { Dayjs } from "dayjs"; +import { PERMIT_TYPES } from "../../../../../types/PermitType"; export const defaultCompanyInfo = { companyId: 74, @@ -36,7 +37,7 @@ export const province = "British Columbia"; export const createdAt = utcToLocalDayjs("2023-06-14T09:00:00.000Z"); export const updatedAt = utcToLocalDayjs("2023-06-15T13:00:00.000Z"); export const defaultApplicationNumber = "ABC-123456"; -export const permitType = "TROS"; +export const permitType = PERMIT_TYPES.TROS; const server = setupServer( // Mock get company info @@ -68,9 +69,11 @@ export const renderTestComponent = ( return renderWithClient( ); }; diff --git a/frontend/src/features/permits/components/permit-list/Columns.tsx b/frontend/src/features/permits/components/permit-list/Columns.tsx index 3f9954e3a..af8ab0b22 100644 --- a/frontend/src/features/permits/components/permit-list/Columns.tsx +++ b/frontend/src/features/permits/components/permit-list/Columns.tsx @@ -4,13 +4,6 @@ import { viewPermitPdf } from "../../helpers/permitPDFHelper"; import { ReadPermitDto } from "../../types/permit"; import { PermitChip } from "./PermitChip"; -/** - * A boolean indicating if a small badge has to be displayed beside the Permit Number. - */ -const shouldShowPermitChip = (permitStatus: string) => { - return permitStatus === "VOIDED" || permitStatus === "REVOKED"; -}; - /** * The column definition for Permits. */ @@ -31,9 +24,7 @@ export const PermitsColumnDefinition: MRT_ColumnDef[] = [ > {props.cell.getValue()} - {shouldShowPermitChip(props.row.original.permitStatus) && ( - - )} + ); }, diff --git a/frontend/src/features/permits/components/permit-list/PermitChip.tsx b/frontend/src/features/permits/components/permit-list/PermitChip.tsx index 49e8aea58..8250a2dd4 100644 --- a/frontend/src/features/permits/components/permit-list/PermitChip.tsx +++ b/frontend/src/features/permits/components/permit-list/PermitChip.tsx @@ -1,71 +1,95 @@ import { OnRouteBCChip } from "../../../../common/components/table/OnRouteBCChip"; import { BC_COLOURS } from "../../../../themes/bcGovStyles"; - -export type EXPIRED_PERMIT_STATUS = "VOIDED" | "REVOKED" | "EXPIRED"; +import { + PERMIT_EXPIRED, + PERMIT_STATUSES, + isPermitInactive, +} from "../../types/PermitStatus"; /** * Returns the colors associated with the badge. - * NOTE: If the permit status is one of VOIDED or REVOKED, a small badge has to be displayed + * NOTE: If the permit is inactive or expired, a small badge has to be displayed * beside the permit number. - * @param permitStatus One of "VOIDED" | "REVOKED" | "EXPIRED" + * @param permitStatus string representing the permit status * @returns An object containing the text and background colors */ const getColors = ( - permitStatus: EXPIRED_PERMIT_STATUS -): { background: string; color: string } => { + permitStatus?: string +): { background: string; color: string } | undefined => { switch (permitStatus) { - case "VOIDED": + case PERMIT_STATUSES.VOIDED: return { background: BC_COLOURS.focus_blue, color: BC_COLOURS.white, }; - case "REVOKED": + case PERMIT_STATUSES.REVOKED: return { background: BC_COLOURS.bc_messages_red_text, color: BC_COLOURS.white, }; - case "EXPIRED": + case PERMIT_STATUSES.SUPERSEDED: + return { + background: BC_COLOURS.bc_border_grey, + color: BC_COLOURS.bc_black, + }; + case PERMIT_EXPIRED: return { background: BC_COLOURS.bc_messages_red_background, color: BC_COLOURS.bc_messages_red_text, }; + default: + return undefined; } }; /** - * Returns the text corresponding to a - * @param permitStatus One of "VOIDED" | "REVOKED" | "EXPIRED" - * @returns + * Returns the text corresponding to the status of a permit. + * @param permitStatus string representing the permit status + * @returns Display text string corresponding to permit status */ -const getTextForBadge = (permitStatus: EXPIRED_PERMIT_STATUS): string => { +const getTextForBadge = (permitStatus?: string): string => { switch (permitStatus) { - case "VOIDED": + case PERMIT_STATUSES.VOIDED: return "Void"; - case "REVOKED": + case PERMIT_STATUSES.REVOKED: return "Revoked"; - case "EXPIRED": + case PERMIT_STATUSES.SUPERSEDED: + return "Superseded"; + case PERMIT_EXPIRED: return "Expired"; default: return ""; } }; +/** + * A boolean indicating if a small badge has to be displayed beside the Permit Number. + */ +const shouldShowPermitChip = (permitStatus?: string) => { + return isPermitInactive(permitStatus) || permitStatus === PERMIT_EXPIRED; +}; + /** * A simple chip component to be displayed beside the permit number. */ export const PermitChip = ({ permitStatus, }: { - permitStatus: EXPIRED_PERMIT_STATUS; + permitStatus?: string; }) => { - return ( + if (!shouldShowPermitChip(permitStatus)) { + return null; + } + + const chipColours = getColors(permitStatus); + return chipColours ? ( <> - ); + ) : null; }; PermitChip.displayName = "PermitChip"; diff --git a/frontend/src/features/permits/components/progressBar/ProgressBar.tsx b/frontend/src/features/permits/components/progressBar/ProgressBar.tsx index 890e21fdb..a5b563131 100644 --- a/frontend/src/features/permits/components/progressBar/ProgressBar.tsx +++ b/frontend/src/features/permits/components/progressBar/ProgressBar.tsx @@ -5,10 +5,15 @@ import { useContext } from "react"; import { useNavigate } from "react-router-dom"; import { BC_COLOURS } from "../../../../themes/bcGovStyles"; -import { ApplicationStep } from "../dashboard/ApplicationDashboard"; +import { APPLICATION_STEPS } from "../dashboard/ApplicationDashboard"; import { ApplicationContext } from "../../context/ApplicationContext"; export const ProgressBar = () => { + const applicationSteps = Object.values(APPLICATION_STEPS); + const indexForm = applicationSteps.indexOf(APPLICATION_STEPS.Form); + const indexReview = applicationSteps.indexOf(APPLICATION_STEPS.Review); + const indexPay = applicationSteps.indexOf(APPLICATION_STEPS.Pay); + const { currentStepIndex, goTo } = useContext(ApplicationContext); const navigate = useNavigate(); @@ -17,12 +22,6 @@ export const ProgressBar = () => { navigate("../"); }; - const indexForm = Object.keys(ApplicationStep).indexOf(ApplicationStep.Form); - const indexReview = Object.keys(ApplicationStep).indexOf( - ApplicationStep.Review - ); - const indexPay = Object.keys(ApplicationStep).indexOf(ApplicationStep.Pay); - return ( { + return duration; +}; + +/** + * Gets full display text for fee summary. + * @param feeSummary fee summary field for a permit (if exists) + * @param duration duration field for a permit (if exists) + * @returns display text for the fee summary (currency amount to 2 decimal places) + */ +export const feeSummaryDisplayText = (feeSummary?: string | null, duration?: number | null) => { + const feeFromSummary = applyWhenNotNullable((numericStr) => Number(numericStr).toFixed(2), feeSummary); + const feeFromDuration = applyWhenNotNullable((num) => calculateFeeByDuration(num).toFixed(2), duration); + const fee = getDefaultRequiredVal("0.00", feeFromSummary, feeFromDuration); + const numericFee = Number(fee); + return numericFee >= 0 ? `$${fee}` : `-$${(numericFee * -1).toFixed(2)}`; +}; + +/** + * Determines whether or not the transaction type of a transaction was a refund. + * @param transactionType Transaction type of a transaction + * @returns whether or not the transaction type was a refund + */ +export const isTransactionTypeRefund = (transactionType: TransactionType) => { + return transactionType === TRANSACTION_TYPES.R; +}; + +/** + * Calculates the net amount from the history of transactions for a permit. + * A positive amount represents the net amount that was paid for a permit, and + * a negative amount represents the net amount that was refunded for a permit, and + * a 0 amount means that the permit is completely paid with no amount outstanding. + * @param permitHistory List of history objects that make up the history of a permit + * @returns total net amount resulting from the history of transactions for the permit + */ +export const calculateNetAmount = (permitHistory: PermitHistory[]) => { + return permitHistory.map(permit => isTransactionTypeRefund(permit.transactionTypeId) + ? -1 * permit.transactionAmount + : permit.transactionAmount).reduce((prev, curr) => prev + curr, 0); +}; diff --git a/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts b/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts index 2575f7c22..8cb70a7c3 100644 --- a/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts +++ b/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts @@ -2,19 +2,23 @@ import dayjs from "dayjs"; import { getUserGuidFromSession } from "../../../common/apiManager/httpRequestHandler"; import { BCeIDUserDetailContext } from "../../../common/authentication/OnRouteBCContext"; +import { TROS_COMMODITIES } from "../constants/termOversizeConstants"; +import { now } from "../../../common/helpers/formatDate"; +import { Address } from "../../manageProfile/types/manageProfile"; +import { PERMIT_STATUSES } from "../types/PermitStatus"; +import { calculateFeeByDuration } from "./feeSummary"; +import { PERMIT_TYPES } from "../types/PermitType"; import { applyWhenNotNullable, getDefaultRequiredVal, } from "../../../common/helpers/util"; + import { Application, ContactDetails, MailingAddress, VehicleDetails, } from "../types/application"; -import { TROS_COMMODITIES } from "../constants/termOversizeConstants"; -import { now } from "../../../common/helpers/formatDate"; -import { Address } from "../../manageProfile/types/manageProfile"; /** * Get default values for contact details, or populate with existing contact details and/or user details @@ -98,6 +102,14 @@ export const getDefaultVehicleDetails = (vehicleDetails?: VehicleDetails) => ({ saveVehicle: getDefaultRequiredVal(false, vehicleDetails?.saveVehicle), }); +export const getDurationOrDefault = (applicationData?: Application): number => { + return applyWhenNotNullable( + (duration) => +duration, + applicationData?.permitData?.permitDuration, + 30 + ); +}; + /** * Gets default values for the application data, or populate with values from existing application data and company id/user details. * @param applicationData existing application data, if any @@ -118,9 +130,9 @@ export const getDefaultValues = ( userGuid: getUserGuidFromSession(), permitId: getDefaultRequiredVal("", applicationData?.permitId), permitNumber: getDefaultRequiredVal("", applicationData?.permitNumber), - permitType: getDefaultRequiredVal("TROS", applicationData?.permitType), + permitType: getDefaultRequiredVal(PERMIT_TYPES.TROS, applicationData?.permitType), permitStatus: getDefaultRequiredVal( - "IN_PROGRESS", + PERMIT_STATUSES.IN_PROGRESS, applicationData?.permitStatus ), createdDateTime: applyWhenNotNullable( @@ -153,11 +165,7 @@ export const getDefaultValues = ( applicationData?.permitData?.startDate, now() ), - permitDuration: applyWhenNotNullable( - (duration) => +duration, - applicationData?.permitData?.permitDuration, - 30 - ), + permitDuration: getDurationOrDefault(applicationData), expiryDate: applyWhenNotNullable( (date) => dayjs(date), applicationData?.permitData?.expiryDate, @@ -184,7 +192,7 @@ export const getDefaultValues = ( applicationData?.permitData?.vehicleDetails ), feeSummary: getDefaultRequiredVal( - "30", + `${calculateFeeByDuration(getDurationOrDefault(applicationData))}`, applicationData?.permitData?.feeSummary ), }, diff --git a/frontend/src/features/permits/helpers/mappers.ts b/frontend/src/features/permits/helpers/mappers.ts index 927eb9b41..2a8ae058f 100644 --- a/frontend/src/features/permits/helpers/mappers.ts +++ b/frontend/src/features/permits/helpers/mappers.ts @@ -7,8 +7,21 @@ import { VehicleTypes, VehicleTypesAsString, } from "../../manageVehicles/types/managevehicles"; -import { Application, ApplicationRequestData, ApplicationResponse, PermitType } from "../types/application"; -import { DATE_FORMATS, dayjsToLocalStr, dayjsToUtcStr, now, toLocalDayjs, utcToLocalDayjs } from "../../../common/helpers/formatDate"; + +import { + Application, + ApplicationRequestData, + ApplicationResponse, +} from "../types/application"; + +import { + DATE_FORMATS, + dayjsToLocalStr, + dayjsToUtcStr, + now, + toLocalDayjs, + utcToLocalDayjs, +} from "../../../common/helpers/formatDate"; /** * This helper function is used to get the vehicle object that matches the vin prop @@ -128,28 +141,3 @@ export const vehicleTypeDisplayText = (vehicleType: VehicleTypesAsString) => { } return "Power Unit"; }; - -/** - * Gets display text for permit type. - * @param permitType Permit type (eg. TROS, STOS, etc) - * @returns display text for the permit type - */ -export const permitTypeDisplayText = (permitType: PermitType) => { - switch (permitType) { - case "TROS": - return "Oversize: Term"; - case "STOS": - return "Oversize: Single Trip"; - default: - return ""; - } -}; - -/** - * Gets display text for fee summary. - * @param applicationData Application Data - * @returns display text for the fee summary - */ -export const feeSummaryDisplayText = (applicationData: Application | undefined) => { - return Number(applicationData?.permitData?.permitDuration); -}; diff --git a/frontend/src/features/permits/helpers/payment.ts b/frontend/src/features/permits/helpers/payment.ts index 14adffebc..6b58a93b4 100644 --- a/frontend/src/features/permits/helpers/payment.ts +++ b/frontend/src/features/permits/helpers/payment.ts @@ -1,4 +1,11 @@ import { MotiPaymentDetails } from "../types/payment"; +import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../common/helpers/util"; +import { + BAMBORA_PAYMENT_METHODS, + BamboraPaymentMethod, + CARD_TYPES, + CardType, +} from "../types/PaymentMethod"; /** * Extracts MotiPaymentDetails from the query parameters of a URL. @@ -9,34 +16,34 @@ import { MotiPaymentDetails } from "../types/payment"; export const getMotiPaymentDetails = (params: URLSearchParams): MotiPaymentDetails => { // Extract the query parameters and assign them to the corresponding properties of MotiPaymentDetails const motiPaymentDetails: MotiPaymentDetails = { - authCode: params.get("authCode") ?? "", - avsAddrMatch: params.get("avsAddrMatch") ?? "", - avsId: params.get("avsId") ?? "", - avsMessage: params.get("avsMessage") ?? "", - avsPostalMatch: params.get("avsPostalMatch") ?? "", - avsProcessed: params.get("avsProcessed") ?? "", - avsResult: params.get("avsResult") ?? "", - cardType: params.get("cardType") ?? "", - cvdId: Number(params.get("cvdId") ?? ""), - trnApproved: Number(params.get("trnApproved") ?? ""), - messageId: params.get("messageId") ?? "", - messageText: params.get("messageText") ?? "", - paymentMethod: params.get("paymentMethod") ?? "", - ref1: params.get("ref1") ?? "", - ref2: params.get("ref2") ?? "", - ref3: params.get("ref3") ?? "", - ref4: params.get("ref4") ?? "", - ref5: params.get("ref5") ?? "", - responseType: params.get("responseType") ?? "", - trnAmount: Number(params.get("trnAmount") ?? ""), - trnCustomerName: params.get("trnCustomerName") ?? "", - trnDate: params.get("trnDate") ?? "", - trnEmailAddress: params.get("trnEmailAddress") ?? "", - trnId: params.get("trnId") ?? "", - trnLanguage: params.get("trnLanguage") ?? "", - trnOrderNumber: params.get("trnOrderNumber") ?? "", - trnPhoneNumber: params.get("trnPhoneNumber") ?? "", - trnType: params.get("trnType") ?? "", + authCode: getDefaultRequiredVal("", params.get("authCode")), + avsAddrMatch: getDefaultRequiredVal("", params.get("avsAddrMatch")), + avsId: getDefaultRequiredVal("", params.get("avsId")), + avsMessage: getDefaultRequiredVal("", params.get("avsMessage")), + avsPostalMatch: getDefaultRequiredVal("", params.get("avsPostalMatch")), + avsProcessed: getDefaultRequiredVal("", params.get("avsProcessed")), + avsResult: getDefaultRequiredVal("", params.get("avsResult")), + cardType: getDefaultRequiredVal(CARD_TYPES.VI, params.get("cardType")) as CardType, + cvdId: applyWhenNotNullable((cvdId) => Number(cvdId), params.get("cvdId"), 0), + trnApproved: applyWhenNotNullable((trnApproved) => Number(trnApproved), params.get("trnApproved"), 0), + messageId: getDefaultRequiredVal("", params.get("messageId")), + messageText: getDefaultRequiredVal("", params.get("messageText")), + paymentMethod: getDefaultRequiredVal(BAMBORA_PAYMENT_METHODS.CC, params.get("paymentMethod")) as BamboraPaymentMethod, + ref1: getDefaultRequiredVal("", params.get("ref1")), + ref2: getDefaultRequiredVal("", params.get("ref2")), + ref3: getDefaultRequiredVal("", params.get("ref3")), + ref4: getDefaultRequiredVal("", params.get("ref4")), + ref5: getDefaultRequiredVal("", params.get("ref5")), + responseType: getDefaultRequiredVal("", params.get("responseType")), + trnAmount: applyWhenNotNullable((trnAmount) => Number(trnAmount), params.get("trnAmount"), 0), + trnCustomerName: getDefaultRequiredVal("", params.get("trnCustomerName")), + trnDate: getDefaultRequiredVal("", params.get("trnDate")), + trnEmailAddress: getDefaultRequiredVal("", params.get("trnEmailAddress")), + trnId: getDefaultRequiredVal("", params.get("trnId")), + trnLanguage: getDefaultRequiredVal("", params.get("trnLanguage")), + trnOrderNumber: getDefaultRequiredVal("", params.get("trnOrderNumber")), + trnPhoneNumber: getDefaultRequiredVal("", params.get("trnPhoneNumber")), + trnType: getDefaultRequiredVal("", params.get("trnType")), }; return motiPaymentDetails; diff --git a/frontend/src/features/permits/hooks/hooks.ts b/frontend/src/features/permits/hooks/hooks.ts index 101772888..2ae3b4617 100644 --- a/frontend/src/features/permits/hooks/hooks.ts +++ b/frontend/src/features/permits/hooks/hooks.ts @@ -1,16 +1,26 @@ import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query"; +import { useState } from "react"; +import { AxiosError } from "axios"; + +import { Application } from "../types/application"; +import { mapApplicationResponseToApplication } from "../helpers/mappers"; +import { IssuePermitsResponse, ReadPermitDto } from "../types/permit"; +import { PermitHistory } from "../types/PermitHistory"; +import { + CompleteTransactionRequestData, + StartTransactionResponseData, +} from "../types/payment"; + import { - getApplicationInProgressById, - getPermitTransaction, - postTransaction, + getApplicationByPermitId, + getPermit, + getPermitHistory, + completeTransaction, submitTermOversize, updateTermOversize, + startTransaction, + issuePermits, } from "../apiManager/permitsAPI"; -import { Application } from "../types/application"; -import { useState } from "react"; -import { mapApplicationResponseToApplication } from "../helpers/mappers"; -import { PermitTransaction } from "../types/payment"; -import { AxiosError } from "axios"; /** * A custom react query mutation hook that saves the application data to the backend API @@ -54,7 +64,7 @@ export const useApplicationDetailsQuery = (permitId?: string) => { const query = useQuery({ queryKey: ["termOversize"], - queryFn: () => getApplicationInProgressById(permitId), + queryFn: () => getApplicationByPermitId(permitId), retry: false, refetchOnMount: "always", // always fetch when component is mounted (ApplicationDashboard page) refetchOnWindowFocus: false, // prevent unnecessary multiple queries on page showing up in foreground @@ -79,7 +89,74 @@ export const useApplicationDetailsQuery = (permitId?: string) => { }; }; -export const usePostTransaction = ( +/** + * A custom react query hook that get permit details from the backend API + * The hook gets permit details by its permit id + * @param permitId permit id for the permit + * @returns permit details, or error if failed + */ +export const usePermitDetailsQuery = (permitId?: string) => { + const [permit, setPermit] = useState(undefined); + + const invalidPermitId = !permitId; + const query = useQuery({ + queryKey: ["permit"], + queryFn: () => getPermit(permitId), + enabled: !invalidPermitId, + retry: false, + refetchOnMount: "always", + refetchOnWindowFocus: false, // prevent unnecessary multiple queries on page showing up in foreground + onSuccess: (permitDetails) => { + setPermit(permitDetails); + }, + }); + + return { + query, + permit, + setPermit, + }; +}; + +/** + * Custom hook that starts a transaction. + * @returns The mutation object, as well as the transaction that was started (if there is one, or undefined if there's an error). + */ +export const useStartTransaction = () => { + const [transaction, setTransaction] = useState(undefined); + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: startTransaction, + retry: false, + onSuccess: (transactionData) => { + queryClient.invalidateQueries(["transaction"]); + queryClient.setQueryData(["transaction"], transactionData); + setTransaction(transactionData); + }, + onError: (err: unknown) => { + console.error(err); + setTransaction(undefined); + }, + }); + + return { + mutation, + transaction, + }; +}; + +/** + * A custom hook that completes the transaction. + * @param transactionId The transaction id of the transaction to complete + * @param transactionDetails Details for the transaction to complete + * @param messageText Message text that indicates the result of the transaction + * @param paymentStatus Payment status signifying the result of the transaction (1 - success, 0 - failed) + * @returns The mutation object, whether or not payment was approved, and the message to display + */ +export const useCompleteTransaction = ( + transactionId: string, + transactionDetails: CompleteTransactionRequestData, messageText: string, paymentStatus: number ) => { @@ -108,10 +185,10 @@ export const usePostTransaction = ( }; const mutation = useMutation({ - mutationFn: postTransaction, + mutationFn: () => completeTransaction(transactionId, transactionDetails), retry: false, onSuccess: (response) => { - if (response.status === 201) { + if (response != null) { queryClient.invalidateQueries(["transactions"]); onTransactionResult(messageText, paymentStatus === 1); } else { @@ -133,30 +210,56 @@ export const usePostTransaction = ( }; /** - * Custom hook for retrieving permit transaction data, which includes permit and transaction info - * @param transactionOrderNumber Transaction order number for the transaction - * @returns Associated permit transaction data + * A custom react query hook that get permit history from the backend API + * The hook gets permit history by its original permit id + * @param originalPermitId original permit id for the permit + * @returns list of permit history, or error if failed */ -export const usePermitTransactionQuery = ( - transactionOrderNumber: string, - paymentApproved: boolean, -) => { - const [permitTransaction, setPermitTransaction] = useState(undefined); - +export const usePermitHistoryQuery = (originalPermitId?: string) => { + const [permitHistory, setPermitHistory] = useState([]); + const query = useQuery({ - queryKey: ["permitTransaction"], - queryFn: () => getPermitTransaction(transactionOrderNumber), - enabled: paymentApproved, + queryKey: ["permitHistory"], + queryFn: () => getPermitHistory(originalPermitId), + enabled: originalPermitId != null, retry: false, refetchOnMount: "always", - refetchOnWindowFocus: false, - onSuccess: (permitTransaction) => { - setPermitTransaction(permitTransaction); + refetchOnWindowFocus: false, // prevent unnecessary multiple queries on page showing up in foreground + onSuccess: (permitHistoryData) => { + setPermitHistory(permitHistoryData); }, }); return { query, - permitTransaction, + permitHistory, + }; +}; + +/** + * Custom hook that issues the permits indicated by the application/permit ids. + * @param ids Application/permit ids for the permits to be issued. + * @returns Mutation object, and the issued results response. + */ +export const useIssuePermits = () => { + const [issueResults, setIssueResults] = useState(undefined); + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: issuePermits, + retry: false, + onSuccess: (issueResponseData) => { + queryClient.invalidateQueries(["termOversize", "permit"]); + setIssueResults(issueResponseData); + }, + onError: (err: unknown) => { + console.error(err); + setIssueResults(undefined); + }, + }); + + return { + mutation, + issueResults, }; }; diff --git a/frontend/src/features/permits/hooks/useDefaultApplicationFormData.ts b/frontend/src/features/permits/hooks/useDefaultApplicationFormData.ts index 66cfef5cd..c2a851a81 100644 --- a/frontend/src/features/permits/hooks/useDefaultApplicationFormData.ts +++ b/frontend/src/features/permits/hooks/useDefaultApplicationFormData.ts @@ -154,5 +154,6 @@ export const useDefaultApplicationFormData = (applicationData?: Application) => defaultApplicationDataValues, setDefaultApplicationDataValues, formMethods, + companyInfo: companyInfoQuery.data, }; }; diff --git a/frontend/src/features/permits/hooks/usePermitVehicleManagement.ts b/frontend/src/features/permits/hooks/usePermitVehicleManagement.ts new file mode 100644 index 000000000..05f273b12 --- /dev/null +++ b/frontend/src/features/permits/hooks/usePermitVehicleManagement.ts @@ -0,0 +1,120 @@ +import { getDefaultRequiredVal } from "../../../common/helpers/util"; +import { VehicleDetails } from "../types/application.d"; +import { PowerUnit, Trailer } from "../../manageVehicles/types/managevehicles.d"; +import { mapVinToVehicleObject } from "../helpers/mappers"; +import { + useAddPowerUnitMutation, + useAddTrailerMutation, + usePowerUnitTypesQuery, + useTrailerTypesQuery, + useUpdatePowerUnitMutation, + useUpdateTrailerMutation, + useVehiclesQuery, +} from "../../manageVehicles/apiManager/hooks"; + +export const usePermitVehicleManagement = () => { + // Mutations used to add/update vehicle details + const addPowerUnitMutation = useAddPowerUnitMutation(); + const updatePowerUnitMutation = useUpdatePowerUnitMutation(); + const addTrailerMutation = useAddTrailerMutation(); + const updateTrailerMutation = useUpdateTrailerMutation(); + + // Queries used to populate select options for vehicle details + const allVehiclesQuery = useVehiclesQuery(); + const powerUnitTypesQuery = usePowerUnitTypesQuery(); + const trailerTypesQuery = useTrailerTypesQuery(); + + // Vehicle details that have been fetched by vehicle details queries + const fetchedVehicles = getDefaultRequiredVal([], allVehiclesQuery.data); + const fetchedPowerUnitTypes = getDefaultRequiredVal([], powerUnitTypesQuery.data); + const fetchedTrailerTypes = getDefaultRequiredVal([], trailerTypesQuery.data); + + const handleSaveVehicle = (vehicleData?: VehicleDetails) => { + // Check if the "add/update vehicle" checkbox was checked by the user + if (!vehicleData?.saveVehicle) return; + + // Get the vehicle info from the form + const vehicle = vehicleData; + + // Check if the vehicle that is to be saved was created from an existing vehicle + const existingVehicle = mapVinToVehicleObject( + fetchedVehicles, + vehicle.vin + ); + + const transformByVehicleType = ( + vehicleFormData: VehicleDetails, + existingVehicle?: PowerUnit | Trailer + ): PowerUnit | Trailer => { + const defaultPowerUnit: PowerUnit = { + powerUnitId: "", + unitNumber: "", + vin: vehicleFormData.vin, + plate: vehicleFormData.plate, + make: vehicleFormData.make, + year: vehicleFormData.year, + countryCode: vehicleFormData.countryCode, + provinceCode: vehicleFormData.provinceCode, + powerUnitTypeCode: vehicleFormData.vehicleSubType, + }; + + const defaultTrailer: Trailer = { + trailerId: "", + unitNumber: "", + vin: vehicleFormData.vin, + plate: vehicleFormData.plate, + make: vehicleFormData.make, + year: vehicleFormData.year, + countryCode: vehicleFormData.countryCode, + provinceCode: vehicleFormData.provinceCode, + trailerTypeCode: vehicleFormData.vehicleSubType, + }; + + switch (vehicleFormData.vehicleType) { + case "trailer": + return { + ...defaultTrailer, + trailerId: getDefaultRequiredVal("", (existingVehicle as Trailer)?.trailerId), + unitNumber: getDefaultRequiredVal("", existingVehicle?.unitNumber), + } as Trailer; + case "powerUnit": + default: + return { + ...defaultPowerUnit, + unitNumber: getDefaultRequiredVal("", existingVehicle?.unitNumber), + powerUnitId: getDefaultRequiredVal("", (existingVehicle as PowerUnit)?.powerUnitId), + } as PowerUnit; + } + }; + + // If the vehicle type is a power unit then create a power unit object + if (vehicle.vehicleType === "powerUnit") { + const powerUnit = transformByVehicleType(vehicle, existingVehicle) as PowerUnit; + + // Either send a PUT or POST request based on powerUnitID + if (powerUnit.powerUnitId) { + updatePowerUnitMutation.mutate({ + powerUnit, + powerUnitId: powerUnit.powerUnitId, + }); + } else { + addPowerUnitMutation.mutate(powerUnit); + } + } else if (vehicle.vehicleType === "trailer") { + const trailer = transformByVehicleType(vehicle, existingVehicle) as Trailer; + + if (trailer.trailerId) { + updateTrailerMutation.mutate({ trailer, trailerId: trailer.trailerId }); + } else { + addTrailerMutation.mutate(trailer); + } + } + }; + + return { + handleSaveVehicle, + powerUnitTypes: fetchedPowerUnitTypes, + trailerTypes: fetchedTrailerTypes, + vehicleOptions: fetchedVehicles, + }; +}; diff --git a/frontend/src/features/permits/pages/Payment/PaymentRedirect.tsx b/frontend/src/features/permits/pages/Payment/PaymentRedirect.tsx index 01fe3b965..8b3a9b4ca 100644 --- a/frontend/src/features/permits/pages/Payment/PaymentRedirect.tsx +++ b/frontend/src/features/permits/pages/Payment/PaymentRedirect.tsx @@ -2,9 +2,14 @@ import { useEffect, useRef } from "react"; import { Navigate, useSearchParams } from "react-router-dom"; import { getMotiPaymentDetails } from "../../helpers/payment"; -import { MotiPaymentDetails, Transaction } from "../../types/payment"; +import { CompleteTransactionRequestData, MotiPaymentDetails } from "../../types/payment"; import { Loading } from "../../../../common/pages/Loading"; -import { usePostTransaction } from "../../hooks/hooks"; +import { useCompleteTransaction, useIssuePermits } from "../../hooks/hooks"; +import { getDefaultRequiredVal } from "../../../../common/helpers/util"; + +const getPermitIdsArray = (permitIds?: string | null) => { + return getDefaultRequiredVal("", permitIds).split(",").filter(id => id !== ""); +}; /** * React component that handles the payment redirect and displays the payment status. @@ -12,34 +17,60 @@ import { usePostTransaction } from "../../hooks/hooks"; * Otherwise, it displays the payment status message. */ export const PaymentRedirect = () => { - const postedTransaction = useRef(false); + const completedTransaction = useRef(false); + const issuedPermit = useRef(false); const [searchParams] = useSearchParams(); const permitIds = searchParams.get("permitIds"); - const transactionIds = searchParams.get("transactionIds"); + const transactionId = searchParams.get("transactionId"); const paymentDetails = getMotiPaymentDetails(searchParams); const transaction = mapTransactionDetails(paymentDetails); const { - mutation: postTransactionMutation, + mutation: completeTransactionMutation, paymentApproved, message, setPaymentApproved, - } = usePostTransaction( + } = useCompleteTransaction( + getDefaultRequiredVal("", transactionId), + transaction, paymentDetails.messageText, paymentDetails.trnApproved ); + const { + mutation: issuePermitsMutation, + issueResults, + } = useIssuePermits(); + + const issueFailed = () => { + if (!issueResults) return false; // since issue results might not be ready yet + return issueResults.success.length === 0 + || (issueResults.success.length === 1 && issueResults.success[0] === "") + || (issueResults.failure.length > 0 && issueResults.failure[0] !== ""); + }; + useEffect(() => { - if (postedTransaction.current === false) { + if (completedTransaction.current === false) { if (paymentDetails.trnApproved > 0) { - postTransactionMutation.mutate(transaction); - postedTransaction.current = true; + completeTransactionMutation.mutate(); + completedTransaction.current = true; } else { setPaymentApproved(false); } } }, [paymentDetails.trnApproved]); + useEffect(() => { + if (issuedPermit.current === false) { + const permitIdsArray = getPermitIdsArray(permitIds); + if (paymentApproved === true && permitIdsArray.length > 0) { + // Issue permit + issuePermitsMutation.mutate(permitIdsArray); + issuedPermit.current = true; + } + } + }, [paymentApproved, permitIds]); + if (paymentApproved === false) { return ( { ); } - if (paymentApproved === true && permitIds && transactionIds) { - const permitIdsArray = permitIds.split(",").filter(id => id !== ""); - const transactionIdsArray = transactionIds.split(",").filter(id => id !== ""); - if (permitIdsArray.length !== 1 || transactionIdsArray.length !== 1) { - return ; + if (issueResults) { + if (issueFailed()) { + const permitIssueFailedMsg = `Permit issue failed for ids ${issueResults.failure.join(",")}`; + return ( + + ); } return ( ); @@ -68,20 +103,16 @@ export const PaymentRedirect = () => { const mapTransactionDetails = ( motiResponse: MotiPaymentDetails -): Transaction => { +): CompleteTransactionRequestData => { return { - transactionType: motiResponse.trnType, - transactionOrderNumber: motiResponse.trnOrderNumber, - providerTransactionId: Number(motiResponse.trnId), - transactionAmount: Number(motiResponse.trnAmount), - approved: Number(motiResponse.trnApproved), - authCode: motiResponse.authCode, - cardType: motiResponse.cardType, - transactionDate: motiResponse.trnDate, - cvdId: Number(motiResponse.cvdId), - paymentMethod: motiResponse.paymentMethod, - paymentMethodId: 1, // TODO: change once different payment methods are implemented, currently 1 == MOTI Pay - messageId: motiResponse.messageId, - messageText: motiResponse.messageText, + pgTransactionId: motiResponse.trnId, + pgApproved: Number(motiResponse.trnApproved), + pgAuthCode: motiResponse.authCode, + pgCardType: motiResponse.cardType, + pgTransactionDate: motiResponse.trnDate, + pgCvdId: Number(motiResponse.cvdId), + pgPaymentMethod: motiResponse.paymentMethod, + pgMessageId: Number(motiResponse.messageId), + pgMessageText: motiResponse.messageText, }; }; diff --git a/frontend/src/features/permits/pages/Refund/RefundPage.scss b/frontend/src/features/permits/pages/Refund/RefundPage.scss new file mode 100644 index 000000000..2fed0d059 --- /dev/null +++ b/frontend/src/features/permits/pages/Refund/RefundPage.scss @@ -0,0 +1,147 @@ +@import "../../../../themes/orbcStyles.scss"; + +.refund-page { + padding: 0 10%; + display: flex; + flex-direction: row; + margin-bottom: 3rem; + + &__section { + display: flex; + flex-direction: column; + + &--left { + width: 60%; + margin-right: 2rem; + } + + &--right { + width: 40%; + margin-left: 2rem; + } + + .refund-info { + padding: 2rem 0; + border-bottom: 1px solid $bc-border-grey; + + &--transactions, &--refund-methods { + padding-top: 0; + padding-bottom: 1rem; + border-bottom: none; + } + + &__header { + margin-bottom: 1em; + font-size: 1.5rem; + font-weight: 600; + } + + &__info { + margin: 0.25rem 0; + } + + .refund-methods { + .refund-method { + padding: 1em; + border: 1px solid $bc-text-box-border-grey; + border-radius: 0.5rem; + margin-bottom: 1rem; + + &--active { + border: 1px solid $bc-black; + background-color: $bc-messages-blue-background; + } + + .radio-label span { + font-size: 1.2rem; + font-weight: 600; + } + + .refund-payment { + display: flex; + flex-direction: row; + + &__info { + &--method { + margin-right: 0.5rem; + width: 50%; + } + + &--transaction { + margin-left: 0.5rem; + width: 50%; + } + } + + &__label { + font-weight: 600; + margin-bottom: 0.5rem; + } + + &__input { + background-color: $white; + font-size: 0.9rem; + + .MuiOutlinedInput-notchedOutline { + top: -6px; + } + + .MuiSelect-icon { + display: none; + } + + &--method { + background-color: $bc-background-light-grey; + } + + &--err { + .MuiOutlinedInput-notchedOutline { + border: 2px solid $bc-red; + } + } + } + + &__err { + color: $bc-red; + } + } + } + } + + &--fee-summary { + padding: 0; + border-bottom: none; + + .refund-fee-summary { + border-radius: 4px; + overflow: hidden; + + &__header { + padding: 1rem; + background-color: $bc-black; + color: $white; + font-weight: 600; + } + + &__title { + font-size: 1.5rem; + } + + .fee-summary { + margin: 0; + } + + &__footer { + padding: 1rem; + background-color: $banner-grey; + + .finish-btn { + font-weight: 600; + width: 100%; + } + } + } + } + } + } +} diff --git a/frontend/src/features/permits/pages/Refund/RefundPage.tsx b/frontend/src/features/permits/pages/Refund/RefundPage.tsx new file mode 100644 index 000000000..d22114524 --- /dev/null +++ b/frontend/src/features/permits/pages/Refund/RefundPage.tsx @@ -0,0 +1,336 @@ +import { useState, useEffect } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { + Button, + FormControl, + FormControlLabel, + RadioGroup, + Radio, + FormLabel, + Select, + MenuItem, + OutlinedInput, + FormHelperText, +} from "@mui/material"; + +import "./RefundPage.scss"; +import { permitTypeDisplayText } from "../../types/PermitType"; +import { RefundFormData } from "./types/RefundFormData"; +import { REFUND_METHODS, getRefundMethodByCardType, refundMethodDisplayText } from "../../types/PaymentMethod"; +import { requiredMessage } from "../../../../common/helpers/validationMessages"; +import { getErrorMessage } from "../../../../common/components/form/CustomFormComponents"; +import { PermitHistory } from "../../types/PermitHistory"; +import { TransactionHistoryTable } from "./components/TransactionHistoryTable"; +import { FeeSummary } from "../../components/feeSummary/FeeSummary"; +import { getDefaultRequiredVal } from "../../../../common/helpers/util"; + +type PermitAction = "void" | "revoke" | "amend"; + +const permitActionText = (permitAction: PermitAction) => { + switch (permitAction) { + case "void": + return "Voiding"; + case "revoke": + return "Revoking"; + case "amend": + return "Amending"; + } +}; + +const transactionIdRules = { + validate: { + requiredWhenSelected: (value: string | undefined, formValues: RefundFormData) => { + return !formValues.shouldUsePrevPaymentMethod + || (value != null && value.trim() !== "") + || requiredMessage(); + } + }, +}; + +const refundOptions = Object.values(REFUND_METHODS).map(refundMethod => ({ + value: refundMethod, + label: refundMethodDisplayText(refundMethod), +})); + +const DEFAULT_REFUND_METHOD = REFUND_METHODS.Cheque; + +export const RefundPage = ({ + permitHistory, + email, + fax, + reason, + permitNumber, + permitType, + permitAction, + amountToRefund, + onFinish, +}: { + permitHistory: PermitHistory[]; + email?: string; + fax?: string; + reason?: string; + permitNumber?: string; + permitType?: string; + permitAction: PermitAction; + amountToRefund: number; + onFinish: (refundData: RefundFormData) => void; +}) => { + const [shouldUsePrevPaymentMethod, setShouldUsePrevPaymentMethod] = useState(true); + + const getRefundMethodForPrevPayMethod = () => { + if (!permitHistory || permitHistory.length === 0) return DEFAULT_REFUND_METHOD; + const cardType = permitHistory[0].pgCardType; + return getRefundMethodByCardType(cardType); + }; + + const getRefundCardType = () => { + if (!permitHistory || permitHistory.length === 0) return ""; + return getDefaultRequiredVal("", permitHistory[0].pgCardType); + }; + + const getRefundOnlineMethod = () => { + if (!permitHistory || permitHistory.length === 0) return ""; + return getDefaultRequiredVal("", permitHistory[0].pgPaymentMethod); + }; + + const formMethods = useForm({ + defaultValues: { + shouldUsePrevPaymentMethod, + refundMethod: getRefundMethodForPrevPayMethod(), + refundCardType: getRefundCardType(), + refundOnlineMethod: getRefundOnlineMethod(), + transactionId: "", + }, + reValidateMode: "onChange", + }); + + const { + control, + getValues, + handleSubmit, + setValue, + formState: { errors }, + register, + clearErrors, + } = formMethods; + + useEffect(() => { + const refundMethod = getRefundMethodForPrevPayMethod(); + setValue("refundMethod", refundMethod); + setValue("refundCardType", getRefundCardType()); + setValue("refundOnlineMethod", getRefundOnlineMethod()); + }, [permitHistory, permitHistory.length]); + + const handleRefundMethodChange = (shouldUsePrev: string) => { + const usePrev = shouldUsePrev === "true"; + setShouldUsePrevPaymentMethod(usePrev); + setValue("shouldUsePrevPaymentMethod", usePrev); + setValue("refundMethod", usePrev ? getRefundMethodForPrevPayMethod() : REFUND_METHODS.Cheque); + setValue("refundCardType", usePrev ? getRefundCardType() : ""); + setValue("refundOnlineMethod", usePrev ? getRefundOnlineMethod() : ""); + clearErrors("transactionId"); + }; + + const handleFinish = () => { + const formValues = getValues(); + onFinish(formValues); + }; + + const showSendSection = permitAction === "void" || permitAction === "revoke"; + const showReasonSection = (permitAction === "void" || permitAction === "revoke") && reason; + + // only show refund method selection when amount to refund is greater than 0 + // we use a small epsilon since there may be decimal precision errors when doing decimal comparisons + const enableRefundSelection = Math.abs(amountToRefund) > 0.0000001; + + return ( +
+
+
+
+ Transaction History +
+ +
+ {showSendSection ? ( +
+
+ Send Permit and Receipt to +
+ {email ? ( +
+ Email: + + {email} + +
+ ) : null} + {fax ? ( +
+ Fax: + + {fax} + +
+ ) : null} +
+ ) : null} + {showReasonSection ? ( +
+
Reason for {permitActionText(permitAction)}
+
+ {reason} +
+
+ ) : null} +
+
+ {enableRefundSelection ? ( +
+
+ Choose a Refund Method +
+ + ( + handleRefundMethodChange(e.target.value)} + > +
+ + } + /> +
+ ( + + + Payment Method + + + + )} + /> + + ( + + + Transaction ID + + + {invalid ? ( + + {getErrorMessage(errors, "transactionId")} + + ) : null} + + )} + /> +
+
+
+ + } + /> +
+
+ )} + /> +
+
+ ) : null} +
+
+
+
+ {permitTypeDisplayText(permitType)} +
+
+ {permitActionText(permitAction)} Permit #: + + {permitNumber} + +
+
+ +
+ +
+
+
+
+
+ ); +}; diff --git a/frontend/src/features/permits/pages/Refund/components/TransactionHistoryTable.scss b/frontend/src/features/permits/pages/Refund/components/TransactionHistoryTable.scss new file mode 100644 index 000000000..9b6ad735b --- /dev/null +++ b/frontend/src/features/permits/pages/Refund/components/TransactionHistoryTable.scss @@ -0,0 +1,52 @@ +@import "../../../../../themes/orbcStyles.scss"; + +.transaction-history-table { + width: 100%; + border: 1px solid $bc-border-grey; + + & &__header { + background-color: $bc-background-light-grey; + + &--permit { + padding-left: 1rem; + padding-right: 0.5rem; + } + + &--payment { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + &--transaction { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + &--amount { + padding-left: 0.5rem; + padding-right: 1rem; + } + } + + & &__data { + &--permit { + padding-left: 1rem; + padding-right: 0.5rem; + } + + &--payment { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + &--transaction { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + &--amount { + padding-left: 0.5rem; + padding-right: 1rem; + } + } +} diff --git a/frontend/src/features/permits/pages/Refund/components/TransactionHistoryTable.tsx b/frontend/src/features/permits/pages/Refund/components/TransactionHistoryTable.tsx new file mode 100644 index 000000000..e3478b855 --- /dev/null +++ b/frontend/src/features/permits/pages/Refund/components/TransactionHistoryTable.tsx @@ -0,0 +1,103 @@ +import { useMemo } from "react"; +import MaterialReactTable, { MRT_ColumnDef } from "material-react-table"; + +import "./TransactionHistoryTable.scss"; +import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../../../common/helpers/util"; +import { feeSummaryDisplayText, isTransactionTypeRefund } from "../../../helpers/feeSummary"; +import { PermitHistory } from "../../../types/PermitHistory"; +import { getPaymentMethod, paymentMethodDisplayText } from "../../../types/PaymentMethod"; + +export const TransactionHistoryTable = ({ + permitHistory, +}: { + permitHistory: PermitHistory[]; +}) => { + const columns = useMemo[]>(() => [ + { + accessorKey: "permitNumber", + header: "Permit #", + muiTableHeadCellProps: { + className: "transaction-history-table__header transaction-history-table__header--permit", + }, + muiTableBodyCellProps: { + className: "transaction-history-table__data transaction-history-table__data--permit", + }, + size: 150, + enableSorting: false, + enableColumnActions: false, + }, + { + accessorFn: (originalRow) => { + const paymentMethod = getPaymentMethod(originalRow.pgPaymentMethod, originalRow.pgCardType); + return getDefaultRequiredVal( + "NA", + applyWhenNotNullable(paymentMethodDisplayText, paymentMethod), + ); + }, + id: "paymentMethod", + header: "Payment Method", + muiTableHeadCellProps: { + className: "transaction-history-table__header transaction-history-table__header--payment", + }, + muiTableBodyCellProps: { + className: "transaction-history-table__data transaction-history-table__data--payment", + }, + size: 200, + enableSorting: false, + enableColumnActions: false, + }, + { + accessorFn: (originalRow) => + getDefaultRequiredVal("NA", `${originalRow.pgTransactionId}`), + id: "providerTransactionId", + header: "Transaction ID", + muiTableHeadCellProps: { + className: "transaction-history-table__header transaction-history-table__header--transaction", + }, + muiTableBodyCellProps: { + className: "transaction-history-table__data transaction-history-table__data--transaction", + }, + size: 100, + enableSorting: false, + enableColumnActions: false, + }, + { + accessorFn: (originalRow) => { + const amount = isTransactionTypeRefund(originalRow.transactionTypeId) + ? -1 * originalRow.transactionAmount : originalRow.transactionAmount; + + return feeSummaryDisplayText( + applyWhenNotNullable((val) => `${val}`, amount) + ); + }, + header: "Amount (CAD)", + muiTableHeadCellProps: { + className: "transaction-history-table__header transaction-history-table__header--amount", + align: "right", + }, + muiTableBodyCellProps: { + className: "transaction-history-table__data transaction-history-table__data--amount", + align: "right", + }, + id: "transactionAmount", + size: 50, + enableSorting: false, + enableColumnActions: false, + }, + ], []); + + return ( + + ); +}; diff --git a/frontend/src/features/permits/pages/Refund/types/RefundFormData.ts b/frontend/src/features/permits/pages/Refund/types/RefundFormData.ts new file mode 100644 index 000000000..62fa43c2e --- /dev/null +++ b/frontend/src/features/permits/pages/Refund/types/RefundFormData.ts @@ -0,0 +1,13 @@ +import { + BamboraPaymentMethod, + CardType, + RefundMethod, +} from "../../../types/PaymentMethod"; + +export interface RefundFormData { + shouldUsePrevPaymentMethod: boolean; + refundMethod: RefundMethod; + refundOnlineMethod: BamboraPaymentMethod | ""; + refundCardType: CardType | ""; + transactionId?: string; +} diff --git a/frontend/src/features/permits/pages/SuccessPage/SuccessPage.scss b/frontend/src/features/permits/pages/SuccessPage/SuccessPage.scss index 73704b894..a89acc36f 100644 --- a/frontend/src/features/permits/pages/SuccessPage/SuccessPage.scss +++ b/frontend/src/features/permits/pages/SuccessPage/SuccessPage.scss @@ -4,30 +4,54 @@ display: flex; flex-direction: column; align-items: center; + background-color: orbcStyles.$bc-white; &__container { display: flex; flex-direction: column; align-items: center; width: 90%; - max-width: 640px; + max-width: 540px; margin: 2em 0; } &__block { margin: 1em 0; + &--success-msg { + color: orbcStyles.$bc-black; + font-weight: 700; + font-size: 1.5rem; + } + &--info { + padding: 1.5em 1em; + border-top: 2px solid orbcStyles.$bc-border-grey; + border-bottom: 2px solid orbcStyles.$bc-border-grey; text-align: center; + font-size: 1.2rem; + font-weight: 600; + color: orbcStyles.$bc-black; + } + + &--apply-permit { + .success-btn { + font-weight: 600; + } } &--view-permits { - button { + .success-btn { border: 1px solid orbcStyles.$bc-text-box-border-grey; - } - button:hover { - color: orbcStyles.$white; + &--view-permits { + border: 2px solid orbcStyles.$bc-text-box-border-grey; + } + + &--view-receipt { + margin-left: 1.5rem; + border: 2px solid orbcStyles.$bc-text-box-border-grey; + } } } } diff --git a/frontend/src/features/permits/pages/SuccessPage/SuccessPage.tsx b/frontend/src/features/permits/pages/SuccessPage/SuccessPage.tsx index 7632fe619..ba879ad02 100644 --- a/frontend/src/features/permits/pages/SuccessPage/SuccessPage.tsx +++ b/frontend/src/features/permits/pages/SuccessPage/SuccessPage.tsx @@ -1,19 +1,9 @@ -import { Box, Button, Typography } from "@mui/material"; +import { Box, Button } from "@mui/material"; import { useEffect } from "react"; -import "./SuccessPage.scss"; import { useNavigate, useParams } from "react-router-dom"; -import { downloadPermitApplicationPdf, downloadReceiptPdf } from "../../apiManager/permitsAPI"; -const downloadFile = (blob: Blob, filename: string) => { - const objUrl = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = objUrl; - link.setAttribute('download', `${filename}`); // Set the desired file name - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(objUrl); -}; +import "./SuccessPage.scss"; +import { viewPermitPdf, viewReceiptPdf } from "../../helpers/permitPDFHelper"; export const SuccessPage = () => { useEffect(() => { @@ -23,30 +13,15 @@ export const SuccessPage = () => { const navigate = useNavigate(); const { permitId } = useParams(); - const viewPermitPdfByPermitId = async (permitId: string) => { - try { - const { blobObj, filename } = await downloadPermitApplicationPdf(permitId); - // Create an object URL for the response - downloadFile(blobObj, filename); - } catch (err) { - console.error(err); - } - }; - - const viewPermitPdf = async () => { + const viewPermits = async () => { if (permitId) { - return await viewPermitPdfByPermitId(permitId); + return await viewPermitPdf(permitId); } }; - const viewReceiptPdf = async () => { + const viewReceipt = async () => { if (permitId) { - try { - const { blobObj, filename } = await downloadReceiptPdf(permitId); - downloadFile(blobObj, filename); - } catch (err) { - console.error(err); - } + return await viewReceiptPdf(permitId); } }; @@ -62,19 +37,18 @@ export const SuccessPage = () => { />
- Success + Success - - The permit has been sent to the email/fax - provided. - + The permit(s) and receipt have been sent to the email/fax + provided. @@ -82,20 +56,21 @@ export const SuccessPage = () => { diff --git a/frontend/src/features/permits/pages/TermOversize/TermOversize.scss b/frontend/src/features/permits/pages/TermOversize/TermOversize.scss deleted file mode 100644 index 453b7d7fd..000000000 --- a/frontend/src/features/permits/pages/TermOversize/TermOversize.scss +++ /dev/null @@ -1,46 +0,0 @@ -@import "../../../../themes/orbcStyles.scss"; - -//TODO: not yet implemented -.tros-main-box { - display: flex; - flex-wrap: wrap; - background-color: $bc-white; - border-bottom: 1px solid $bc-text-box-border-grey; - padding-bottom: 24px; -} - -.tros-left-box { - padding-top: 24px; - min-width: 400px; - max-width: $tros-left-column-width; -} - -.tros-right-box { - padding-top: 24px; - min-width: calc(100% - $tros-left-column-width); - max-width: $tros-left-column-width; -} - -// Custom styling for MUI Select fields -.select-field-form-label { - font-weight: bold !important; - margin-bottom: 8px !important; -} - -.payment { - padding: 0px 60px; - //overflow: hidden; - background-color: white; - min-height: calc(100vh - 390px); - height: 100%; - display: flex; - flex-direction: column; - //min-width: 100%; - - - &--fee-summary { - padding: 24px; - max-width: 488px; - } - -} diff --git a/frontend/src/features/permits/pages/TermOversize/TermOversizeForm.tsx b/frontend/src/features/permits/pages/TermOversize/TermOversizeForm.tsx index 96df15a29..006e288f3 100644 --- a/frontend/src/features/permits/pages/TermOversize/TermOversizeForm.tsx +++ b/frontend/src/features/permits/pages/TermOversize/TermOversizeForm.tsx @@ -1,40 +1,18 @@ -import { FormProvider, FieldValues } from "react-hook-form"; -import { Box, Button, useMediaQuery, useTheme } from "@mui/material"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faSave } from "@fortawesome/free-solid-svg-icons"; +import { FieldValues, FormProvider } from "react-hook-form"; import { useNavigate } from "react-router-dom"; -import { Application, VehicleDetails as VehicleDetailsType } from "../../types/application"; -import { ContactDetails } from "../../components/form/ContactDetails"; -import { ApplicationDetails } from "../../components/form/ApplicationDetails"; -import { VehicleDetails } from "./form/VehicleDetails/VehicleDetails"; import { useContext, useState } from "react"; -import { BC_COLOURS } from "../../../../themes/bcGovStyles"; -import { PERMIT_LEFT_COLUMN_WIDTH } from "../../../../themes/orbcStyles"; + +import { Application } from "../../types/application.d"; import { ApplicationContext } from "../../context/ApplicationContext"; -import { PermitDetails } from "./form/PermitDetails"; import { ProgressBar } from "../../components/progressBar/ProgressBar"; -import { ScrollButton } from "../../components/scrollButton/ScrollButton"; -import { - useAddPowerUnitMutation, - useUpdatePowerUnitMutation, - useAddTrailerMutation, - useUpdateTrailerMutation, - useVehiclesQuery, - usePowerUnitTypesQuery, - useTrailerTypesQuery, -} from "../../../manageVehicles/apiManager/hooks"; -import { - PowerUnit, - Trailer, -} from "../../../manageVehicles/types/managevehicles"; -import { mapVinToVehicleObject } from "../../helpers/mappers"; import { useSaveTermOversizeMutation } from "../../hooks/hooks"; import { SnackBarContext } from "../../../../App"; import { LeaveApplicationDialog } from "../../components/dialog/LeaveApplicationDialog"; import { areApplicationDataEqual } from "../../helpers/equality"; import { useDefaultApplicationFormData } from "../../hooks/useDefaultApplicationFormData"; -import { getDefaultRequiredVal } from "../../../../common/helpers/util"; import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext"; +import { PermitForm } from "./components/form/PermitForm"; +import { usePermitVehicleManagement } from "../../hooks/usePermitVehicleManagement"; /** * The first step in creating and submitting a TROS Application. @@ -44,10 +22,6 @@ export const TermOversizeForm = () => { //The name of this feature that is used for id's, keys, and associating form components const FEATURE = "term-oversize"; - // Styling / responsiveness - const theme = useTheme(); - const matches = useMediaQuery(theme.breakpoints.down("lg")); - // Context to hold all of the application data related to the TROS application const applicationContext = useContext(ApplicationContext); @@ -59,6 +33,7 @@ export const TermOversizeForm = () => { const { defaultApplicationDataValues: termOversizeDefaultValues, formMethods, + companyInfo, } = useDefaultApplicationFormData( applicationContext?.applicationData ); @@ -67,6 +42,13 @@ export const TermOversizeForm = () => { const snackBar = useContext(SnackBarContext); const { companyLegalName, onRouteBCClientNumber } = useContext(OnRouteBCContext); + const { + handleSaveVehicle, + vehicleOptions, + powerUnitTypes, + trailerTypes, + } = usePermitVehicleManagement(); + // Show leave application dialog const [showLeaveApplicationDialog, setShowLeaveApplicationDialog] = useState(false); @@ -150,102 +132,7 @@ export const TermOversizeForm = () => { onSaveFailure(); } }; - - // Mutations used to add/update vehicle details - const addPowerUnitMutation = useAddPowerUnitMutation(); - const updatePowerUnitMutation = useUpdatePowerUnitMutation(); - const addTrailerMutation = useAddTrailerMutation(); - const updateTrailerMutation = useUpdateTrailerMutation(); - - // Queries used to populate select options for vehicle details - const allVehiclesQuery = useVehiclesQuery(); - const powerUnitTypesQuery = usePowerUnitTypesQuery(); - const trailerTypesQuery = useTrailerTypesQuery(); - - // Vehicle details that have been fetched by vehicle details queries - const fetchedVehicles = getDefaultRequiredVal([], allVehiclesQuery.data); - const fetchedPowerUnitTypes = getDefaultRequiredVal([], powerUnitTypesQuery.data); - const fetchedTrailerTypes = getDefaultRequiredVal([], trailerTypesQuery.data); - - const handleSaveVehicle = (vehicleData?: VehicleDetailsType) => { - // Check if the "add/update vehicle" checkbox was checked by the user - if (!vehicleData?.saveVehicle) return; - - // Get the vehicle info from the form - const vehicle = vehicleData; - - // Check if the vehicle that is to be saved was created from an existing vehicle - const existingVehicle = mapVinToVehicleObject( - fetchedVehicles, - vehicle.vin - ); - - const transformByVehicleType = (vehicleFormData: VehicleDetailsType, existingVehicle?: PowerUnit | Trailer): PowerUnit | Trailer => { - const defaultPowerUnit: PowerUnit = { - powerUnitId: "", - unitNumber: "", - vin: vehicleFormData.vin, - plate: vehicleFormData.plate, - make: vehicleFormData.make, - year: vehicleFormData.year, - countryCode: vehicleFormData.countryCode, - provinceCode: vehicleFormData.provinceCode, - powerUnitTypeCode: vehicleFormData.vehicleSubType, - }; - - const defaultTrailer: Trailer = { - trailerId: "", - unitNumber: "", - vin: vehicleFormData.vin, - plate: vehicleFormData.plate, - make: vehicleFormData.make, - year: vehicleFormData.year, - countryCode: vehicleFormData.countryCode, - provinceCode: vehicleFormData.provinceCode, - trailerTypeCode: vehicleFormData.vehicleSubType, - }; - - switch (vehicleFormData.vehicleType) { - case "trailer": - return { - ...defaultTrailer, - trailerId: getDefaultRequiredVal("", (existingVehicle as Trailer)?.trailerId), - unitNumber: getDefaultRequiredVal("", existingVehicle?.unitNumber), - } as Trailer; - case "powerUnit": - default: - return { - ...defaultPowerUnit, - unitNumber: getDefaultRequiredVal("", existingVehicle?.unitNumber), - powerUnitId: getDefaultRequiredVal("", (existingVehicle as PowerUnit)?.powerUnitId), - } as PowerUnit; - } - }; - - // If the vehicle type is a power unit then create a power unit object - if (vehicle.vehicleType === "powerUnit") { - const powerUnit = transformByVehicleType(vehicle, existingVehicle) as PowerUnit; - - // Either send a PUT or POST request based on powerUnitID - if (powerUnit.powerUnitId) { - updatePowerUnitMutation.mutate({ - powerUnit, - powerUnitId: powerUnit.powerUnitId, - }); - } else { - addPowerUnitMutation.mutate(powerUnit); - } - } else if (vehicle.vehicleType === "trailer") { - const trailer = transformByVehicleType(vehicle, existingVehicle) as Trailer; - - if (trailer.trailerId) { - updateTrailerMutation.mutate({ trailer, trailerId: trailer.trailerId }); - } else { - addTrailerMutation.mutate(trailer); - } - } - }; - + // Whenever "Leave" button is clicked const handleLeaveApplication = () => { if (!isApplicationSaved()) { @@ -266,98 +153,30 @@ export const TermOversizeForm = () => { return ( <> - - - - - - - - - - - - - - - - - - + + onSaveApplication()} + onContinue={handleSubmit(onContinue)} + isAmendAction={false} + permitType={termOversizeDefaultValues.permitType} + applicationNumber={termOversizeDefaultValues.applicationNumber} + permitNumber={termOversizeDefaultValues.permitNumber} + createdDateTime={termOversizeDefaultValues.createdDateTime} + updatedDateTime={termOversizeDefaultValues.updatedDateTime} + permitStartDate={termOversizeDefaultValues.permitData.startDate} + permitDuration={termOversizeDefaultValues.permitData.permitDuration} + permitCommodities={termOversizeDefaultValues.permitData.commodities} + vehicleDetails={termOversizeDefaultValues.permitData.vehicleDetails} + vehicleOptions={vehicleOptions} + powerUnitTypes={powerUnitTypes} + trailerTypes={trailerTypes} + companyInfo={companyInfo} + /> + + { const { applicationData } = useContext(ApplicationContext); - const calculatedFee = feeSummaryDisplayText(applicationData); - useEffect(() => { - window.scrollTo(0, 0); - }, []); - - return ( - <> - - - - - - - ); -}; - -const ApplicationSummary = () => { - const { applicationData } = useContext(ApplicationContext); - const applicationName = permitTypeDisplayText( - getDefaultRequiredVal("", applicationData?.permitType) as PermitType + const calculatedFee = calculateFeeByDuration( + getDefaultRequiredVal(0, applicationData?.permitData?.permitDuration) ); - return ( - - - {applicationName} - - {applicationData?.applicationNumber && - applicationData?.applicationNumber !== "" ? ( - <> - - Application # {applicationData.applicationNumber} - - - ) : ( - <> - )} - - ); -}; + const { + mutation: startTransactionMutation, + transaction, + } = useStartTransaction(); -const FeeSummary = ({ calculatedFee }: { calculatedFee: number }) => { - const { applicationData } = useContext(ApplicationContext); - if (!applicationData?.permitId) - return ; + useEffect(() => { + window.scrollTo(0, 0); + }, []); + + const handlePay = () => { + if (!applicationData?.permitId) { + console.error("Invalid permit id"); + return; + } + + startTransactionMutation.mutate({ + transactionTypeId: TRANSACTION_TYPES.P, + paymentMethodId: "1", // Hardcoded value for Web/MotiPay, still need to implement payment method (ie payBC, manual, etc) + applicationDetails: [ + { + applicationId: applicationData?.permitId, + transactionAmount: calculatedFee, + } + ], + }); + }; - // TODO: Use transaction amount - const transactionAmount = applicationData.permitData.permitDuration; - const permitIds = [applicationData.permitId]; - const transactionSubmitDate = dayjs().utc().toISOString(); - const paymentMethodId = 1; // TODO: implement payment method (ie payBC, manual, etc) + if (typeof transaction !== "undefined") { + if (!transaction?.url) { + console.error("Invalid transaction url"); + } else { + window.open(transaction.url, "_self"); + } + } - const handlePayNow = async () => { - const url = await getMotiPayTransactionUrl( - paymentMethodId, - transactionSubmitDate, - transactionAmount, - permitIds - ); - window.open(url, "_self"); - }; + if (!applicationData?.permitId) { + return ; + } return ( <> - - - - Fee Summary - - - - Description - - - Amount - - - - - {permitTypeDisplayText(applicationData?.permitType)} - - ${calculatedFee}.00 - - - Total (CAD) - ${calculatedFee}.00 - - + + + + - + - - Have questions? Please contact the Provincial Permit Centre. Toll-free: - 1-800-559-9688 or Email:{" "} - ppcpermit@gov.bc.ca - ); }; diff --git a/frontend/src/features/permits/pages/TermOversize/TermOversizeReview.tsx b/frontend/src/features/permits/pages/TermOversize/TermOversizeReview.tsx index 63b0d9dfa..581b9c17f 100644 --- a/frontend/src/features/permits/pages/TermOversize/TermOversizeReview.tsx +++ b/frontend/src/features/permits/pages/TermOversize/TermOversizeReview.tsx @@ -1,27 +1,27 @@ -import { Box, Button } from "@mui/material"; import { useContext, useEffect, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPencil } from "@fortawesome/free-solid-svg-icons"; -import { WarningBcGovBanner } from "../../../../common/components/banners/AlertBanners"; -import { BC_COLOURS } from "../../../../themes/bcGovStyles"; -import { ApplicationDetails } from "../../components/form/ApplicationDetails"; import { ApplicationContext } from "../../context/ApplicationContext"; import { Application } from "../../types/application"; import { useSaveTermOversizeMutation } from "../../hooks/hooks"; -import { ReviewContactDetails } from "./review/ReviewContactDetails"; -import { ReviewFeeSummary } from "./review/ReviewFeeSummary"; -import { ReviewPermitDetails } from "./review/ReviewPermitDetails"; -import { ReviewVehicleInfo } from "./review/ReviewVehicleInfo"; import { ProgressBar } from "../../components/progressBar/ProgressBar"; +import { useCompanyInfoQuery } from "../../../manageProfile/apiManager/hooks"; +import { PermitReview } from "./components/review/PermitReview"; +import { usePowerUnitTypesQuery, useTrailerTypesQuery } from "../../../manageVehicles/apiManager/hooks"; export const TermOversizeReview = () => { - const { applicationData, setApplicationData, back, next } = - useContext(ApplicationContext); + const { + applicationData, + setApplicationData, + back, + next, + } = useContext(ApplicationContext); + const companyQuery = useCompanyInfoQuery(); + const powerUnitTypesQuery = usePowerUnitTypesQuery(); + const trailerTypesQuery = useTrailerTypesQuery(); const methods = useForm(); - + // For the confirmation checkboxes const [isChecked, setIsChecked] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); @@ -50,72 +50,33 @@ export const TermOversizeReview = () => { return ( <> - - - - - - - - - - - - - - - - + + + + ); }; diff --git a/frontend/src/features/permits/pages/TermOversize/form/ConditionsTable.tsx b/frontend/src/features/permits/pages/TermOversize/components/form/ConditionsTable.tsx similarity index 96% rename from frontend/src/features/permits/pages/TermOversize/form/ConditionsTable.tsx rename to frontend/src/features/permits/pages/TermOversize/components/form/ConditionsTable.tsx index a18aa3a77..22dc07b61 100644 --- a/frontend/src/features/permits/pages/TermOversize/form/ConditionsTable.tsx +++ b/frontend/src/features/permits/pages/TermOversize/components/form/ConditionsTable.tsx @@ -1,3 +1,5 @@ +import { useState, useEffect } from "react"; +import { Controller, useFormContext } from "react-hook-form"; import { Checkbox, FormControlLabel, @@ -9,10 +11,9 @@ import { TableHead, TableRow, } from "@mui/material"; -import { useState, useEffect } from "react"; -import { Controller, useFormContext } from "react-hook-form"; -import { TROS_COMMODITIES } from "../../../constants/termOversizeConstants"; -import { Commodities } from "../../../types/application"; + +import { Commodities } from "../../../../types/application"; +import { TROS_COMMODITIES } from "../../../../constants/termOversizeConstants"; export const ConditionsTable = ({ commodities, diff --git a/frontend/src/features/permits/pages/TermOversize/components/form/FormActions.scss b/frontend/src/features/permits/pages/TermOversize/components/form/FormActions.scss new file mode 100644 index 000000000..38d5a5388 --- /dev/null +++ b/frontend/src/features/permits/pages/TermOversize/components/form/FormActions.scss @@ -0,0 +1,63 @@ +@import "../../../../../../themes/orbcStyles.scss"; + +.tros-form-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: $bc-white; + display: flex; + align-items: center; + padding: 2rem 3.75rem; + justify-content: space-between; + border-top: 1px solid $bc-text-box-border-grey; + + & &__section { + display: flex; + align-items: center; + } + + & &__btn { + position: relative; + + &--save { + display: flex; + align-items: center; + gap: 0.5em; + } + + &--continue { + margin-left: 1.5em; + margin-top: 0; + } + } +} + +@media (width < 768px) { + .tros-form-actions { + padding: 2rem 1.25rem; + } +} + +@media (width < 440px) { + .tros-form-actions { + flex-direction: column; + align-items: center; + + & &__section { + flex-direction: column; + align-items: center; + + &--main { + margin-top: 1em; + } + } + + & &__btn { + &--continue { + margin-left: 0; + margin-top: 1em; + } + } + } +} diff --git a/frontend/src/features/permits/pages/TermOversize/components/form/FormActions.tsx b/frontend/src/features/permits/pages/TermOversize/components/form/FormActions.tsx new file mode 100644 index 000000000..dc7cfde9d --- /dev/null +++ b/frontend/src/features/permits/pages/TermOversize/components/form/FormActions.tsx @@ -0,0 +1,85 @@ +import { faSave } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Box, Button } from "@mui/material"; + +import "./FormActions.scss"; +import { ScrollButton } from "../../../../components/scrollButton/ScrollButton"; + +export const FormActions = ({ + onLeave, + onSave, + onCancel, + onContinue, + enableScroll = true, +}: { + onLeave?: () => void; + onSave?: () => Promise; + onCancel?: () => void; + onContinue: () => Promise; + enableScroll?: boolean; +}) => { + return ( + + {onLeave ? ( + + ) : null} + + + {onSave ? ( + + ) : null} + + {onCancel ? ( + + ) : null} + + + + {enableScroll ? ( + + ) : null} + + + ); +}; diff --git a/frontend/src/features/permits/pages/TermOversize/form/PermitDetails.tsx b/frontend/src/features/permits/pages/TermOversize/components/form/PermitDetails.tsx similarity index 87% rename from frontend/src/features/permits/pages/TermOversize/form/PermitDetails.tsx rename to frontend/src/features/permits/pages/TermOversize/components/form/PermitDetails.tsx index 2c405fd5f..9ebbbb8fa 100644 --- a/frontend/src/features/permits/pages/TermOversize/form/PermitDetails.tsx +++ b/frontend/src/features/permits/pages/TermOversize/components/form/PermitDetails.tsx @@ -1,21 +1,22 @@ import { Box, MenuItem, Typography } from "@mui/material"; import { useFormContext } from "react-hook-form"; -import { InfoBcGovBanner } from "../../../../../common/components/banners/AlertBanners"; -import { PermitExpiryDateBanner } from "../../../../../common/components/banners/PermitExpiryDateBanner"; -import { CustomFormComponent } from "../../../../../common/components/form/CustomFormComponents"; -import { PHONE_WIDTH } from "../../../../../themes/bcGovStyles"; +import dayjs, { Dayjs } from "dayjs"; +import { useEffect } from "react"; + +import { InfoBcGovBanner } from "../../../../../../common/components/banners/AlertBanners"; +import { PermitExpiryDateBanner } from "../../../../../../common/components/banners/PermitExpiryDateBanner"; +import { CustomFormComponent } from "../../../../../../common/components/form/CustomFormComponents"; +import { PHONE_WIDTH } from "../../../../../../themes/bcGovStyles"; import { ConditionsTable } from "./ConditionsTable"; +import { TROS_PERMIT_DURATIONS } from "../../../../constants/termOversizeConstants"; +import { requiredMessage } from "../../../../../../common/helpers/validationMessages"; +import { Commodities } from "../../../../types/application"; import { PERMIT_MAIN_BOX_STYLE, PERMIT_LEFT_BOX_STYLE, PERMIT_LEFT_HEADER_STYLE, PERMIT_RIGHT_BOX_STYLE, -} from "../../../../../themes/orbcStyles"; -import { TROS_PERMIT_DURATIONS } from "../../../constants/termOversizeConstants"; -import dayjs, { Dayjs } from "dayjs"; -import { requiredMessage } from "../../../../../common/helpers/validationMessages"; -import { useEffect } from "react"; -import { Commodities } from "../../../types/application"; +} from "../../../../../../themes/orbcStyles"; export const PermitDetails = ({ feature, diff --git a/frontend/src/features/permits/pages/TermOversize/components/form/PermitForm.scss b/frontend/src/features/permits/pages/TermOversize/components/form/PermitForm.scss new file mode 100644 index 000000000..0c9563d47 --- /dev/null +++ b/frontend/src/features/permits/pages/TermOversize/components/form/PermitForm.scss @@ -0,0 +1,10 @@ +@import "../../../../../../themes/orbcStyles.scss"; + +.permit-form { + padding-top: 1.5rem; + background-color: $bc-white; + + &__form { + padding-bottom: 5rem; + } +} diff --git a/frontend/src/features/permits/pages/TermOversize/components/form/PermitForm.tsx b/frontend/src/features/permits/pages/TermOversize/components/form/PermitForm.tsx new file mode 100644 index 000000000..71a54421a --- /dev/null +++ b/frontend/src/features/permits/pages/TermOversize/components/form/PermitForm.tsx @@ -0,0 +1,84 @@ +import { Box } from "@mui/material"; +import { Dayjs } from "dayjs"; + +import "./PermitForm.scss"; +import { FormActions } from "./FormActions"; +import { ApplicationDetails } from "../../../../components/form/ApplicationDetails"; +import { ContactDetails } from "../../../../components/form/ContactDetails"; +import { PermitDetails } from "./PermitDetails"; +import { VehicleDetails } from "./VehicleDetails/VehicleDetails"; +import { CompanyProfile } from "../../../../../manageProfile/types/manageProfile.d"; +import { PermitType } from "../../../../types/PermitType"; +import { + PowerUnit, + Trailer, + VehicleType, +} from "../../../../../manageVehicles/types/managevehicles.d"; + +import { + Commodities, + VehicleDetails as VehicleDetailsType, +} from "../../../../types/application.d"; + +interface PermitFormProps { + feature: string; + onLeave?: () => void; + onSave?: () => Promise; + onCancel?: () => void; + onContinue: () => Promise; + isAmendAction: boolean; + permitType: PermitType; + applicationNumber?: string; + permitNumber?: string; + createdDateTime?: Dayjs; + updatedDateTime?: Dayjs; + permitStartDate: Dayjs; + permitDuration: number; + permitCommodities: Commodities[]; + vehicleDetails?: VehicleDetailsType; + vehicleOptions: (PowerUnit | Trailer)[]; + powerUnitTypes: VehicleType[]; + trailerTypes: VehicleType[]; + children?: React.ReactNode; + companyInfo?: CompanyProfile; +} + +export const PermitForm = (props: PermitFormProps) => { + return ( + + + + + + + {props.children} + + + + + ); +}; diff --git a/frontend/src/features/permits/pages/TermOversize/form/VehicleDetails/VehicleDetails.tsx b/frontend/src/features/permits/pages/TermOversize/components/form/VehicleDetails/VehicleDetails.tsx similarity index 92% rename from frontend/src/features/permits/pages/TermOversize/form/VehicleDetails/VehicleDetails.tsx rename to frontend/src/features/permits/pages/TermOversize/components/form/VehicleDetails/VehicleDetails.tsx index b9f333e02..3d463b967 100644 --- a/frontend/src/features/permits/pages/TermOversize/form/VehicleDetails/VehicleDetails.tsx +++ b/frontend/src/features/permits/pages/TermOversize/components/form/VehicleDetails/VehicleDetails.tsx @@ -12,23 +12,30 @@ import { import { useFormContext } from "react-hook-form"; import { useEffect, useState } from "react"; -import { CountryAndProvince } from "../../../../../../common/components/form/CountryAndProvince"; -import { CustomFormComponent } from "../../../../../../common/components/form/CustomFormComponents"; -import { InfoBcGovBanner } from "../../../../../../common/components/banners/AlertBanners"; -import { VehicleDetails as VehicleDetailsType } from "../../../../types/application"; +import { CountryAndProvince } from "../../../../../../../common/components/form/CountryAndProvince"; +import { CustomFormComponent } from "../../../../../../../common/components/form/CustomFormComponents"; +import { InfoBcGovBanner } from "../../../../../../../common/components/banners/AlertBanners"; +import { VehicleDetails as VehicleDetailsType } from "../../../../../types/application"; +import { PowerUnit, Trailer, Vehicle, VehicleType } from "../../../../../../manageVehicles/types/managevehicles"; +import { mapVinToVehicleObject } from "../../../../../helpers/mappers"; +import { getDefaultRequiredVal } from "../../../../../../../common/helpers/util"; +import { sortVehicleSubTypes } from "../../../../../helpers/sorter"; +import { removeIneligibleVehicleSubTypes } from "../../../../../helpers/removeIneligibleVehicles"; +import { TROS_INELIGIBLE_POWERUNITS, TROS_INELIGIBLE_TRAILERS } from "../../../../../constants/termOversizeConstants"; +import { CustomInputHTMLAttributes } from "../../../../../../../common/types/formElements"; +import { SelectUnitOrPlate } from "./customFields/SelectUnitOrPlate"; +import { SelectVehicleDropdown } from "./customFields/SelectVehicleDropdown"; import { PERMIT_MAIN_BOX_STYLE, PERMIT_LEFT_BOX_STYLE, PERMIT_LEFT_HEADER_STYLE, PERMIT_RIGHT_BOX_STYLE, -} from "../../../../../../themes/orbcStyles"; +} from "../../../../../../../themes/orbcStyles"; -import { SelectUnitOrPlate } from "./customFields/SelectUnitOrPlate"; -import { SelectVehicleDropdown } from "./customFields/SelectVehicleDropdown"; import { CHOOSE_FROM_OPTIONS, VEHICLE_TYPES, -} from "../../../../constants/constants"; +} from "../../../../../constants/constants"; import { invalidNumber, @@ -36,14 +43,7 @@ import { invalidVINLength, invalidYearMin, requiredMessage -} from "../../../../../../common/helpers/validationMessages"; -import { PowerUnit, Trailer, Vehicle, VehicleType } from "../../../../../manageVehicles/types/managevehicles"; -import { mapVinToVehicleObject } from "../../../../helpers/mappers"; -import { getDefaultRequiredVal } from "../../../../../../common/helpers/util"; -import { sortVehicleSubTypes } from "../../../../helpers/sorter"; -import { removeIneligibleVehicleSubTypes } from "../../../../helpers/removeIneligibleVehicles"; -import { TROS_INELIGIBLE_POWERUNITS, TROS_INELIGIBLE_TRAILERS } from "../../../../constants/termOversizeConstants"; -import { CustomInputHTMLAttributes } from "../../../../../../common/types/formElements"; +} from "../../../../../../../common/helpers/validationMessages"; export const VehicleDetails = ({ feature, diff --git a/frontend/src/features/permits/pages/TermOversize/form/VehicleDetails/customFields/SelectPermitType.tsx b/frontend/src/features/permits/pages/TermOversize/components/form/VehicleDetails/customFields/SelectPermitType.tsx similarity index 78% rename from frontend/src/features/permits/pages/TermOversize/form/VehicleDetails/customFields/SelectPermitType.tsx rename to frontend/src/features/permits/pages/TermOversize/components/form/VehicleDetails/customFields/SelectPermitType.tsx index 39bb8c109..9e5f59406 100644 --- a/frontend/src/features/permits/pages/TermOversize/form/VehicleDetails/customFields/SelectPermitType.tsx +++ b/frontend/src/features/permits/pages/TermOversize/components/form/VehicleDetails/customFields/SelectPermitType.tsx @@ -4,7 +4,9 @@ import { Select, SelectChangeEvent, } from "@mui/material"; -import { SELECT_FIELD_STYLE } from "../../../../../../../themes/orbcStyles"; + +import { SELECT_FIELD_STYLE } from "../../../../../../../../themes/orbcStyles"; +import { PERMIT_TYPES } from "../../../../../../types/PermitType"; export const SelectPermitType = ({ label, @@ -21,7 +23,7 @@ export const SelectPermitType = ({ {label} + {invalid ? ( + + {getErrorMessage(errors, "reason")} + + ) : null} + + )} + /> +
+ + +
+ +
+ + ); +}; diff --git a/frontend/src/features/permits/pages/Void/components/VoidPermitForm.scss b/frontend/src/features/permits/pages/Void/components/VoidPermitForm.scss new file mode 100644 index 000000000..d57f17ebe --- /dev/null +++ b/frontend/src/features/permits/pages/Void/components/VoidPermitForm.scss @@ -0,0 +1,131 @@ +@import "../../../../../themes/orbcStyles.scss"; + +.void-permit__form { + padding: 0 10%; + + .form-section { + display: flex; + flex-direction: row; + padding: 2em 0; + border-bottom: 1px solid $bc-border-grey; + + &--reason { + margin-top: 0.5em; + } + + &__label { + font-weight: 600; + font-size: 1.2rem; + width: 40%; + } + + &__input-area { + width: 60%; + + .void-input { + &--reason { + border: 1.5px solid $bc-text-box-border-grey; + border-radius: 4px; + resize: none; + } + + &--email { + max-width: 375px; + + .custom-form-control { + margin: 0; + } + } + + &--fax { + max-width: 375px; + + .custom-form-control { + margin: 1em 0 0 0; + } + } + + &--err { + border: 2px solid $bc-red; + + &:focus { + outline: transparent; + } + } + + &__err { + color: $bc-red; + } + } + + .reason-container { + display: flex; + flex-direction: row; + align-items: flex-start; + + &__left { + display: flex; + flex-direction: column; + width: 60%; + margin-right: 2rem; + } + + &__right { + display: flex; + flex-direction: column; + width: 40%; + + .revoke { + display: flex; + flex-direction: column; + + &__header { + background-color: $bc-messages-red-text; + color: $white; + padding: 1em; + font-weight: 600; + font-size: 1.2rem; + } + + &__body { + background-color: $bc-messages-red-background; + padding: 1em; + } + + &__msg { + &--bold { + font-weight: 600; + } + } + + &__btn { + color: $white; + background-color: $bc-red; + font-weight: 600; + cursor: pointer; + margin-top: 1em; + width: 100%; + } + } + } + } + + .bc-gov-alertbanner { + margin: 0; + } + + .void-permit-button { + &--cancel { + background-color: $bc-background-light-grey; + color: $bc-black; + margin-right: 1.5em; + border: none; + } + + &--continue { + font-weight: 600; + } + } + } + } +} diff --git a/frontend/src/features/permits/pages/Void/components/VoidPermitForm.tsx b/frontend/src/features/permits/pages/Void/components/VoidPermitForm.tsx new file mode 100644 index 000000000..c2bcf1298 --- /dev/null +++ b/frontend/src/features/permits/pages/Void/components/VoidPermitForm.tsx @@ -0,0 +1,245 @@ +import { Controller, FormProvider } from "react-hook-form"; +import isEmail from "validator/lib/isEmail"; +import { useNavigate } from "react-router-dom"; +import { Button, FormControl, FormHelperText } from "@mui/material"; +import { useEffect, useState } from "react"; + +import "./VoidPermitForm.scss"; +import { CustomFormComponent, getErrorMessage } from "../../../../../common/components/form/CustomFormComponents"; +import { invalidEmail, invalidPhoneLength, requiredMessage } from "../../../../../common/helpers/validationMessages"; +import { useVoidPermitForm } from "../hooks/useVoidPermitForm"; +import { VoidPermitHeader } from "./VoidPermitHeader"; +import { ReadPermitDto } from "../../../types/permit"; +import { SEARCH_RESULTS } from "../../../../../routes/constants"; +import { RevokeDialog } from "./RevokeDialog"; +import { usePermitHistoryQuery } from "../../../hooks/hooks"; +import { calculateNetAmount } from "../../../helpers/feeSummary"; +import { FeeSummary } from "../../../components/feeSummary/FeeSummary"; +import { VoidPermitFormData } from "../types/VoidPermit"; +import { useVoidPermit } from "../hooks/useVoidPermit"; +import { mapToRevokeRequestData } from "../helpers/mapper"; + +const FEATURE = "void-permit"; +const searchRoute = `${SEARCH_RESULTS}?searchEntity=permits`; + +export const VoidPermitForm = ({ + permit, + onRevokeSuccess, +}: { + permit: ReadPermitDto | null; + onRevokeSuccess: () => void; +}) => { + const navigate = useNavigate(); + const [openRevokeDialog, setOpenRevokeDialog] = useState(false); + const { + formMethods, + permitId, + setVoidPermitData, + next, + } = useVoidPermitForm(); + + const { + query: permitHistoryQuery, + permitHistory, + } = usePermitHistoryQuery(permit?.originalPermitId); + + const { + mutation: revokePermitMutation, + voidResults, + } = useVoidPermit(); + + useEffect(() => { + if (voidResults && voidResults.success.length > 0) { + setOpenRevokeDialog(false); + onRevokeSuccess(); + } + }, [voidResults]); + + const amountToRefund = permitHistoryQuery.isInitialLoading + ? 0 : -1 * calculateNetAmount(permitHistory); + + const { + control, + getValues, + handleSubmit, + register, + formState: { errors }, + } = formMethods; + + const handleCancel = () => { + navigate(searchRoute); + }; + + const handleContinue = () => { + const formValues = getValues(); + setVoidPermitData(formValues); + console.log(formValues); // + next(); + }; + + const handleOpenRevokeDialog = () => { + setOpenRevokeDialog(true); + }; + + const handleCancelRevoke = () => { + setOpenRevokeDialog(false); + }; + + const handleRevoke = (revokeData: VoidPermitFormData) => { + revokePermitMutation.mutate({ + permitId, + voidData: mapToRevokeRequestData(revokeData), + }); + }; + + const voidReasonRules = { + required: { + value: true, + message: requiredMessage(), + }, + }; + + return ( + + +
+
+
+ Send Permit and Receipt to +
+
+ + isEmail(email) || invalidEmail(), + }, + }, + label: "Email", + }} + className="void-input void-input--email" + /> + + (fax == null || fax === "") + || (fax != null && fax !== "" && fax.length >= 10 && fax.length <= 20) + || invalidPhoneLength(10, 20), + }, + }, + label: "Fax", + }} + className="void-input void-input--fax" + /> +
+
+ +
+
+ Reason for Voiding +
+
+
+
+ ( + + + {invalid ? ( + + {getErrorMessage(errors, "reason")} + + ) : null} + + )} + /> + +
+
+
+
+ Revoke this permit? +
+
+
+ Revoking a permit is a severe action that cannot be reversed. There are no refunds for revoked permits. +
+ +
+
+
+
+
+
+ +
+
+
+ + +
+
+ + {openRevokeDialog ? ( + + ) : null} +
+
+ ); +}; diff --git a/frontend/src/features/permits/pages/Void/components/VoidPermitHeader.scss b/frontend/src/features/permits/pages/Void/components/VoidPermitHeader.scss new file mode 100644 index 000000000..a844e6bb7 --- /dev/null +++ b/frontend/src/features/permits/pages/Void/components/VoidPermitHeader.scss @@ -0,0 +1,34 @@ +.void-permit__header { + display: flex; + flex-direction: column; + padding: 0 10%; + + .header-title { + padding: 0.25em 0; + } + + .permit-number { + padding: 0.5em 0; + border: none; + font-size: 1.5rem; + font-weight: 600; + + &__label { + margin-right: 0.25em; + } + } + + .permit-info { + display: flex; + flex-direction: row; + + &--start, &--end { + margin-right: 1em; + } + + &__label { + margin-right: 0.25em; + font-weight: 600; + } + } +} diff --git a/frontend/src/features/permits/pages/Void/components/VoidPermitHeader.tsx b/frontend/src/features/permits/pages/Void/components/VoidPermitHeader.tsx new file mode 100644 index 000000000..c3cfb2685 --- /dev/null +++ b/frontend/src/features/permits/pages/Void/components/VoidPermitHeader.tsx @@ -0,0 +1,100 @@ +import { Box, Typography } from "@mui/material"; + +import "./VoidPermitHeader.scss"; +import { ReadPermitDto } from "../../../types/permit"; +import { DATE_FORMATS, toLocal } from "../../../../../common/helpers/formatDate"; +import { CompanyBanner } from "../../../../../common/components/banners/CompanyBanner"; +import { permitTypeDisplayText } from "../../../types/PermitType"; + +export const VoidPermitHeader = ({ + permit, +}: { + permit: ReadPermitDto | null; +}) => { + return permit ? ( +
+ + {permitTypeDisplayText(permit.permitType)} + + + + + Voiding Permit #: + + + {permit.permitNumber} + + + + + + + Permit Start Date: + + + {toLocal(permit.permitData.startDate, DATE_FORMATS.DATEONLY_ABBR_MONTH)} + + + + + Permit End Date: + + + {toLocal(permit.permitData.expiryDate, DATE_FORMATS.DATEONLY_ABBR_MONTH)} + + + {permit.permitData.vehicleDetails?.plate ? ( + + + Plate #: + + + {permit.permitData.vehicleDetails.plate} + + + ) : null} + + + {permit.permitData.clientNumber && permit.permitData.companyName ? ( + + ) : null} +
+ ) : null; +}; diff --git a/frontend/src/features/permits/pages/Void/context/VoidPermitContext.ts b/frontend/src/features/permits/pages/Void/context/VoidPermitContext.ts new file mode 100644 index 000000000..e8649b4ea --- /dev/null +++ b/frontend/src/features/permits/pages/Void/context/VoidPermitContext.ts @@ -0,0 +1,20 @@ +import { createContext } from "react"; +import { VoidPermitFormData } from "../types/VoidPermit"; + +interface VoidPermitContextType { + voidPermitData: VoidPermitFormData; + setVoidPermitData: (data: VoidPermitFormData) => void; + next: () => void; + back: () => void; +} + +export const VoidPermitContext = createContext({ + voidPermitData: { + permitId: "", + reason: "", + revoke: false, + }, + setVoidPermitData: () => undefined, + next: () => undefined, + back: () => undefined, +}); diff --git a/frontend/src/features/permits/pages/Void/helpers/mapper.ts b/frontend/src/features/permits/pages/Void/helpers/mapper.ts new file mode 100644 index 000000000..e8e9c3d40 --- /dev/null +++ b/frontend/src/features/permits/pages/Void/helpers/mapper.ts @@ -0,0 +1,34 @@ +import { PERMIT_STATUSES } from "../../../types/PermitStatus"; +import { RefundFormData } from "../../Refund/types/RefundFormData"; +import { + RevokePermitRequestData, + VoidPermitFormData, + VoidPermitRequestData, +} from "../types/VoidPermit"; + +export const mapToRevokeRequestData = ( + voidPermitFormData: VoidPermitFormData +): RevokePermitRequestData => { + return { + status: PERMIT_STATUSES.REVOKED, + paymentMethodId: "1", // hardcoded to "1" - Web + transactionAmount: 0, + comment: voidPermitFormData.reason, + }; +}; + +export const mapToVoidRequestData = ( + voidPermitFormData: VoidPermitFormData, + refundData: RefundFormData, + amountToRefund: number, +): VoidPermitRequestData => { + return { + status: PERMIT_STATUSES.VOIDED, + pgTransactionId: refundData.transactionId, + paymentMethodId: "1", // hardcoded to "1" - Web + transactionAmount: amountToRefund, + pgPaymentMethod: refundData.refundOnlineMethod ? refundData.refundOnlineMethod : undefined, + pgCardType: refundData.refundCardType ? refundData.refundCardType : undefined, + comment: voidPermitFormData.reason, + }; +}; diff --git a/frontend/src/features/permits/pages/Void/hooks/useVoidPermit.ts b/frontend/src/features/permits/pages/Void/hooks/useVoidPermit.ts new file mode 100644 index 000000000..04af23140 --- /dev/null +++ b/frontend/src/features/permits/pages/Void/hooks/useVoidPermit.ts @@ -0,0 +1,29 @@ +import { useState } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { VoidPermitResponseData } from "../types/VoidPermit"; +import { voidPermit } from "../../../apiManager/permitsAPI"; + +export const useVoidPermit = () => { + const [voidResults, setVoidResults] = useState(undefined); + + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: voidPermit, + retry: false, + onSuccess: (voidResponseData) => { + queryClient.invalidateQueries(["permit"]); + setVoidResults(voidResponseData); + }, + onError: (err: unknown) => { + console.error(err); + setVoidResults(undefined); + }, + }); + + return { + mutation, + voidResults, + }; +}; diff --git a/frontend/src/features/permits/pages/Void/hooks/useVoidPermitForm.ts b/frontend/src/features/permits/pages/Void/hooks/useVoidPermitForm.ts new file mode 100644 index 000000000..1c472b1d7 --- /dev/null +++ b/frontend/src/features/permits/pages/Void/hooks/useVoidPermitForm.ts @@ -0,0 +1,44 @@ +import { useEffect, useContext } from "react"; +import { useForm } from "react-hook-form"; + +import { getDefaultRequiredVal } from "../../../../../common/helpers/util"; +import { VoidPermitFormData } from "../types/VoidPermit"; +import { VoidPermitContext } from "../context/VoidPermitContext"; + +export const useVoidPermitForm = () => { + const { + voidPermitData, + setVoidPermitData, + next, + } = useContext(VoidPermitContext); + + const defaultFormData = { + permitId: voidPermitData.permitId, + reason: getDefaultRequiredVal("", voidPermitData.reason), + revoke: voidPermitData.revoke, + email: getDefaultRequiredVal("", voidPermitData.email), + fax: getDefaultRequiredVal("", voidPermitData.fax), + }; + + const formMethods = useForm({ + defaultValues: defaultFormData, + reValidateMode: "onChange", + }); + + const { setValue } = formMethods; + + useEffect(() => { + setValue("email", getDefaultRequiredVal("", voidPermitData.email)); + }, [voidPermitData.email]); + + useEffect(() => { + setValue("fax", getDefaultRequiredVal("", voidPermitData.fax)); + }, [voidPermitData.fax]); + + return { + permitId: voidPermitData.permitId, + formMethods, + setVoidPermitData, + next, + }; +}; diff --git a/frontend/src/features/permits/pages/Void/types/VoidPermit.ts b/frontend/src/features/permits/pages/Void/types/VoidPermit.ts new file mode 100644 index 000000000..7144c2eba --- /dev/null +++ b/frontend/src/features/permits/pages/Void/types/VoidPermit.ts @@ -0,0 +1,32 @@ +import { BamboraPaymentMethod, CardType } from "../../../types/PaymentMethod"; +import { PermitStatus } from "../../../types/PermitStatus"; +import { PermitsActionResponse } from "../../../types/permit"; + +export interface VoidPermitFormData { + permitId: string; + reason: string; + revoke: boolean; + email?: string; + fax?: string; +} + +export interface VoidPermitRequestData { + status: Extract; + pgTransactionId?: string; + paymentMethodId: string; // hardcoded to "1" - Web + transactionAmount: number; + pgTransactionDate?: string; + pgPaymentMethod?: BamboraPaymentMethod; + pgCardType?: CardType; + comment: string; +} + +export interface RevokePermitRequestData { + status: Extract; + paymentMethodId: string; // hardcoded to "1" - Web + pgPaymentMethod?: BamboraPaymentMethod; + transactionAmount: 0; + comment: string; +} + +export type VoidPermitResponseData = PermitsActionResponse; diff --git a/frontend/src/features/permits/types/PaymentMethod.ts b/frontend/src/features/permits/types/PaymentMethod.ts new file mode 100644 index 000000000..8f3a0e0c9 --- /dev/null +++ b/frontend/src/features/permits/types/PaymentMethod.ts @@ -0,0 +1,202 @@ +export const PAYMENT_METHODS = { + Cash: "Cash", + Cheque: "Cheque", + CreditAccount: "CreditAccount", + GA: "GA", + IcepayAMEX: "IcepayAMEX", + IcepayDebit: "IcepayDebit", + IcepayMastercard: "IcepayMastercard", + IcepayMastercardDebit: "IcepayMastercardDebit", + IcepayVisa: "IcepayVisa", + IcepayVisaDebit: "IcepayVisaDebit", + PoSAMEX: "PoSAMEX", + PoSDebit: "PoSDebit", + PoSMastercard: "PoSMastercard", + PoSMastercardDebit: "PoSMastercardDebit", + PoSVisa: "PoSVisa", + PoSVisaDebit: "PoSVisaDebit", + WebAMEX: "WebAMEX", + WebMastercard: "WebMastercard", + WebMastercardDebit: "WebMastercardDebit", + WebVisa: "WebVisa", + WebVisaDebit: "WebVisaDebit", +} as const; + +export type PaymentMethod = typeof PAYMENT_METHODS[keyof typeof PAYMENT_METHODS]; + +export const REFUND_METHODS = { + Cheque: "Cheque", + CreditAccount: "CreditAccount", + PPCAMEX: "PPCAMEX", + PPCDebit: "PPCDebit", + PPCMastercard: "PPCMastercard", + PPCMastercardDebit: "PPCMastercardDebit", + PPCVisa: "PPCVisa", + PPCVisaDebit: "PPCVisaDebit", +} as const; + +export type RefundMethod = typeof REFUND_METHODS[keyof typeof REFUND_METHODS]; + +export const paymentMethodDisplayText = (paymentMethod: PaymentMethod) => { + switch (paymentMethod) { + case PAYMENT_METHODS.Cash: + return "Cash"; + case PAYMENT_METHODS.Cheque: + return "Cheque"; + case PAYMENT_METHODS.CreditAccount: + return "Credit Account"; + case PAYMENT_METHODS.GA: + return "GA Payment"; + case PAYMENT_METHODS.IcepayAMEX: + return "Icepay - AMEX"; + case PAYMENT_METHODS.IcepayDebit: + return "Icepay - Debit"; + case PAYMENT_METHODS.IcepayMastercard: + return "Icepay - Mastercard"; + case PAYMENT_METHODS.IcepayMastercardDebit: + return "Icepay - Mastercard (Debit)"; + case PAYMENT_METHODS.IcepayVisa: + return "Icepay - Visa"; + case PAYMENT_METHODS.IcepayVisaDebit: + return "Icepay - Visa (Debit)"; + case PAYMENT_METHODS.PoSAMEX: + return "PoS - AMEX"; + case PAYMENT_METHODS.PoSDebit: + return "PoS - Debit"; + case PAYMENT_METHODS.PoSMastercard: + return "PoS - Mastercard"; + case PAYMENT_METHODS.PoSMastercardDebit: + return "PoS - Mastercard (Debit)"; + case PAYMENT_METHODS.PoSVisa: + return "PoS - Visa"; + case PAYMENT_METHODS.PoSVisaDebit: + return "PoS - Visa (Debit)"; + case PAYMENT_METHODS.WebAMEX: + return "Web - AMEX"; + case PAYMENT_METHODS.WebMastercard: + return "Web - Mastercard"; + case PAYMENT_METHODS.WebMastercardDebit: + return "Web - Mastercard (Debit)"; + case PAYMENT_METHODS.WebVisa: + return "Web - Visa"; + case PAYMENT_METHODS.WebVisaDebit: + return "Web - Visa (Debit)"; + } +}; + +export const refundMethodDisplayText = (refundMethod: RefundMethod) => { + switch (refundMethod) { + case REFUND_METHODS.Cheque: + return "Cheque"; + case REFUND_METHODS.CreditAccount: + return "Credit Account"; + case REFUND_METHODS.PPCAMEX: + return "PPC - AMEX"; + case REFUND_METHODS.PPCDebit: + return "PPC - Debit"; + case REFUND_METHODS.PPCMastercard: + return "PPC - Mastercard"; + case REFUND_METHODS.PPCMastercardDebit: + return "PPC - Mastercard (Debit)"; + case REFUND_METHODS.PPCVisa: + return "PPC - Visa"; + case REFUND_METHODS.PPCVisaDebit: + return "PPC - Visa (Debit)"; + } +}; + +export const mapPaymentMethodToRefundMethods = (paymentMethod: PaymentMethod, useCreditAccount?: boolean): RefundMethod => { + switch (paymentMethod) { + case PAYMENT_METHODS.Cash: + case PAYMENT_METHODS.Cheque: + case PAYMENT_METHODS.GA: + return REFUND_METHODS.Cheque; + case PAYMENT_METHODS.CreditAccount: + return useCreditAccount ? REFUND_METHODS.CreditAccount : REFUND_METHODS.Cheque; + case PAYMENT_METHODS.IcepayAMEX: + case PAYMENT_METHODS.PoSAMEX: + case PAYMENT_METHODS.WebAMEX: + return REFUND_METHODS.PPCAMEX; + case PAYMENT_METHODS.IcepayDebit: + case PAYMENT_METHODS.PoSDebit: + return REFUND_METHODS.PPCDebit; + case PAYMENT_METHODS.IcepayMastercard: + case PAYMENT_METHODS.PoSMastercard: + case PAYMENT_METHODS.WebMastercard: + return REFUND_METHODS.PPCMastercard; + case PAYMENT_METHODS.IcepayMastercardDebit: + case PAYMENT_METHODS.PoSMastercardDebit: + case PAYMENT_METHODS.WebMastercardDebit: + return REFUND_METHODS.PPCMastercardDebit; + case PAYMENT_METHODS.IcepayVisa: + case PAYMENT_METHODS.PoSVisa: + case PAYMENT_METHODS.WebVisa: + return REFUND_METHODS.PPCVisa; + case PAYMENT_METHODS.IcepayVisaDebit: + case PAYMENT_METHODS.PoSVisaDebit: + case PAYMENT_METHODS.WebVisaDebit: + return REFUND_METHODS.PPCVisaDebit; + } +}; + +export const BAMBORA_PAYMENT_METHODS = { + CC: "CC", // Credit Card Transaction + IO: "IO", // Interac Online Transaction +} as const; + +export type BamboraPaymentMethod = typeof BAMBORA_PAYMENT_METHODS[keyof typeof BAMBORA_PAYMENT_METHODS]; + +export const CARD_TYPES = { + VI: "VI", // Visa + MC: "MC", // MasterCard + AM: "AM", // American Express + PV: "PV", // Visa Debit + MD: "MD", // Debit MasterCard + IO: "IO", // Interac Online +} as const; + +export type CardType = typeof CARD_TYPES[keyof typeof CARD_TYPES]; + +// Incomplete, needs to confirm and change database schema for payment methods +// since it's insufficient to determine payment method through Bambora payment method and card type alone +export const getPaymentMethod = ( + payMethod?: BamboraPaymentMethod | null, + cardType?: CardType | null +): PaymentMethod | undefined => { + if (payMethod !== BAMBORA_PAYMENT_METHODS.CC) { + return undefined; // Could be either cash, cheque, GA, Credit Account, or Interac/Debit + } + + // Paid by credit card + switch (cardType) { + case CARD_TYPES.AM: + return PAYMENT_METHODS.WebAMEX; + case CARD_TYPES.MC: + return PAYMENT_METHODS.WebMastercard; + case CARD_TYPES.MD: + return PAYMENT_METHODS.WebMastercardDebit; + case CARD_TYPES.PV: + return PAYMENT_METHODS.WebVisaDebit; + case CARD_TYPES.VI: + return PAYMENT_METHODS.WebVisa; + default: + return undefined; // unknown value for Interac payment + } +}; + +export const getRefundMethodByCardType = (cardType?: CardType | null): RefundMethod => { + switch (cardType) { + case CARD_TYPES.AM: + return REFUND_METHODS.PPCAMEX; + case CARD_TYPES.MC: + return REFUND_METHODS.PPCMastercard; + case CARD_TYPES.MD: + return REFUND_METHODS.PPCMastercardDebit; + case CARD_TYPES.PV: + return REFUND_METHODS.PPCVisaDebit; + case CARD_TYPES.VI: + return REFUND_METHODS.PPCVisa; + default: + return REFUND_METHODS.Cheque; + } +}; diff --git a/frontend/src/features/permits/types/PermitHistory.ts b/frontend/src/features/permits/types/PermitHistory.ts new file mode 100644 index 000000000..02b0ae3ed --- /dev/null +++ b/frontend/src/features/permits/types/PermitHistory.ts @@ -0,0 +1,14 @@ +import { BamboraPaymentMethod, CardType } from "./PaymentMethod"; +import { TransactionType } from "./payment"; + +export interface PermitHistory { + permitNumber: string; + comment: string | null; + commentUsername: string; + transactionAmount: number; + transactionOrderNumber: string; + pgTransactionId: string; + pgPaymentMethod: BamboraPaymentMethod | null; + pgCardType: CardType | null; + transactionTypeId: TransactionType; +} diff --git a/frontend/src/features/permits/types/PermitStatus.ts b/frontend/src/features/permits/types/PermitStatus.ts new file mode 100644 index 000000000..ef21ae0b2 --- /dev/null +++ b/frontend/src/features/permits/types/PermitStatus.ts @@ -0,0 +1,24 @@ +export const PERMIT_STATUSES = { + APPROVED: "APPROVED", + AUTO_APPROVED: "AUTO_APPROVED", + CANCELLED: "CANCELLED", + IN_PROGRESS: "IN_PROGRESS", + REJECTED: "REJECTED", + UNDER_REVIEW: "UNDER_REVIEW", + WAITING_APPROVAL: "WAITING_APPROVAL", + WAITING_PAYMENT: "WAITING_PAYMENT", + ISSUED: "ISSUED", + SUPERSEDED: "SUPERSEDED", + REVOKED: "REVOKED", + VOIDED: "VOIDED", +} as const; + +export const PERMIT_EXPIRED = "EXPIRED"; + +export type PermitStatus = typeof PERMIT_STATUSES[keyof typeof PERMIT_STATUSES]; + +export const isPermitInactive = (permitStatus?: string) => { + return permitStatus === PERMIT_STATUSES.VOIDED + || permitStatus === PERMIT_STATUSES.REVOKED + || permitStatus === PERMIT_STATUSES.SUPERSEDED; +}; diff --git a/frontend/src/features/permits/types/PermitType.ts b/frontend/src/features/permits/types/PermitType.ts new file mode 100644 index 000000000..3c752ef0c --- /dev/null +++ b/frontend/src/features/permits/types/PermitType.ts @@ -0,0 +1,101 @@ +export const PERMIT_TYPES = { + EPTOP: "EPTOP", + HC: "HC", + LCV: "LCV", + MFP: "MFP", + NRQBS: "NRQBS", + NRQCL: "NRQCL", + NRQCV: "NRQCV", + NRQFT: "NRQFT", + NRQFV: "NRQFV", + NRQXP: "NRQXP", + NRSBS: "NRSBS", + NRSCL: "NRSCL", + NRSCV: "NRSCV", + NRSFT: "NRSFT", + NRSFV: "NRSFV", + NRSXP: "NRSXP", + RIG: "RIG", + STOS: "STOS", + STOW: "STOW", + STWS: "STWS", + TRAX: "TRAX", + TROS: "TROS", + TROW: "TROW", +} as const; + +export type PermitType = typeof PERMIT_TYPES[keyof typeof PERMIT_TYPES]; + +/** + * Returns the name/description of the permit type. + * @param permitType String (if any) that represents the permit type + * @returns Name/description of the permit type, or empty string if no mapping exists for permit type + */ +export const getPermitTypeName = (permitType?: string | null) => { + switch (permitType) { + case PERMIT_TYPES.EPTOP: + return "Extra-Provincial Temporary Operating"; + case PERMIT_TYPES.HC: + return "Highway Crossing"; + case PERMIT_TYPES.LCV: + return "Long Combination Vehicle"; + case PERMIT_TYPES.MFP: + return "Motive Fuel User"; + case PERMIT_TYPES.NRQBS: + return "Quarterly Non Resident Reg. / Ins. - Bus"; + case PERMIT_TYPES.NRQCL: + return "Non Resident Quarterly Conditional License"; + case PERMIT_TYPES.NRQCV: + return "Quarterly Non Resident Reg. / Ins. - Comm Vehicle"; + case PERMIT_TYPES.NRQFT: + return "Non Resident Quarterly Farm Tractor"; + case PERMIT_TYPES.NRQFV: + return "Quarterly Non Resident Reg. / Ins. - Farm Vehicle"; + case PERMIT_TYPES.NRQXP: + return "Non Resident Quarterly X Plated"; + case PERMIT_TYPES.NRSBS: + return "Single Trip Non-Resident Registration / Insurance - Buses"; + case PERMIT_TYPES.NRSCL: + return "Non Resident Single Trip Conditional License"; + case PERMIT_TYPES.NRSCV: + return "Single Trip Non-Resident Reg. / Ins. - Commercial Vehicle"; + case PERMIT_TYPES.NRSFT: + return "Non Resident Farm Tractor Single Trip"; + case PERMIT_TYPES.NRSFV: + return "Single Trip Non-Resident Reg. / Ins. - Farm Vehicle"; + case PERMIT_TYPES.NRSXP: + return "Non Resident Single Trip X Plated Vehicle"; + case PERMIT_TYPES.RIG: + return "Rig Move"; + case PERMIT_TYPES.STOS: + return "Single Trip Oversize"; + case PERMIT_TYPES.STOW: + return "Single Trip Over Weight"; + case PERMIT_TYPES.STWS: + return "Single Trip Overweight Oversize"; + case PERMIT_TYPES.TRAX: + return "Term Axle Overweight"; + case PERMIT_TYPES.TROS: + return "Term Oversize"; + case PERMIT_TYPES.TROW: + return "Term Overweight"; + default: + return ""; + } +}; + +/** + * Gets display text for permit type. + * @param permitType Permit type (eg. TROS, STOS, etc) + * @returns display text for the permit type + */ +export const permitTypeDisplayText = (permitType?: string) => { + switch (permitType) { + case PERMIT_TYPES.TROS: + return "Oversize: Term"; + case PERMIT_TYPES.STOS: + return "Oversize: Single Trip"; + default: + return getPermitTypeName(permitType); + } +}; diff --git a/frontend/src/features/permits/types/application.d.ts b/frontend/src/features/permits/types/application.d.ts index 7cae1da5b..0981b2d20 100644 --- a/frontend/src/features/permits/types/application.d.ts +++ b/frontend/src/features/permits/types/application.d.ts @@ -1,5 +1,8 @@ import { Dayjs } from "dayjs"; +import { PermitStatus, PERMIT_STATUSES } from "./PermitStatus"; +import { PermitType } from "./PermitType"; + /** * A type that replaces all direct entries with Dayjs types to string types. * @@ -15,16 +18,17 @@ type ReplaceDayjsWithString = { [K in keyof T]: T[K] extends Dayjs ? string : (T[K] extends (Dayjs | undefined) ? (string | undefined) : T[K]); }; -export type PermitType = "STOS" | "TROS"; type PermitApplicationOrigin = "ONLINE" | "PPC"; + type PermitApprovalSource = "AUTO" | "PPC" | "TPS"; -export type PermitStatus = "APPROVED" | "AUTO_APPROVED" | "CANCELLED" | "IN_PROGRESS" | "REJECTED" | "UNDER_REVIEW" | "WAITING_APPROVAL" | "WAITING_PAYMENT" | "ISSUED"; /** * A base permit type. This is an incomplete object and meant to be extended for use. */ export interface Application { permitId?: string; + originalPermitId?: string; + comment?: string; permitStatus?: PermitStatus; companyId: number; userGuid?: string | null; @@ -122,7 +126,7 @@ export interface PermitApplicationInProgress { permitData: ReplaceDayjsWithString; permitId: string permitNumber?: string | null; - permitStatus: "IN_PROGRESS"; + permitStatus: typeof PERMIT_STATUSES.IN_PROGRESS; permitType: PermitType; updatedDateTime: string; userGuid: string; diff --git a/frontend/src/features/permits/types/payment.d.ts b/frontend/src/features/permits/types/payment.d.ts index b07b8dec4..8cc100d8f 100644 --- a/frontend/src/features/permits/types/payment.d.ts +++ b/frontend/src/features/permits/types/payment.d.ts @@ -1,3 +1,5 @@ +import { BamboraPaymentMethod, CardType } from "./PaymentMethod"; + export interface MotiPaymentDetails { authCode: string; avsAddrMatch: string; @@ -6,12 +8,12 @@ export interface MotiPaymentDetails { avsPostalMatch: string; avsProcessed: string; avsResult: string; - cardType: string; + cardType: CardType; cvdId: number; trnApproved: number; messageId: string; messageText: string; - paymentMethod: string; + paymentMethod: BamboraPaymentMethod; ref1: string; ref2: string; ref3: string; @@ -41,7 +43,7 @@ export interface Transaction { transactionDate: string; cvdId: number; paymentMethod: string; - paymentMethodId: number; // TODO: what is this? + paymentMethodId: number; messageId: string; messageText: string; } @@ -49,4 +51,58 @@ export interface Transaction { export interface PermitTransaction { permitId: string; transactionId: number; -} \ No newline at end of file +} + +export const TRANSACTION_TYPES = { + P: "P", + R: "R", + VP: "VP", + VR: "VR", + PA: "PA", + PAC: "PAC", + Q: "Q", + Z: "Z", +} as const; + +export type TransactionType = typeof TRANSACTION_TYPES[keyof typeof TRANSACTION_TYPES]; + +export interface PaymentGatewayData { + pgTransactionId: string; + pgApproved: number; + pgAuthCode: string; + pgCardType: CardType; + pgTransactionDate: string; + pgCvdId: number; + pgPaymentMethod: BamboraPaymentMethod; + pgMessageId: number; + pgMessageText: string; +} + +export interface StartTransactionRequestData extends Partial { + transactionTypeId: TransactionType; + paymentMethodId: string; + applicationDetails: { + applicationId: string; + transactionAmount: number; + }[]; +} + +export interface StartTransactionResponseData extends Partial { + transactionId: string; + transactionTypeId: TransactionType; + paymentMethodId: string; + totalTransactionAmount: number; + transactionSubmitDate: string; + transactionOrderNumber: string; + applicationDetails: { + applicationId: string; + transactionAmount: number; + }[]; + url?: string; +} + +export type CompleteTransactionRequestData = PaymentGatewayData; + +export interface CompleteTransactionResponseData extends PaymentGatewayData { + transactionid: string; +} diff --git a/frontend/src/features/permits/types/permit.d.ts b/frontend/src/features/permits/types/permit.d.ts index f4c38632a..7341f6d0d 100644 --- a/frontend/src/features/permits/types/permit.d.ts +++ b/frontend/src/features/permits/types/permit.d.ts @@ -1,5 +1,8 @@ import { Dayjs } from "dayjs"; +import { PermitStatus } from "./PermitStatus"; +import { PermitType } from "./PermitType"; + /** * A type that replaces all direct entries with Dayjs types to string types. * @@ -24,10 +27,14 @@ type ReplaceDayjsWithString = { */ interface PartialPermitType { permitId: number; - permitStatus: string; + originalPermitId: string; + revision: number; + previousRevision?: number | null; + comment?: string | null; + permitStatus: PermitStatus; companyId: number; userGuid?: string | null; - permitType: string; + permitType: PermitType; applicationNumber: string; permitNumber: string; permitApprovalSource: string; @@ -111,4 +118,17 @@ interface TermOversizeApplication { mailingAddress: MailingAddress; feeSummary: string; companyName?: string; + clientNumber?: string; +} + +export interface PermitsActionResponse { + success: string[]; + failure: string[]; } + +export interface IssuePermitRequest { + applicationIds: string[]; + companyId?: number; +} + +export type IssuePermitsResponse = PermitsActionResponse; diff --git a/frontend/src/features/wizard/UserInfoWizard.tsx b/frontend/src/features/wizard/UserInfoWizard.tsx index 211fe663e..18600d8c6 100644 --- a/frontend/src/features/wizard/UserInfoWizard.tsx +++ b/frontend/src/features/wizard/UserInfoWizard.tsx @@ -13,7 +13,7 @@ import { ErrorFallback } from "../../common/pages/ErrorFallback"; import { createMyOnRouteBCUserProfile } from "../manageProfile/apiManager/manageProfileAPI"; import { ReusableUserInfoForm } from "../manageProfile/components/forms/common/ReusableUserInfoForm"; import { UserInformation } from "../manageProfile/types/manageProfile"; -import { BCeIDAuthGroup } from "../manageProfile/types/userManagement.d"; +import { BCEID_AUTH_GROUP } from "../manageProfile/types/userManagement.d"; import { OnRouteBCProfileCreated } from "./pages/OnRouteBCProfileCreated"; /** @@ -26,7 +26,7 @@ export const UserInfoWizard = React.memo(() => { >({ defaultValues: { // Remove this userAuthGroup once backend integrates the auth group. - userAuthGroup: BCeIDAuthGroup.CVCLIENT as string, + userAuthGroup: BCEID_AUTH_GROUP.CVCLIENT as string, }, }); diff --git a/frontend/src/features/wizard/components/dashboard/CreateProfileSteps.tsx b/frontend/src/features/wizard/components/dashboard/CreateProfileSteps.tsx index fa180d8dd..f9dbc5e28 100644 --- a/frontend/src/features/wizard/components/dashboard/CreateProfileSteps.tsx +++ b/frontend/src/features/wizard/components/dashboard/CreateProfileSteps.tsx @@ -24,6 +24,7 @@ import { CompanyAndUserRequest } from "../../../manageProfile/types/manageProfil import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext"; import { SnackBarContext } from "../../../../App"; import { getDefaultRequiredVal } from "../../../../common/helpers/util"; +import { BCEID_AUTH_GROUP } from "../../../manageProfile/types/userManagement.d"; const CompanyBanner = ({ legalName }: { legalName: string }) => { return ( @@ -138,7 +139,7 @@ export const CreateProfileSteps = React.memo(() => { extension: "", fax: "", adminUser: { - userAuthGroup: "ORGADMIN", + userAuthGroup: BCEID_AUTH_GROUP.ORGADMIN, firstName: "", lastName: "", email: "", diff --git a/frontend/src/routes/Routes.tsx b/frontend/src/routes/Routes.tsx index 9ea0a0e92..f4e933144 100644 --- a/frontend/src/routes/Routes.tsx +++ b/frontend/src/routes/Routes.tsx @@ -21,6 +21,7 @@ import { EditUserDashboard } from "../features/manageProfile/pages/EditUserDashb import { IDIRSearchResultsDashboard } from "../features/idir/search/pages/IDIRSearchResultsDashboard"; import { IDIRWelcome } from "../features/idir/IDIRWelcome"; import { UserInfoWizard } from "../features/wizard/UserInfoWizard"; +import { VoidPermit } from "../features/permits/pages/Void/VoidPermit"; export const AppRoutes = () => { return ( @@ -112,6 +113,12 @@ export const AppRoutes = () => { /> + }> + } + /> + }> } /> diff --git a/frontend/src/routes/constants.tsx b/frontend/src/routes/constants.tsx index 9094d3fe0..ef90f9410 100644 --- a/frontend/src/routes/constants.tsx +++ b/frontend/src/routes/constants.tsx @@ -13,6 +13,8 @@ export const EDIT_USER = `/${MANAGE_PROFILES}/edit-user`; // Permits export const PERMITS = "permits"; +export const PERMIT_VOID = "void"; +export const PERMIT_AMEND = "amend"; // Applications export const APPLICATIONS = "applications";