diff --git a/package.json b/package.json index 0fc0e2e31..4311eda80 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "package:check": "npx npm-check-updates -i", "generate:keys": "npx ts-node scripts/generate-keys.ts", "migrate:fresh": "yarn migrate:remove && yarn migrate:seed", - "migrate:seed": "nestjs-command seed:country && nestjs-command seed:apikey && nestjs-command seed:role && nestjs-command seed:user && nestjs-command seed:settings && nestjs-command seed:termPolicy", - "migrate:remove": "nestjs-command remove:user && nestjs-command remove:country && nestjs-command remove:apikey && nestjs-command remove:role && nestjs-command remove:settings && nestjs-command remove:termPolicy", + "migrate:seed": "nestjs-command seed:country && nestjs-command seed:termPolicy && nestjs-command seed:apikey && nestjs-command seed:role && nestjs-command seed:user && nestjs-command seed:settings", + "migrate:remove": "nestjs-command remove:user && nestjs-command remove:country && nestjs-command remove:termPolicy && nestjs-command remove:apikey && nestjs-command remove:role && nestjs-command remove:settings", "migrate:template": "nestjs-command migrate:emailTemplate && nestjs-command migrate:termPolicyTemplate", "rollback:template": "nestjs-command rollback:emailTemplate && nestjs-command rollback:termPolicyTemplate" }, diff --git a/src/languages/en/termPolicy.json b/src/languages/en/termPolicy.json index 1393fdbc3..fdb0af82b 100644 --- a/src/languages/en/termPolicy.json +++ b/src/languages/en/termPolicy.json @@ -3,6 +3,7 @@ "get": "Get term and policy success", "accept": "Term or policy accepted", "accepted": "List of accepted terms and policies.", + "reject": "Term or policy rejected", "pending": "List of pending terms and policies.", "delete": "Term and policy deleted success.", "update": "Update term and policy success.", @@ -13,6 +14,7 @@ "notAccepted": "You need to accept {property} policy before you can use this feature.", "inactive": "The term and policy is not active, most likely there's a newer version available", "alreadyAccepted": "Term and policy has already been accepted", + "alreadyRejected": "Term and policy has already been rejected", "newerVersionExist": "Newer version of the term and policy exists.", "updateForbiddenStatusPublished": "You can't update a published term and policy, you need to create a new version.", "requireAgreement": "Agreement for ${property} is required" diff --git a/src/migration/seeds/migration.term-policy.seed.ts b/src/migration/seeds/migration.term-policy.seed.ts index 2aef7495d..45b2054f2 100644 --- a/src/migration/seeds/migration.term-policy.seed.ts +++ b/src/migration/seeds/migration.term-policy.seed.ts @@ -10,12 +10,14 @@ import { Types } from 'mongoose'; import { CountryDoc } from '@modules/country/repository/entities/country.entity'; import { CountryService } from '@modules/country/services/country.service'; import { ENUM_AWS_S3_ACCESSIBILITY } from '@modules/aws/enums/aws.enum'; +import { TermPolicyAcceptanceService } from '@modules/term-policy/services/term-policy.acceptance.service'; @Injectable() export class MigrationTermPolicySeed { constructor( private readonly countryService: CountryService, - private readonly termPolicyService: TermPolicyService + private readonly termPolicyService: TermPolicyService, + private readonly termPolicyAcceptanceService: TermPolicyAcceptanceService ) {} @Command({ @@ -100,6 +102,7 @@ export class MigrationTermPolicySeed { async remove(): Promise { try { await this.termPolicyService.deleteMany(); + await this.termPolicyAcceptanceService.deleteMany() } catch (err: any) { throw new Error(err); } diff --git a/src/migration/seeds/migration.user.seed.ts b/src/migration/seeds/migration.user.seed.ts index 5ef163a59..070624c81 100644 --- a/src/migration/seeds/migration.user.seed.ts +++ b/src/migration/seeds/migration.user.seed.ts @@ -16,6 +16,9 @@ import { MessageService } from '@common/message/services/message.service'; import { ENUM_PASSWORD_HISTORY_TYPE } from '@modules/password-history/enums/password-history.enum'; import { SessionService } from '@modules/session/services/session.service'; import { VerificationService } from '@modules/verification/services/verification.service'; +import { TermPolicyAcceptanceService } from '@modules/term-policy/services/term-policy.acceptance.service'; +import { ENUM_TERM_POLICY_TYPE } from '@modules/term-policy/enums/term-policy.enum'; +import { ENUM_MESSAGE_LANGUAGE } from '@common/message/enums/message.enum'; @Injectable() export class MigrationUserSeed { @@ -28,7 +31,8 @@ export class MigrationUserSeed { private readonly activityService: ActivityService, private readonly messageService: MessageService, private readonly sessionService: SessionService, - private readonly verificationService: VerificationService + private readonly verificationService: VerificationService, + private readonly termPolicyAcceptanceService: TermPolicyAcceptanceService ) {} @Command({ @@ -129,6 +133,17 @@ export class MigrationUserSeed { type: ENUM_PASSWORD_HISTORY_TYPE.SIGN_UP, }), this.verificationService.verify(verification), + this.termPolicyAcceptanceService.createAcceptances( + user._id, + [ + ENUM_TERM_POLICY_TYPE.TERM, + ENUM_TERM_POLICY_TYPE.PRIVACY, + ENUM_TERM_POLICY_TYPE.COOKIES, + ENUM_TERM_POLICY_TYPE.MARKETING, + ], + ENUM_MESSAGE_LANGUAGE.EN, + country._id + ), ]; await Promise.all(promises); diff --git a/src/modules/term-policy/controllers/term-policy.public.controller.ts b/src/modules/term-policy/controllers/term-policy.public.controller.ts index acce1eaad..b256598be 100644 --- a/src/modules/term-policy/controllers/term-policy.public.controller.ts +++ b/src/modules/term-policy/controllers/term-policy.public.controller.ts @@ -33,7 +33,7 @@ export class TermPolicyPublicController { private readonly paginationService: PaginationService ) {} - @ResponsePaging('termPolicy.accepted') + @ResponsePaging('termPolicy.list') @TermPolicyPublicListDoc() @ApiKeyProtected() @Get('/list') diff --git a/src/modules/term-policy/controllers/term-policy.user.controller.ts b/src/modules/term-policy/controllers/term-policy.user.controller.ts index 2d352b5e0..22338501c 100644 --- a/src/modules/term-policy/controllers/term-policy.user.controller.ts +++ b/src/modules/term-policy/controllers/term-policy.user.controller.ts @@ -2,7 +2,7 @@ import { ApiTags } from '@nestjs/swagger'; import { BadRequestException, Body, - Controller, + Controller, Delete, Get, InternalServerErrorException, NotFoundException, @@ -30,13 +30,14 @@ import { ENUM_POLICY_ROLE_TYPE } from '@modules/policy/enums/policy.enum'; import { TermPolicyAcceptanceResponseDto } from '@modules/term-policy/dtos/response/term-policy-acceptance.response.dto'; import { TermPolicyUserAcceptDoc, - TermPolicyUserAcceptedDoc, + TermPolicyUserAcceptedDoc, TermPolicyUserRejectDoc, } from '@modules/term-policy/docs/term-policy.user.doc'; import { UserService } from '@modules/user/services/user.service'; import { TERM_POLICY_ACCEPTANCE_DEFAULT_AVAILABLE_ORDER_BY } from '@modules/term-policy/constants/term-policy.list.constant'; import { DatabaseService } from '@common/database/services/database.service'; import { ENUM_APP_STATUS_CODE_ERROR } from '@app/enums/app.status-code.enum'; import { TermPolicyAcceptanceService } from '@modules/term-policy/services/term-policy.acceptance.service'; +import { TermPolicyRejectRequestDto } from '@modules/term-policy/dtos/request/term-policy.reject.request.dto'; @ApiTags('modules.user.term-policy') @Controller({ @@ -141,4 +142,72 @@ export class TermPolicyUserController { return; } + + @TermPolicyUserRejectDoc() + @Response('termPolicy.reject') + @PolicyRoleProtected(ENUM_POLICY_ROLE_TYPE.USER) + @UserProtected() + @AuthJwtAccessProtected() + @ApiKeyProtected() + @Delete('/reject') + async reject( + @AuthJwtPayload('user') userId: string, + @Body() { type, country }: TermPolicyRejectRequestDto + ): Promise { + const user = await this.userService.findOneById(userId); + if (!user.termPolicy[type.toLowerCase()]) { + throw new BadRequestException({ + statusCode: ENUM_TERM_POLICY_STATUS_CODE_ERROR.ALREADY_REJECTED, + message: 'termPolicy.error.alreadyRejected', + }); + } + + const policy = await this.termPolicyService.findOnePublished( + type, + country + ); + if (!policy) { + throw new NotFoundException({ + statusCode: ENUM_TERM_POLICY_STATUS_CODE_ERROR.NOT_FOUND, + message: 'termPolicy.error.notFound', + }); + } + + const session = await this.databaseService.createTransaction(); + + try { + const acceptance = await this.termPolicyAcceptanceService.findOne( + { + user: userId, + termPolicy: policy._id, + }, + { session } + ); + + const deleted = await this.termPolicyAcceptanceService.softDelete( + acceptance, + { session } + ); + if (!deleted) { + throw new NotFoundException({ + statusCode: ENUM_TERM_POLICY_STATUS_CODE_ERROR.NOT_FOUND, + message: 'termPolicy.error.notFound', + }); + } + + await this.userService.rejectTermPolicy(user, type, { session }); + + await this.databaseService.commitTransaction(session); + } catch (err: unknown) { + await this.databaseService.abortTransaction(session); + + throw new InternalServerErrorException({ + statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err, + }); + } + + return; + } } diff --git a/src/modules/term-policy/docs/term-policy.user.doc.ts b/src/modules/term-policy/docs/term-policy.user.doc.ts index ca4b8738d..59f4f605e 100644 --- a/src/modules/term-policy/docs/term-policy.user.doc.ts +++ b/src/modules/term-policy/docs/term-policy.user.doc.ts @@ -10,6 +10,7 @@ import { ENUM_DOC_REQUEST_BODY_TYPE } from '@common/doc/enums/doc.enum'; import { TermPolicyAcceptRequestDto } from '@modules/term-policy/dtos/request/term-policy.accept.request.dto'; import { TermPolicyAcceptanceResponseDto } from '@modules/term-policy/dtos/response/term-policy-acceptance.response.dto'; import { applyDecorators } from '@nestjs/common'; +import { TermPolicyRejectRequestDto } from '@modules/term-policy/dtos/request/term-policy.reject.request.dto'; export function TermPolicyUserAcceptedDoc(): MethodDecorator { return applyDecorators( @@ -51,3 +52,23 @@ export function TermPolicyUserAcceptDoc(): MethodDecorator { DocResponse('termPolicy.accept') ); } + +export function TermPolicyUserRejectDoc(): MethodDecorator { + return applyDecorators( + Doc({ + summary: 'User rejects term or policy', + }), + DocAuth({ + jwtAccessToken: true, + xApiKey: true, + }), + DocGuard({ + role: true, + }), + DocRequest({ + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.JSON, + dto: TermPolicyRejectRequestDto, + }), + DocResponse('termPolicy.reject') + ); +} \ No newline at end of file diff --git a/src/modules/term-policy/dtos/request/term-policy.reject.request.dto.ts b/src/modules/term-policy/dtos/request/term-policy.reject.request.dto.ts new file mode 100644 index 000000000..ebc9fda3c --- /dev/null +++ b/src/modules/term-policy/dtos/request/term-policy.reject.request.dto.ts @@ -0,0 +1,19 @@ +import { PickType, ApiProperty } from '@nestjs/swagger'; +import { TermPolicyCreateRequestDto } from '@modules/term-policy/dtos/request/term-policy.create.request.dto'; +import { ENUM_TERM_POLICY_TYPE } from '@modules/term-policy/enums/term-policy.enum'; +import { IsEnum, IsNotEmpty } from 'class-validator'; + +export class TermPolicyRejectRequestDto extends PickType( + TermPolicyCreateRequestDto, + ['country' ] +) { + @ApiProperty({ + description: 'Type of the terms policy', + example: ENUM_TERM_POLICY_TYPE.COOKIES, + enum: ENUM_TERM_POLICY_TYPE, + required: true, + }) + @IsEnum([ENUM_TERM_POLICY_TYPE.COOKIES, ENUM_TERM_POLICY_TYPE.MARKETING]) + @IsNotEmpty() + readonly type: ENUM_TERM_POLICY_TYPE; +} diff --git a/src/modules/term-policy/enums/term-policy.status-code.enum.ts b/src/modules/term-policy/enums/term-policy.status-code.enum.ts index 89ce7dcf9..0c273a396 100644 --- a/src/modules/term-policy/enums/term-policy.status-code.enum.ts +++ b/src/modules/term-policy/enums/term-policy.status-code.enum.ts @@ -3,6 +3,7 @@ export enum ENUM_TERM_POLICY_STATUS_CODE_ERROR { EXIST = 6101, INVALID_STATUS = 6102, ALREADY_ACCEPTED = 6103, + ALREADY_REJECTED = 6107, REQUIRED_ACCEPTANCE = 6104, PREDEFINED_REQUIRED_ACCEPTANCE_NOT_FOUND = 6105, AT_LEAST_ONE_DOCUMENT_REQUIRED = 6106, diff --git a/src/modules/term-policy/interfaces/term-policy.acceptance-service.interface.ts b/src/modules/term-policy/interfaces/term-policy.acceptance-service.interface.ts index 3507c5688..2b7a00db8 100644 --- a/src/modules/term-policy/interfaces/term-policy.acceptance-service.interface.ts +++ b/src/modules/term-policy/interfaces/term-policy.acceptance-service.interface.ts @@ -1,7 +1,8 @@ import { - IDatabaseFindAllOptions, - IDatabaseCreateOptions, + IDatabaseCreateOptions, IDatabaseDeleteOptions, + IDatabaseFindAllOptions, IDatabaseFindOneOptions, IDatabaseGetTotalOptions, + IDatabaseSoftDeleteOptions, } from '@common/database/interfaces/database.interface'; import { ENUM_TERM_POLICY_TYPE } from '@modules/term-policy/enums/term-policy.enum'; import { ENUM_MESSAGE_LANGUAGE } from '@common/message/enums/message.enum'; @@ -13,35 +14,47 @@ import { } from '@modules/term-policy/interfaces/term-policy.acceptance.interface'; export interface ITermPolicyAcceptanceService { + findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise; + findAllByUser( user: string, options?: IDatabaseFindAllOptions ): Promise; + getTotalUser( user: string, options?: IDatabaseGetTotalOptions ): Promise; + create( user: string, termPolicy: string, options?: IDatabaseCreateOptions ): Promise; + findAll( find?: Record, options?: IDatabaseFindAllOptions ): Promise; + getTotal( find?: Record, options?: IDatabaseGetTotalOptions ): Promise; + mapList( policies: (ITermPolicyAcceptanceDoc | ITermPolicyAcceptanceEntity)[] ): TermPolicyAcceptanceResponseDto[]; + createMany( user: string, termPolicies: Array, options?: IDatabaseCreateOptions ): Promise; + createAcceptances( user: string, termPolicyTypes: ENUM_TERM_POLICY_TYPE[], @@ -49,4 +62,14 @@ export interface ITermPolicyAcceptanceService { country: string, options?: IDatabaseCreateOptions ): Promise; + + softDelete( + repository: TermPolicyAcceptanceDoc, + options?: IDatabaseSoftDeleteOptions + ): Promise; + + deleteMany( + find?: Record, + options?: IDatabaseDeleteOptions + ): Promise; } diff --git a/src/modules/term-policy/interfaces/term-policy.service.interface.ts b/src/modules/term-policy/interfaces/term-policy.service.interface.ts index 593c6d468..64ae1d698 100644 --- a/src/modules/term-policy/interfaces/term-policy.service.interface.ts +++ b/src/modules/term-policy/interfaces/term-policy.service.interface.ts @@ -29,37 +29,45 @@ export interface ITermPolicyService { find?: Record, options?: IDatabaseFindAllOptions ): Promise; + getTotal( find?: Record, options?: IDatabaseGetTotalOptions ): Promise; + findOnePublished( type: ENUM_TERM_POLICY_TYPE, country: string, options?: IDatabaseFindOneOptions ): Promise; + findOneLatest( type: ENUM_TERM_POLICY_TYPE, country: string, options?: IDatabaseFindOneOptions ): Promise; + mapList( policies: ITermPolicyDoc[] | ITermPolicyEntity[], options?: ClassTransformOptions ): TermPolicyResponseDto[]; + exist( type: ENUM_TERM_POLICY_TYPE, country: string, option?: IDatabaseExistsOptions ): Promise; + findOneById( _id: string, options?: IDatabaseFindOneOptions ): Promise; + findOne( find: Record, options?: IDatabaseFindOneOptions ): Promise; + create( country: string, type: ENUM_TERM_POLICY_TYPE, @@ -68,21 +76,28 @@ export interface ITermPolicyService { version: number, options?: IDatabaseCreateOptions ): Promise; + updateDocument( repository: TermPolicyDoc, language: ENUM_MESSAGE_LANGUAGE, { size, ...aws }: AwsS3Dto, options?: IDatabaseSaveOptions ): Promise; + delete( repository: TermPolicyDoc, options?: IDatabaseDeleteOptions ): Promise; + createMany( country: string, types: Record, status: ENUM_TERM_POLICY_STATUS, options?: IDatabaseCreateManyOptions ): Promise; - deleteMany(options?: IDatabaseDeleteOptions): Promise; + + deleteMany( + find?: Record, + options?: IDatabaseDeleteOptions + ): Promise; } diff --git a/src/modules/term-policy/repository/entities/term-policy-acceptance.entity.ts b/src/modules/term-policy/repository/entities/term-policy-acceptance.entity.ts index e3153575f..a87c66ffc 100644 --- a/src/modules/term-policy/repository/entities/term-policy-acceptance.entity.ts +++ b/src/modules/term-policy/repository/entities/term-policy-acceptance.entity.ts @@ -39,5 +39,3 @@ export type TermPolicyAcceptanceDoc = export const TermPolicyAcceptanceSchema = DatabaseSchema( TermPolicyAcceptanceEntity ); - -TermPolicyAcceptanceSchema.index({ user: 1, termPolicy: 1 }, { unique: true }); diff --git a/src/modules/term-policy/services/term-policy.acceptance.service.ts b/src/modules/term-policy/services/term-policy.acceptance.service.ts index 277ff3e15..fd2fd0d7f 100644 --- a/src/modules/term-policy/services/term-policy.acceptance.service.ts +++ b/src/modules/term-policy/services/term-policy.acceptance.service.ts @@ -10,9 +10,11 @@ import { ENUM_TERM_POLICY_TYPE, } from '@modules/term-policy/enums/term-policy.enum'; import { - IDatabaseCreateOptions, + IDatabaseCreateOptions, IDatabaseDeleteOptions, IDatabaseFindAllOptions, + IDatabaseFindOneOptions, IDatabaseGetTotalOptions, + IDatabaseSoftDeleteOptions, } from '@common/database/interfaces/database.interface'; import { HelperDateService } from '@common/helper/services/helper.date.service'; import { ENUM_MESSAGE_LANGUAGE } from '@common/message/enums/message.enum'; @@ -38,6 +40,13 @@ export class TermPolicyAcceptanceService private readonly databaseService: DatabaseService ) {} + async findOne( + find: Record, + options?: IDatabaseFindOneOptions, + ): Promise { + return this.termPolicyAcceptanceRepository.findOne(find, options); + } + async findAllByUser( user: string, options?: IDatabaseFindAllOptions @@ -143,14 +152,14 @@ export class TermPolicyAcceptanceService } const latestTermPolicies: TermPolicyEntity[] = - await this.termPolicyRepository.findAll([ + await this.termPolicyRepository.findAll( { - language, + 'urls.language': language, country, status: ENUM_TERM_POLICY_STATUS.PUBLISHED, ...this.databaseService.filterIn('type', termPolicyTypes), }, - ]); + ); if (latestTermPolicies.length === 0) { return; @@ -162,4 +171,18 @@ export class TermPolicyAcceptanceService options ); } + + async softDelete( + repository: TermPolicyAcceptanceDoc, + options?: IDatabaseSoftDeleteOptions, + ): Promise { + return this.termPolicyAcceptanceRepository.softDelete(repository, options); + } + + async deleteMany( + find?: Record, + options?: IDatabaseDeleteOptions + ): Promise { + await this.termPolicyAcceptanceRepository.deleteMany(find, options); + } } diff --git a/src/modules/term-policy/services/term-policy.service.ts b/src/modules/term-policy/services/term-policy.service.ts index 5d54537b0..070dc1e2c 100644 --- a/src/modules/term-policy/services/term-policy.service.ts +++ b/src/modules/term-policy/services/term-policy.service.ts @@ -275,7 +275,10 @@ export class TermPolicyService implements ITermPolicyService { await this.termPolicyRepository.createMany(entities, options); } - async deleteMany(options?: IDatabaseDeleteOptions): Promise { - await this.termPolicyRepository.deleteMany({}, options); + async deleteMany( + find?: Record, + options?: IDatabaseDeleteOptions + ): Promise { + await this.termPolicyRepository.deleteMany(find, options); } } diff --git a/src/modules/user/interfaces/user.service.interface.ts b/src/modules/user/interfaces/user.service.interface.ts index 2f263fd70..db6c47dd4 100644 --- a/src/modules/user/interfaces/user.service.interface.ts +++ b/src/modules/user/interfaces/user.service.interface.ts @@ -226,6 +226,11 @@ export interface IUserService { type: ENUM_TERM_POLICY_TYPE, options?: IDatabaseSaveOptions ): Promise; + rejectTermPolicy( + repository: UserDoc, + type: ENUM_TERM_POLICY_TYPE, + options?: IDatabaseSaveOptions + ): Promise; releaseTermPolicy( type: ENUM_TERM_POLICY_TYPE, options?: IDatabaseUpdateManyOptions diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index c8de0e48d..c1511b210 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -638,6 +638,16 @@ export class UserService implements IUserService { return this.userRepository.save(repository, options); } + async rejectTermPolicy( + repository: UserDoc, + type: ENUM_TERM_POLICY_TYPE, + options?: IDatabaseSaveOptions + ): Promise { + repository.termPolicy[type.toLowerCase()] = false; + + return this.userRepository.save(repository, options); + } + async releaseTermPolicy( type: ENUM_TERM_POLICY_TYPE, options?: IDatabaseUpdateManyOptions