From ba076e0ac58b4e09c5ec7535f532468d0574075a Mon Sep 17 00:00:00 2001 From: Krishnan Subramanian <84348052+krishnan-aot@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:08:28 -0700 Subject: [PATCH] ORV2-2073 Shopping Cart APIs (#1320) Co-authored-by: praju-aot --- .../versions/revert/v_26_ddl_revert.sql | 1 - vehicles/src/app.module.ts | 2 + .../helper/permit-application.helper.ts | 37 +++ .../application/application.service.ts | 29 -- .../permit/dto/response/read-permit.dto.ts | 2 +- .../dto/request/add-to-shopping-cart.dto.ts | 22 ++ .../pathParam/companyId.path-param.dto.ts | 14 + .../dto/request/update-shopping-cart.dto.ts | 3 + .../dto/response/read-shopping-cart.dto.ts | 90 ++++++ .../shopping-cart/dto/response/result.dto.ts | 5 + .../profile/shopping-cart.profile.ts | 75 +++++ .../shopping-cart/shopping-cart.controller.ts | 168 +++++++++++ .../shopping-cart/shopping-cart.module.ts | 17 ++ .../shopping-cart/shopping-cart.service.ts | 264 ++++++++++++++++++ 14 files changed, 698 insertions(+), 31 deletions(-) create mode 100644 vehicles/src/modules/shopping-cart/dto/request/add-to-shopping-cart.dto.ts create mode 100644 vehicles/src/modules/shopping-cart/dto/request/pathParam/companyId.path-param.dto.ts create mode 100644 vehicles/src/modules/shopping-cart/dto/request/update-shopping-cart.dto.ts create mode 100644 vehicles/src/modules/shopping-cart/dto/response/read-shopping-cart.dto.ts create mode 100644 vehicles/src/modules/shopping-cart/dto/response/result.dto.ts create mode 100644 vehicles/src/modules/shopping-cart/profile/shopping-cart.profile.ts create mode 100644 vehicles/src/modules/shopping-cart/shopping-cart.controller.ts create mode 100644 vehicles/src/modules/shopping-cart/shopping-cart.module.ts create mode 100644 vehicles/src/modules/shopping-cart/shopping-cart.service.ts diff --git a/database/mssql/scripts/versions/revert/v_26_ddl_revert.sql b/database/mssql/scripts/versions/revert/v_26_ddl_revert.sql index 405fcb762..a1574cfcc 100644 --- a/database/mssql/scripts/versions/revert/v_26_ddl_revert.sql +++ b/database/mssql/scripts/versions/revert/v_26_ddl_revert.sql @@ -44,4 +44,3 @@ ELSE BEGIN PRINT 'The database update failed' END GO - diff --git a/vehicles/src/app.module.ts b/vehicles/src/app.module.ts index f84791414..b7fab5bbd 100644 --- a/vehicles/src/app.module.ts +++ b/vehicles/src/app.module.ts @@ -34,6 +34,7 @@ import { CompanySuspendModule } from './modules/company-user-management/company- import { PermitModule } from './modules/permit-application-payment/permit/permit.module'; import { ApplicationModule } from './modules/permit-application-payment/application/application.module'; import { PaymentModule } from './modules/permit-application-payment/payment/payment.module'; +import { ShoppingCartModule } from './modules/shopping-cart/shopping-cart.module'; const envPath = path.resolve(process.cwd() + '/../'); @@ -91,6 +92,7 @@ const envPath = path.resolve(process.cwd() + '/../'); PendingUsersModule, AuthModule, PaymentModule, + ShoppingCartModule, ApplicationModule, //! Application Module should be imported before PermitModule to avoid URI conflict PermitModule, FeatureFlagsModule, diff --git a/vehicles/src/common/helper/permit-application.helper.ts b/vehicles/src/common/helper/permit-application.helper.ts index 499e70a05..8c8b860ea 100644 --- a/vehicles/src/common/helper/permit-application.helper.ts +++ b/vehicles/src/common/helper/permit-application.helper.ts @@ -10,6 +10,14 @@ import { callDatabaseSequence } from './database.helper'; import { PermitApplicationOrigin as PermitApplicationOriginEnum } from '../enum/permit-application-origin.enum'; import { PermitApprovalSource as PermitApprovalSourceEnum } from '../enum/permit-approval-source.enum'; import { randomInt } from 'crypto'; +import { Directory } from '../enum/directory.enum'; +import { doesUserHaveAuthGroup } from './auth.helper'; +import { + IDIR_USER_AUTH_GROUP_LIST, + UserAuthGroup, +} from '../enum/user-auth-group.enum'; +import { PPC_FULL_TEXT } from '../constants/api.constant'; +import { User } from '../../modules/company-user-management/users/entities/user.entity'; /** * Fetches and resolves various types of names associated with a permit using cache. @@ -189,3 +197,32 @@ export const generatePermitNumber = async ( const permitNumber = `P${approvalSourceId}-${sequence}-${randomNumber}${revision}`; return permitNumber; }; + +/** + * Determines the appropriate display name for the applicant based on their directory type and the + * current user's authorization group. + * - For users from the IDIR directory, it returns the user's username if the current user has the + * correct authorization group. Otherwise, it returns a predefined full text constant. + * - For users from other directories, it returns the user's first and last name, concatenated. + * @param applicationOwner The user object representing the owner of the application. + * @param currentUserAuthGroup The authorization group of the current user. + * @returns The display name of the application owner as a string. + */ +export const getApplicantDisplay = ( + applicationOwner: User, + currentUserAuthGroup: UserAuthGroup, +): string => { + if (applicationOwner?.directory === Directory.IDIR) { + if ( + doesUserHaveAuthGroup(currentUserAuthGroup, IDIR_USER_AUTH_GROUP_LIST) + ) { + return applicationOwner?.userName; + } else { + return PPC_FULL_TEXT; + } + } else { + const firstName = applicationOwner?.userContact?.firstName ?? ''; + const lastName = applicationOwner?.userContact?.lastName ?? ''; + return (firstName + ' ' + lastName).trim(); + } +}; diff --git a/vehicles/src/modules/permit-application-payment/application/application.service.ts b/vehicles/src/modules/permit-application-payment/application/application.service.ts index 7d0554634..b13f119aa 100644 --- a/vehicles/src/modules/permit-application-payment/application/application.service.ts +++ b/vehicles/src/modules/permit-application-payment/application/application.service.ts @@ -196,35 +196,6 @@ export class ApplicationService { }); } - private async findOneWithSuccessfulTransaction( - applicationId: string, - companyId?: number, - ): Promise { - const permitQB = this.permitRepository.createQueryBuilder('permit'); - permitQB - .leftJoinAndSelect('permit.company', 'company') - .innerJoinAndSelect('permit.permitData', 'permitData') - .innerJoinAndSelect('permit.permitTransactions', 'permitTransactions') - .innerJoinAndSelect('permitTransactions.transaction', 'transaction') - .innerJoinAndSelect('transaction.receipt', 'receipt') - .leftJoinAndSelect('permit.applicationOwner', 'applicationOwner') - .leftJoinAndSelect( - 'applicationOwner.userContact', - 'applicationOwnerContact', - ) - .where('permit.permitId = :permitId', { - permitId: applicationId, - }) - .andWhere('receipt.receiptNumber IS NOT NULL'); - - if (companyId) { - permitQB.andWhere('company.companyId = :companyId', { - companyId: companyId, - }); - } - return await permitQB.getOne(); - } - /** * Finds multiple permits by application IDs or a single transaction ID with successful transactions, * optionally filtering by companyId. diff --git a/vehicles/src/modules/permit-application-payment/permit/dto/response/read-permit.dto.ts b/vehicles/src/modules/permit-application-payment/permit/dto/response/read-permit.dto.ts index 0d2db70d3..878377426 100644 --- a/vehicles/src/modules/permit-application-payment/permit/dto/response/read-permit.dto.ts +++ b/vehicles/src/modules/permit-application-payment/permit/dto/response/read-permit.dto.ts @@ -75,7 +75,7 @@ export class ReadPermitDto { @AutoMap() @ApiProperty({ - description: 'Satus of Permit/Permit Application', + description: 'Status of Permit/Permit Application', example: PermitStatus.ISSUED, required: false, }) diff --git a/vehicles/src/modules/shopping-cart/dto/request/add-to-shopping-cart.dto.ts b/vehicles/src/modules/shopping-cart/dto/request/add-to-shopping-cart.dto.ts new file mode 100644 index 000000000..204296d22 --- /dev/null +++ b/vehicles/src/modules/shopping-cart/dto/request/add-to-shopping-cart.dto.ts @@ -0,0 +1,22 @@ +import { AutoMap } from '@automapper/classes'; +import { ApiProperty } from '@nestjs/swagger'; +import { ArrayMinSize, IsNumberString } from 'class-validator'; + +/** + * DTO (Data Transfer Object) for creating a shopping cart. + * It includes the necessary properties and validations for the creation process. + * + * @property {string[]} applicationIds - Application Ids to be added to the cart. Should be an array of number strings without symbols. + */ +export class AddToShoppingCartDto { + @AutoMap() + @ApiProperty({ + description: 'Application Ids to be added to the cart.', + isArray: true, + type: String, + example: ['74'], + }) + @ArrayMinSize(1) + @IsNumberString({ no_symbols: true }, { each: true }) + applicationIds: string[]; +} diff --git a/vehicles/src/modules/shopping-cart/dto/request/pathParam/companyId.path-param.dto.ts b/vehicles/src/modules/shopping-cart/dto/request/pathParam/companyId.path-param.dto.ts new file mode 100644 index 000000000..7c6a269f2 --- /dev/null +++ b/vehicles/src/modules/shopping-cart/dto/request/pathParam/companyId.path-param.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsInt, IsPositive } from 'class-validator'; + +export class CompanyIdPathParamDto { + @ApiProperty({ + description: `Id of the company the cart belongs to.`, + example: 74, + }) + @IsInt() + @IsPositive() + @Type(() => Number) + companyId: number; +} diff --git a/vehicles/src/modules/shopping-cart/dto/request/update-shopping-cart.dto.ts b/vehicles/src/modules/shopping-cart/dto/request/update-shopping-cart.dto.ts new file mode 100644 index 000000000..32d0237b4 --- /dev/null +++ b/vehicles/src/modules/shopping-cart/dto/request/update-shopping-cart.dto.ts @@ -0,0 +1,3 @@ +import { AddToShoppingCartDto } from './add-to-shopping-cart.dto'; + +export class UpdateShoppingCartDto extends AddToShoppingCartDto {} diff --git a/vehicles/src/modules/shopping-cart/dto/response/read-shopping-cart.dto.ts b/vehicles/src/modules/shopping-cart/dto/response/read-shopping-cart.dto.ts new file mode 100644 index 000000000..6d1a07d36 --- /dev/null +++ b/vehicles/src/modules/shopping-cart/dto/response/read-shopping-cart.dto.ts @@ -0,0 +1,90 @@ +import { AutoMap } from '@automapper/classes'; +import { ApiProperty } from '@nestjs/swagger'; +import { ApplicationStatus } from '../../../../common/enum/application-status.enum'; +import { PermitType } from '../../../../common/enum/permit-type.enum'; + +export class ReadShoppingCartDto { + @AutoMap() + @ApiProperty({ + description: 'Id of the company requesting the permit.', + example: 74, + }) + companyId: number; + + @AutoMap() + @ApiProperty({ + description: 'Id of the application.', + example: 74, + }) + applicationId: string; + + @AutoMap() + @ApiProperty({ + enum: PermitType, + description: 'The permit type abbreviation.', + example: PermitType.TERM_OVERSIZE, + }) + permitType: PermitType; + + @AutoMap() + @ApiProperty({ + example: 'A2-00000002-120', + description: 'Unique formatted permit application number.', + }) + applicationNumber: string; + + @AutoMap() + @ApiProperty({ + description: 'Status of Permit/Permit Application', + example: ApplicationStatus.IN_CART, + required: false, + }) + permitStatus: ApplicationStatus.IN_CART; + + @AutoMap() + @ApiProperty({ + description: 'Application updated Date and Time.', + }) + updatedDateTime: string; + + @AutoMap() + @ApiProperty({ + description: 'Name of the applicant', + example: 'John Smith', + }) + applicant: string; + + @AutoMap() + @ApiProperty({ + description: 'The userGUID of the applicant', + example: 'S822OKE22LK', + }) + applicantGUID: string; + + @AutoMap() + @ApiProperty({ + description: 'The vehicle plate', + example: 'S822OK', + }) + plate: string; + + @AutoMap() + @ApiProperty({ + description: 'The permit start date.', + example: '2023-06-05T19:12:22Z', + }) + startDate: string; + + @AutoMap() + @ApiProperty({ + description: 'The permit start date.', + example: '2023-07-04T19:12:22Z', + }) + expiryDate: string; + + @ApiProperty({ + description: 'The permit fee', + example: 200, + }) + fee: number; +} diff --git a/vehicles/src/modules/shopping-cart/dto/response/result.dto.ts b/vehicles/src/modules/shopping-cart/dto/response/result.dto.ts new file mode 100644 index 000000000..f415fe2d8 --- /dev/null +++ b/vehicles/src/modules/shopping-cart/dto/response/result.dto.ts @@ -0,0 +1,5 @@ +export class ResultDto { + success: string[]; + + failure: string[]; +} diff --git a/vehicles/src/modules/shopping-cart/profile/shopping-cart.profile.ts b/vehicles/src/modules/shopping-cart/profile/shopping-cart.profile.ts new file mode 100644 index 000000000..763b134b5 --- /dev/null +++ b/vehicles/src/modules/shopping-cart/profile/shopping-cart.profile.ts @@ -0,0 +1,75 @@ +import { + Mapper, + createMap, + forMember, + mapFrom, + mapWithArguments, +} from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; + +import { UserAuthGroup } from '../../../common/enum/user-auth-group.enum'; +import { getApplicantDisplay } from '../../../common/helper/permit-application.helper'; +import { Permit as Application } from '../../permit-application-payment/permit/entities/permit.entity'; +import { ReadShoppingCartDto } from '../dto/response/read-shopping-cart.dto'; +import { PermitData } from '../../../common/interface/permit.template.interface'; + +@Injectable() +export class ShoppingCartProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper: Mapper) => { + createMap( + mapper, + Application, + ReadShoppingCartDto, + forMember( + (d) => d.companyId, + mapWithArguments((_s, { companyId }) => companyId), + ), + // permitId + forMember( + (d) => d.applicationId, + mapFrom((s) => s?.permitId), + ), + forMember( + (d) => d.applicant, + mapWithArguments((s, { currentUserAuthGroup }) => { + return getApplicantDisplay( + s.applicationOwner, + currentUserAuthGroup as UserAuthGroup, + ); + }), + ), + forMember( + (d) => d.applicantGUID, + mapFrom((s) => s?.applicationOwner?.userGUID), + ), + forMember( + (d) => d.fee, + mapFrom((s) => { + const parsedPermitData = JSON.parse( + s?.permitData?.permitData, + ) as PermitData; + return +parsedPermitData?.feeSummary; + }), + ), + forMember( + (d) => d.plate, + mapFrom((s) => s?.permitData?.plate), + ), + forMember( + (d) => d.startDate, + mapFrom((s) => s?.permitData?.startDate), + ), + forMember( + (d) => d.expiryDate, + mapFrom((s) => s?.permitData?.expiryDate), + ), + ); + }; + } +} diff --git a/vehicles/src/modules/shopping-cart/shopping-cart.controller.ts b/vehicles/src/modules/shopping-cart/shopping-cart.controller.ts new file mode 100644 index 000000000..e27353013 --- /dev/null +++ b/vehicles/src/modules/shopping-cart/shopping-cart.controller.ts @@ -0,0 +1,168 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Req, +} from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiBearerAuth, + ApiCreatedResponse, + ApiInternalServerErrorResponse, + ApiMethodNotAllowedResponse, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { Request } from 'express'; +import { Roles } from '../../common/decorator/roles.decorator'; +import { Role } from '../../common/enum/roles.enum'; +import { ExceptionDto } from '../../common/exception/exception.dto'; +import { IUserJWT } from '../../common/interface/user-jwt.interface'; +import { AddToShoppingCartDto } from './dto/request/add-to-shopping-cart.dto'; +import { CompanyIdPathParamDto } from './dto/request/pathParam/companyId.path-param.dto'; +import { UpdateShoppingCartDto } from './dto/request/update-shopping-cart.dto'; +import { ReadShoppingCartDto } from './dto/response/read-shopping-cart.dto'; +import { ResultDto } from './dto/response/result.dto'; +import { ShoppingCartService } from './shopping-cart.service'; + +@ApiBearerAuth() +@ApiTags('Shopping Cart') +@Controller('companies/:companyId/shopping-cart') +@ApiMethodNotAllowedResponse({ + description: 'The Application Api Method Not Allowed Response', + type: ExceptionDto, +}) +@ApiInternalServerErrorResponse({ + description: 'The Application Api Internal Server Error Response', + type: ExceptionDto, +}) +export class ShoppingCartController { + constructor(private readonly shoppingCartService: ShoppingCartService) {} + + /** + * Adds a new item to the shopping cart. + * + * @param { companyId } - The companyId path parameter. + * @param { applicationIds } - The DTO to create a new shopping cart item. + * @returns The result of the creation operation. + */ + @ApiOperation({ + summary: 'Adds one or more applications to the shopping cart.', + description: + 'Adds one or more applications to the shopping cart, enforcing authentication.', + }) + @ApiCreatedResponse({ + description: 'The result of the changes to cart.', + type: ResultDto, + }) + @Post() + @Roles(Role.WRITE_PERMIT) + async addToCart( + @Req() request: Request, + @Param() { companyId }: CompanyIdPathParamDto, + @Body() { applicationIds }: AddToShoppingCartDto, + ): Promise { + return await this.shoppingCartService.addToCart(request.user as IUserJWT, { + applicationIds, + companyId, + }); + } + + /** + * Retrieves applications within the shopping cart based on the user's permissions and optional query parameters. + * + * @param request - The incoming request object, used to extract the user's authentication details. + * @param companyIdPathParamDto - DTO containing companyId path parameter. + * @returns A promise resolved with the applications found in the shopping cart for the authenticated user. + */ + @ApiOperation({ + summary: 'Returns the applications in the shopping cart.', + description: + 'Returns one or more applications from the shopping cart, enforcing authentication.', + }) + @ApiBadRequestResponse({ + description: 'Bad Request Response.', + type: ExceptionDto, + }) + @ApiOkResponse({ + description: 'The result of the changes to cart.', + type: ResultDto, + }) + @Get() + @Roles(Role.WRITE_PERMIT) + async getApplicationsInCart( + @Req() request: Request, + @Param() { companyId }: CompanyIdPathParamDto, + ): Promise { + return await this.shoppingCartService.findApplicationsInCart( + request.user as IUserJWT, + companyId, + ); + } + + /** + * Retrieves applications within the shopping cart based on the user's permissions and optional query parameters. + * + * @param request - The incoming request object, used to extract the user's authentication details. + * @param { companyId } - The companyId path parameter. + * @returns A promise resolved with the applications found in the shopping cart for the authenticated user. + */ + @ApiOperation({ + summary: 'Returns the number of applications in the shopping cart.', + description: + 'Returns the number of applications from the shopping cart, enforcing authentication.', + }) + @ApiBadRequestResponse({ + description: 'Bad Request Response.', + type: ExceptionDto, + }) + @ApiOkResponse({ + description: 'The result of the changes to cart.', + type: ResultDto, + }) + @Get('count') + @Roles(Role.WRITE_PERMIT) + async getCartCount( + @Req() request: Request, + @Param() { companyId }: CompanyIdPathParamDto, + ): Promise { + return await this.shoppingCartService.getCartCount( + request.user as IUserJWT, + companyId, + ); + } + + /** + * Removes one or more applications from the shopping cart. + * + * @param { companyId } - The companyId path parameter. + * @param { applicationIds } - The DTO to update a shopping cart. + * @returns The result of the removal operation. + */ + @Delete() + @Roles(Role.WRITE_PERMIT) + @ApiOperation({ + summary: 'Removes one or more applications from the shopping cart.', + description: + 'Removes one or more applications from the shopping cart, enforcing authentication.', + }) + @ApiOkResponse({ + description: 'The result of the changes to cart.', + type: ResultDto, + status: 200, + }) + async removeFromCart( + @Req() request: Request, + @Param() { companyId }: CompanyIdPathParamDto, + @Body() { applicationIds }: UpdateShoppingCartDto, + ): Promise { + return await this.shoppingCartService.remove(request.user as IUserJWT, { + companyId, + applicationIds, + }); + } +} diff --git a/vehicles/src/modules/shopping-cart/shopping-cart.module.ts b/vehicles/src/modules/shopping-cart/shopping-cart.module.ts new file mode 100644 index 000000000..9a3e0b93d --- /dev/null +++ b/vehicles/src/modules/shopping-cart/shopping-cart.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ShoppingCartController } from './shopping-cart.controller'; +import { ShoppingCartService } from './shopping-cart.service'; +import { Permit as Application } from '../permit-application-payment/permit/entities/permit.entity'; +import { PermitData as ApplicationData } from '../permit-application-payment/permit/entities/permit-data.entity'; +import { PermitType } from '../permit-application-payment/permit/entities/permit-type.entity'; +import { ShoppingCartProfile } from './profile/shopping-cart.profile'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Application, ApplicationData, PermitType]), + ], + controllers: [ShoppingCartController], + providers: [ShoppingCartService, ShoppingCartProfile], +}) +export class ShoppingCartModule {} diff --git a/vehicles/src/modules/shopping-cart/shopping-cart.service.ts b/vehicles/src/modules/shopping-cart/shopping-cart.service.ts new file mode 100644 index 000000000..ec78c4614 --- /dev/null +++ b/vehicles/src/modules/shopping-cart/shopping-cart.service.ts @@ -0,0 +1,264 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { NotBrackets, Repository, SelectQueryBuilder } from 'typeorm'; +import { LogAsyncMethodExecution } from '../../common/decorator/log-async-method-execution.decorator'; +import { ApplicationStatus } from '../../common/enum/application-status.enum'; +import { Directory } from '../../common/enum/directory.enum'; +import { + ClientUserAuthGroup, + IDIR_USER_AUTH_GROUP_LIST, + UserAuthGroup, +} from '../../common/enum/user-auth-group.enum'; +import { Permit as Application } from '../permit-application-payment/permit/entities/permit.entity'; +import { AddToShoppingCartDto } from './dto/request/add-to-shopping-cart.dto'; +import { UpdateShoppingCartDto } from './dto/request/update-shopping-cart.dto'; +import { ResultDto } from './dto/response/result.dto'; +import { IUserJWT } from '../../common/interface/user-jwt.interface'; +import { doesUserHaveAuthGroup } from '../../common/helper/auth.helper'; +import { InjectMapper } from '@automapper/nestjs'; +import { Mapper } from '@automapper/core'; +import { ReadShoppingCartDto } from './dto/response/read-shopping-cart.dto'; + +@Injectable() +export class ShoppingCartService { + private readonly logger = new Logger(ShoppingCartService.name); + constructor( + @InjectMapper() private readonly classMapper: Mapper, + @InjectRepository(Application) + private readonly applicationRepository: Repository, + ) {} + + /** + * Adds selected applications to the shopping cart by updating their status to `IN_CART`. + * The method handles permission checks based on the current user's role and updates the status accordingly. + * + * @param currentUser - The current user's JWT payload containing user identification and roles. + * @param dto - An object containing the application IDs to add to the cart and the associated company ID. + * @returns A promise that resolves with a `ResultDto` containing arrays of `success` with IDs of applications successfully added to the cart, and `failure` with IDs of applications that failed to be added. + */ + + @LogAsyncMethodExecution() + async addToCart( + currentUser: IUserJWT, + { applicationIds, companyId }: AddToShoppingCartDto & { companyId: number }, + ): Promise { + const { orbcUserAuthGroup } = currentUser; + if ( + orbcUserAuthGroup === ClientUserAuthGroup.COMPANY_ADMINISTRATOR || + doesUserHaveAuthGroup(orbcUserAuthGroup, IDIR_USER_AUTH_GROUP_LIST) + ) { + return await this.updateApplicationStatus( + { applicationIds, companyId }, + ApplicationStatus.IN_CART, + ); + } else if (orbcUserAuthGroup === ClientUserAuthGroup.PERMIT_APPLICANT) { + const { userGUID } = currentUser; + return await this.updateApplicationStatus( + { + applicationIds, + companyId, + userGUID, + }, + ApplicationStatus.IN_CART, + ); + } else { + return { failure: applicationIds, success: [] }; + } + } + + /** + * Finds all permit applications in a shopping cart based on the current user's information and optionally filtered by a company ID. + * + * @param currentUser - An object containing the current user's GUID and authorization group. + * @param companyId - The ID of the company to filter the permit applications. If not provided, applications are not filtered by company. + * @returns A promise resolved with an array of Application entities that are currently in the shopping cart. + */ + @LogAsyncMethodExecution() + async findApplicationsInCart( + currentUser: IUserJWT, + companyId: number, + ): Promise { + const { userGUID, orbcUserAuthGroup } = currentUser; + const applications = await this.getSelectShoppingCartQB( + companyId, + userGUID, + orbcUserAuthGroup, + ) + .orderBy({ 'application.updatedDateTime': 'DESC' }) + .getMany(); + + return await this.classMapper.mapArrayAsync( + applications, + Application, + ReadShoppingCartDto, + { + extraArgs: () => ({ + currentUserAuthGroup: orbcUserAuthGroup, + companyId, + }), + }, + ); + } + + /** + * Retrieves the count of permit applications currently in the shopping cart for a specific company. + * Optionally considers the user's GUID and authorization group for further filtering. + * + * @param currentUser - The current user's credentials and information. + * @param companyId - The ID of the company for which to count permit applications in the shopping cart. + * @returns A promise resolved with the number of permit applications in the shopping cart that match the criteria. + */ + @LogAsyncMethodExecution() + async getCartCount( + currentUser: IUserJWT, + companyId: number, + ): Promise { + const { userGUID, orbcUserAuthGroup } = currentUser; + return await this.getSelectShoppingCartQB( + companyId, + userGUID, + orbcUserAuthGroup, + ).getCount(); + } + + /** + * Retrieves a `SelectQueryBuilder` for permit applications based on specified criteria. + * This method prepares a query to fetch applications with the status `IN_CART`, further filtered by company ID, + * and optionally by the user's GUID and authorization group for more fine-grained control. + * + * @param companyId - The ID of the company to filter permit applications by. + * @param userGUID - (Optional) The user's GUID to filter applications by, depending on the user's authorization group. + * @param orbcUserAuthGroup - (Optional) The user's authorization group which determines the level of access and filtering applied to the query. + * @returns A `SelectQueryBuilder` configured with the appropriate conditions to fetch the desired permit applications. + */ + private getSelectShoppingCartQB( + companyId: number, + userGUID?: string, + orbcUserAuthGroup?: UserAuthGroup, + ): SelectQueryBuilder { + const queryBuilder = this.applicationRepository + .createQueryBuilder('application') + .leftJoin('application.company', 'company') + .leftJoinAndSelect('application.permitData', 'permitData') + .leftJoinAndSelect('application.applicationOwner', 'applicationOwner') + .leftJoinAndSelect('applicationOwner.userContact', 'userContact'); + + queryBuilder.where('application.permitStatus = :permitStatus', { + permitStatus: ApplicationStatus.IN_CART, + }); + queryBuilder.andWhere('company.companyId = :companyId', { companyId }); + + // If user is a Permit Applicant, get only their own applications in cart + if (orbcUserAuthGroup === ClientUserAuthGroup.PERMIT_APPLICANT) { + queryBuilder.andWhere('applicationOwner.userGUID = :userGUID', { + userGUID, + }); + } + // If user is a Company Admin, get all applications in cart for that company + // EXCEPT for those created by staff user. + else if (orbcUserAuthGroup === ClientUserAuthGroup.COMPANY_ADMINISTRATOR) { + queryBuilder.andWhere( + new NotBrackets((qb) => { + qb.where('applicationOwner.directory = :directory', { + directory: Directory.IDIR, + }); + }), + ); + } + return queryBuilder; + } + + /** + * Removes items from the shopping cart by updating their application status to IN_PROGRESS. + * It takes a DTO containing the IDs of applications to be updated and processes them accordingly. + * + * @param updateShoppingCartDto - Data Transfer Object containing the details required to update the application status of items in the shopping cart. + * @returns A ResultDto object which contains two arrays: 'success' with IDs of successfully updated applications, and 'failure' with IDs of applications that failed to update. + */ + @LogAsyncMethodExecution() + async remove( + currentUser: IUserJWT, + { + applicationIds, + companyId, + }: UpdateShoppingCartDto & { companyId: number }, + ): Promise { + const { orbcUserAuthGroup } = currentUser; + if (orbcUserAuthGroup === ClientUserAuthGroup.COMPANY_ADMINISTRATOR) { + return await this.updateApplicationStatus( + { applicationIds, companyId }, + ApplicationStatus.IN_PROGRESS, + ); + } else if (orbcUserAuthGroup === ClientUserAuthGroup.PERMIT_APPLICANT) { + const { userGUID } = currentUser; + return await this.updateApplicationStatus( + { + applicationIds, + companyId, + userGUID, + }, + ApplicationStatus.IN_PROGRESS, + ); + } + if (doesUserHaveAuthGroup(orbcUserAuthGroup, IDIR_USER_AUTH_GROUP_LIST)) { + return await this.updateApplicationStatus( + { + applicationIds, + companyId, + }, + ApplicationStatus.IN_PROGRESS, + ); + } else { + return { failure: applicationIds, success: [] }; + } + } + + /** + * Updates the status of selected applications based on provided IDs, company ID, and optional user GUID. + * It's mainly used for adding applications to the shopping cart or moving them back to the in-progress state. + * + * @param params - An object containing application IDs, company ID, and an optional user GUID. + * The `applicationIds` are the IDs of the applications to update. + * The `companyId` is used to ensure applications belong to the correct company. + * The `userGUID` is optional and used for filtering applications by the user, if applicable. + * @param statusToUpdateTo - The target status to update the applications to. Can be either `IN_CART` to add applications to the shopping cart or `IN_PROGRESS` to move them back to the in-progress state. + * @returns A promise that resolves with a `ResultDto` object, which contains arrays of `success` and `failure`. `success` includes IDs of applications successfully updated to the target status, while `failure` includes IDs of applications that failed to update. + */ + private async updateApplicationStatus( + { + applicationIds, + companyId, + userGUID, + }: (UpdateShoppingCartDto | AddToShoppingCartDto) & { + companyId: number; + userGUID?: string; + }, + statusToUpdateTo: ApplicationStatus.IN_CART | ApplicationStatus.IN_PROGRESS, + ): Promise { + const success = []; + const failure = []; + for (const applicationId of applicationIds) { + try { + const { affected } = await this.applicationRepository.update( + { + permitId: applicationId, + company: { companyId }, + ...(userGUID && { userGUID }), + }, + { + permitStatus: statusToUpdateTo, + }, + ); + if (affected === 1) { + success.push(applicationId); + } else { + failure.push(applicationId); + } + } catch (error) { + this.logger.error(error); + failure.push(applicationId); + } + } + return { success, failure }; + } +}