From 5b8f419d81ac40714402461d20d1ee52882452a1 Mon Sep 17 00:00:00 2001 From: Matin Date: Thu, 20 Feb 2025 11:29:17 +0330 Subject: [PATCH] chore: get announcements for user that buy service --- .../announcements/announcement.controller.ts | 3 +- .../announcements/announcement.module.ts | 2 +- .../{ => providers}/announcement.service.ts | 101 +++++++++++++++--- src/modules/users/providers/users.service.ts | 68 +++++++++++- src/modules/users/users.controller.ts | 25 +++++ 5 files changed, 181 insertions(+), 18 deletions(-) rename src/modules/announcements/{ => providers}/announcement.service.ts (61%) diff --git a/src/modules/announcements/announcement.controller.ts b/src/modules/announcements/announcement.controller.ts index beb2731..50d3908 100644 --- a/src/modules/announcements/announcement.controller.ts +++ b/src/modules/announcements/announcement.controller.ts @@ -1,8 +1,8 @@ import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common"; import { ApiProperty, ApiTags } from "@nestjs/swagger"; -import { AnnouncementService } from "./announcement.service"; import { CreateAnnouncementDto } from "./DTO/create-announcement.dto"; +import { AnnouncementService } from "./providers/announcement.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; @@ -17,6 +17,7 @@ export class AnnouncementController { constructor(private readonly announcementService: AnnouncementService) {} @AuthGuards() + @PermissionsDec(PermissionEnum.ANNOUNCEMENTS) @ApiProperty({ description: "Create a new announcement ===> login as admin" }) @Post() async create(@Body() createAnnouncementDto: CreateAnnouncementDto) { diff --git a/src/modules/announcements/announcement.module.ts b/src/modules/announcements/announcement.module.ts index 04d57b9..5195eea 100644 --- a/src/modules/announcements/announcement.module.ts +++ b/src/modules/announcements/announcement.module.ts @@ -2,8 +2,8 @@ import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { AnnouncementController } from "./announcement.controller"; -import { AnnouncementService } from "./announcement.service"; import { Announcement } from "./entities/announcement.entity"; +import { AnnouncementService } from "./providers/announcement.service"; import { AnnouncementRepository } from "./repositories/announcement.repository"; import { DanakServicesModule } from "../danak-services/danak-services.module"; import { UsersModule } from "../users/users.module"; diff --git a/src/modules/announcements/announcement.service.ts b/src/modules/announcements/providers/announcement.service.ts similarity index 61% rename from src/modules/announcements/announcement.service.ts rename to src/modules/announcements/providers/announcement.service.ts index e08d2ce..cffb116 100644 --- a/src/modules/announcements/announcement.service.ts +++ b/src/modules/announcements/providers/announcement.service.ts @@ -1,13 +1,13 @@ import { BadRequestException, Injectable } from "@nestjs/common"; -import { CreateAnnouncementDto } from "./DTO/create-announcement.dto"; -import { SearchAnnouncementQueryDto } from "./DTO/search-announcement-query.dto"; -import { AnnouncementRepository } from "./repositories/announcement.repository"; -import { UserAnnouncementRepository } from "./repositories/user-announcement.repository"; -import { AnnouncementMessage, CommonMessage } from "../../common/enums/message.enum"; -import { DanakServicesService } from "../danak-services/providers/danak-services.service"; -import { UsersService } from "../users/providers/users.service"; -import { PaginationUtils } from "../utils/providers/pagination.utils"; +import { AnnouncementMessage, CommonMessage } from "../../../common/enums/message.enum"; +import { DanakServicesService } from "../../danak-services/providers/danak-services.service"; +import { UsersService } from "../../users/providers/users.service"; +import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { CreateAnnouncementDto } from "../DTO/create-announcement.dto"; +import { SearchAnnouncementQueryDto } from "../DTO/search-announcement-query.dto"; +import { AnnouncementRepository } from "../repositories/announcement.repository"; +import { UserAnnouncementRepository } from "../repositories/user-announcement.repository"; @Injectable() export class AnnouncementService { @@ -18,6 +18,7 @@ export class AnnouncementService { private readonly usersService: UsersService, ) {} + /***************************************** */ async createAnnouncement(createAnnouncementDto: CreateAnnouncementDto) { let service; let groups; @@ -40,6 +41,8 @@ export class AnnouncementService { }; } + /***************************************** */ + async getAllAnnouncements(queryDto: SearchAnnouncementQueryDto, userId: string) { const { limit, skip } = PaginationUtils(queryDto); @@ -70,18 +73,81 @@ export class AnnouncementService { return { announcements, count, paginate: true }; } + /***************************************** */ + + // async getAllAnnouncementsByUser(queryDto: SearchAnnouncementQueryDto, userId: string) { + // const { limit, skip } = PaginationUtils(queryDto); + // const queryBuilder = this.announcementRepository + // .createQueryBuilder("announcement") + // .leftJoin("announcement.service", "danak_service") + // .addSelect(["danak_service.id", "danak_service.name"]) + // .leftJoin("announcement.targetGroups", "group") + // .addSelect(["group.id", "group.name"]) + // .leftJoin("group.users", "groupUser") + // .leftJoinAndSelect("announcement.userAnnouncements", "userAnnouncements", "userAnnouncements.userId = :userId", { userId }) + // .where("announcement.isPublic = :isPublic", { isPublic: true }) // Check if the announcement is public + // .orWhere("groupUser.id = :userId", { userId }); + + // if (queryDto.since) { + // queryBuilder.andWhere("announcement.createdAt >= :since", { since: queryDto.since }); + // } + + // if (queryDto.publishAt) { + // queryBuilder.andWhere("announcement.publishAt >= :publishAt", { publishAt: queryDto.publishAt }); + // } + + // if (queryDto.q) { + // queryBuilder.andWhere("announcement.title LIKE :q", { q: `%${queryDto.q}%` }); + // } + + // queryBuilder.orderBy("announcement.createdAt", "DESC"); + // const rawResults = await queryBuilder.getRawMany(); + + // const [, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount(); + + // const announcements = rawResults.map((raw) => ({ + // id: raw.announcement_id, + // createdAt: raw.announcement_createdAt, + // updatedAt: raw.announcement_updatedAt, + // title: raw.announcement_title, + // content: raw.announcement_content, + // publishAt: raw.announcement_publishAt, + // service: { + // id: raw.danak_service_id, + // name: raw.danak_service_name, + // }, + // isImportant: raw.announcement_isImportant, + // targetGroups: raw.group_id ? [{ id: raw.group_id, name: raw.group_name }] : [], + // isPublic: raw.announcement_isPublic, + // isRead: raw.userAnnouncements_isRead ? true : false, + // })); + + // return { announcements, count, paginate: true }; + // } + + /***************************************** */ + async getAllAnnouncementsByUser(queryDto: SearchAnnouncementQueryDto, userId: string) { const { limit, skip } = PaginationUtils(queryDto); + const queryBuilder = this.announcementRepository .createQueryBuilder("announcement") - .leftJoin("announcement.service", "danak_service") - .addSelect(["danak_service.id", "danak_service.name"]) + .leftJoin("announcement.service", "service") + .addSelect(["service.id", "service.name"]) .leftJoin("announcement.targetGroups", "group") .addSelect(["group.id", "group.name"]) .leftJoin("group.users", "groupUser") + .leftJoin("service.subscriptionPlans", "subscriptionPlan") + .leftJoin( + "UserSubscription", + "userSubscription", + "userSubscription.planId = subscriptionPlan.id AND userSubscription.userId = :userId", + { userId }, + ) .leftJoinAndSelect("announcement.userAnnouncements", "userAnnouncements", "userAnnouncements.userId = :userId", { userId }) - .where("announcement.isPublic = :isPublic", { isPublic: true }) // Check if the announcement is public - .orWhere("groupUser.id = :userId", { userId }); + .where("announcement.isPublic = :isPublic", { isPublic: true }) + .orWhere("groupUser.id = :userId", { userId }) + .orWhere("userSubscription.userId = :userId", { userId }); if (queryDto.since) { queryBuilder.andWhere("announcement.createdAt >= :since", { since: queryDto.since }); @@ -97,7 +163,6 @@ export class AnnouncementService { queryBuilder.orderBy("announcement.createdAt", "DESC"); const rawResults = await queryBuilder.getRawMany(); - const [, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount(); const announcements = rawResults.map((raw) => ({ @@ -108,8 +173,8 @@ export class AnnouncementService { content: raw.announcement_content, publishAt: raw.announcement_publishAt, service: { - id: raw.danak_service_id, - name: raw.danak_service_name, + id: raw.service_id, + name: raw.service_name, }, isImportant: raw.announcement_isImportant, targetGroups: raw.group_id ? [{ id: raw.group_id, name: raw.group_name }] : [], @@ -120,6 +185,8 @@ export class AnnouncementService { return { announcements, count, paginate: true }; } + /***************************************** */ + async getOneAnnouncement(id: string, userId: string) { const announcement = await this.announcementRepository.findOneBy({ id }); if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND); @@ -132,11 +199,15 @@ export class AnnouncementService { return { announcement }; } + /***************************************** */ + async getPublicAnnouncements() { const announcements = await this.announcementRepository.find({ where: { isPublic: true }, relations: ["service", "targetGroups"] }); return { announcements }; } + /***************************************** */ + async getAnnouncementByService(queryDto: SearchAnnouncementQueryDto, serviceId: string) { const { limit, skip } = PaginationUtils(queryDto); diff --git a/src/modules/users/providers/users.service.ts b/src/modules/users/providers/users.service.ts index 479ded6..8d721cb 100644 --- a/src/modules/users/providers/users.service.ts +++ b/src/modules/users/providers/users.service.ts @@ -1,6 +1,6 @@ import { createHmac } from "node:crypto"; -import { BadRequestException, Injectable } from "@nestjs/common"; +import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { FastifyReply } from "fastify"; import slugify from "slugify"; @@ -9,6 +9,7 @@ import { In, Not, QueryRunner } from "typeorm"; import { AdminMessage, AdsMessage, + AuthMessage, CommonMessage, LegalUserMessage, RealUserMessage, @@ -16,10 +17,14 @@ import { } from "../../../common/enums/message.enum"; import { AddressService } from "../../address/providers/address.service"; import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto"; +import { RequestOtpDto } from "../../auth/DTO/request-otp.dto"; +import { VerifyOtpDto } from "../../auth/DTO/verify-otp.dto"; import { UserSettingsService } from "../../settings/providers/user-settings.service"; import { EmailService } from "../../utils/providers/email.service"; +import { OTPService } from "../../utils/providers/otp.service"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { PasswordService } from "../../utils/providers/password.service"; +import { SmsService } from "../../utils/providers/sms.service"; import { WalletsService } from "../../wallets/providers/wallets.service"; import { CheckValidityDTO } from "../DTO/check-validity.dto"; import { CreateAdminDto } from "../DTO/create-admin.dto"; @@ -47,6 +52,7 @@ import { UserRepository } from "../repositories/users.repository"; @Injectable() export class UsersService { + private readonly logger = new Logger(UsersService.name); constructor( private readonly userRepository: UserRepository, private readonly userGroupRepository: UserGroupRepository, @@ -60,6 +66,8 @@ export class UsersService { private readonly addressService: AddressService, private readonly emailService: EmailService, private readonly configService: ConfigService, + private readonly otpService: OTPService, + private readonly smsService: SmsService, ) {} /************************************************************ */ @@ -178,6 +186,50 @@ export class UsersService { /************************************************************ */ + async updatePhone(userId: string, requestOtpDto: RequestOtpDto) { + const { phone } = requestOtpDto; + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + + const existPhone = await this.userRepository.findOneBy({ phone }); + if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); + + const existCode = await this.otpService.checkExistOtp(phone, "LOGIN"); + if (existCode) { + return { + message: AuthMessage.OTP_ALREADY_SENT, + ttlSecond: existCode, + }; + } + + const otpCode = await this.otpService.generateAndSetInCache(phone, "LOGIN"); + // + await this.smsService.sendSmsVerifyCode(phone, otpCode); + this.logger.debug(`OTP sent to ${phone}: ${otpCode}`); + + return { + message: AuthMessage.OTP_SENT, + otpCode, + }; + } + + /************************************************************ */ + + async verifyPhone(verifyOtpDto: VerifyOtpDto, userId: string) { + const { code, phone } = verifyOtpDto; + + const user = await this.checkUserLoginCredentialWithPhone(phone, code, userId); + + user.phone = phone; + await this.userRepository.save(user); + + return { + message: CommonMessage.UPDATE_SUCCESS, + }; + } + + /************************************************************ */ + async findOneWithEmail(email: string): Promise { const user = await this.userRepository.findOneWithEmail(email); return user; @@ -594,4 +646,18 @@ export class UsersService { } /************************************************************ */ + + private async checkUserLoginCredentialWithPhone(phone: string, otpCode: string, userId: string) { + const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN"); + if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); + + await this.otpService.delOtpFormCache(phone, "LOGIN"); + + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + + return user; + } + + /************************************************************ */ } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 4431091..96e18c8 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -21,6 +21,8 @@ import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; +import { RequestOtpDto } from "../auth/DTO/request-otp.dto"; +import { VerifyOtpDto } from "../auth/DTO/verify-otp.dto"; @Controller("users") @ApiTags("users") @@ -54,6 +56,8 @@ export class UsersController { return this.usersService.updateProfile(userId, updateProfileDto); } + /************************************************************ */ + @AuthGuards() @ApiOperation({ summary: "Update user email" }) @Patch("update-email") @@ -61,6 +65,8 @@ export class UsersController { return this.usersService.updateEmail(userId, updateEmailDto); } + /************************************************************ */ + @ApiOperation({ summary: "Verify email" }) @HttpCode(HttpStatus.PERMANENT_REDIRECT) @Get("verify-email") @@ -70,6 +76,25 @@ export class UsersController { /************************************************************ */ + @AuthGuards() + @ApiOperation({ summary: "Update user phone" }) + @Patch("update-phone") + updatePhone(@Body() updateDto: RequestOtpDto, @UserDec("id") userId: string) { + return this.usersService.updatePhone(userId, updateDto); + } + + /************************************************************ */ + + @AuthGuards() + @ApiOperation({ summary: "Verify phone" }) + @HttpCode(HttpStatus.OK) + @Patch("verify-phone") + verifyPhone(@Body() verifyOtpDto: VerifyOtpDto, @UserDec("id") userId: string) { + return this.usersService.verifyPhone(verifyOtpDto, userId); + } + + /************************************************************ */ + @AuthGuards() @ApiOperation({ summary: "Check validity of user field" }) @HttpCode(HttpStatus.OK)