update: add new field for subscription of user when purchased

This commit is contained in:
mahyargdz
2025-02-28 12:08:46 +03:30
parent 93c5ad49d3
commit 76feef7645
29 changed files with 220 additions and 302 deletions
+28 -5
View File
@@ -113,6 +113,10 @@ export const enum CategoryMessage {
CATEGORY_NOT_EXIST = "دسته بندی مورد نظر یافت نشد",
IS_ACTIVE_SHOULD_BE_BOOLEAN = "وضعیت دسته بندی باید یک بولین باشد",
SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد",
IS_SUGGEST_SHOULD_BE_BOOLEAN = "وضعیت پیشنهادی بودن دسته بندی باید یک بولین باشد",
IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت دسته بندی باید یکی از مقادیر ۰ و ۱ باشد",
IS_SUGGEST_SHOULD_BE_1_0 = "وضعیت پیشنهادی بودن دسته بندی باید یکی از مقادیر ۰ و ۱ باشد",
CATEGORY_NOT_EXIST_OR_HAS_NO_SERVICE = "دسته بندی مورد نظر یافت نشد یا هیچ سرویسی در این دسته بندی وجود ندارد",
}
export const enum ServiceMessage {
@@ -159,6 +163,8 @@ export const enum ServiceMessage {
REVIEW_NOT_EXIST = "نظری برای این سرویس یافت نشد",
REVIEW_APPROVED = "نظر با موفقیت تایید شد",
REVIEW_REJECTED = "نظر با موفقیت رد شد",
IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت سرویس باید یکی از مقادیر ۰ و ۱ باشد",
IS_SUGGEST_SHOULD_BE_1_0 = "وضعیت پیشنهادی بودن سرویس باید یکی از مقادیر ۰ و ۱ باشد",
}
export const enum AnnouncementMessage {
@@ -173,6 +179,13 @@ export const enum AnnouncementMessage {
USER_MUST_BE_UUID = "شناسه کاربر باید یک UUID معتبر باشد",
SERVICE_MUST_BE_ID = "ایدی سرویس اجباری است",
NOT_FOUND = "اطلاعیه ای با این ایدی یافت نشد",
USER_IDS_MUST_BE_ARRAY = "شناسه کاربران باید یک آرایه باشد",
IMPORTANT_IS_REQUIRED = "وضعیت اهمیت اطلاعیه مورد نیاز است",
TITLE_LENGTH = "عنوان اطلاعیه باید بین ۵ تا ۱۰۰ کاراکتر باشد",
PUBLISH_AT_MUST_BE_DATE = "تاریخ انتشار باید یک تاریخ معتبر باشد",
IMPORTANT_MUST_BE_BOOLEAN = "وضعیت اهمیت اطلاعیه باید یک بولین باشد",
SOME_SERVICES_NOT_FOUND = "بعضی از سرویس ها یافت نشدند",
SOME_USERS_NOT_FOUND = "بعضی از کاربران یافت نشدند",
}
export const enum CriticismMessage {
@@ -182,6 +195,8 @@ export const enum CriticismMessage {
CONTENT_IS_STRING = "متن انتقادات باید رشته باشد",
NOT_FOUND = "انتقادی با این ایدی یافت نشد",
SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد",
FILES_IS_REQUIRED = "فایل های انتقادات مورد نیاز است",
FILES_SHOULD_BE_URL = "فایل های انتقادات باید یک آدرس یو آر ال باشد",
}
export const enum TicketMessageEnum {
TITLE_REQUIRED = "عنوان تیکت مورد نیاز است",
@@ -339,11 +354,16 @@ export const enum SubscriptionMessage {
SUBS_REQUIRED = "اشتراک مورد نیاز است",
SUBS_SHOULD_BE_ARRAY = "اشتراک باید یک آرایه باشد",
USER_SUBS_NOT_FOUND = "اشتراک کاربر یافت نشد",
BUSINESS_NAME_REQUIRED = "BUSINESS_NAME_REQUIRED",
BUSINESS_NAME_LENGTH = "BUSINESS_NAME_LENGTH",
BUSINESS_PHONE_INVALID = "BUSINESS_PHONE_INVALID",
DESCRIPTION_LENGTH = "DESCRIPTION_LENGTH",
BUSINESS_NAME_STRING = "BUSINESS_NAME_STRING",
BUSINESS_NAME_REQUIRED = "نام کسب و کار نمی‌تواند خالی باشد",
BUSINESS_NAME_LENGTH = "نام کسب و کار باید بین ۳ تا ۱۰۰ کاراکتر باشد",
BUSINESS_PHONE_INVALID = "شماره تلفن کسب و کار معتبر نیست",
DESCRIPTION_LENGTH = "طول توضیحات باید بین ۱۰ تا ۵۰۰ کاراکتر باشد",
BUSINESS_NAME_STRING = "نام کسب و کار باید یک رشته باشد",
BUSINESS_PHONE_REQUIRED = "شماره تلفن کسب و کار نمی‌تواند خالی باشد",
BUSINESS_DESCRIPTION_REQUIRED = "توضیحات کسب و کار نمی‌تواند خالی باشد",
BUSINESS_DESCRIPTION_LENGTH = "توضیحات کسب و کار باید بین ۱۰ تا ۵۰۰ کاراکتر باشد",
BUSINESS_DESCRIPTION_STRING = "توضیحات کسب و کار باید یک رشته باشد",
IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت اشتراک باید یکی از مقادیر ۰ و ۱ باشد",
}
export const enum InvoiceMessage {
@@ -369,6 +389,8 @@ export const enum InvoiceMessage {
INVOICE_PAID = "صورت حساب با موفقیت پرداخت شد",
DISCOUNT_APPLIED = "تخفیف با موفقیت اعمال شد",
DISCOUNT_CANCELLED = "تخفیف با موفقیت لغو شد",
INVOICE_IS_REJECTED = "فاکتور قبلا رد شده است",
INVOICE_CAN_NOT_PAID = "فاکتور قابل پرداخت نیست",
}
export const enum LearningMessage {
@@ -488,6 +510,7 @@ export const enum AdsMessage {
ADS_ACTIVATED = "تبلیغ با موفقیت فعال شد",
ADS_DEACTIVATED = "تبلیغ با موفقیت غیرفعال شد",
ROLE_NOT_FOUND = "نقشی با این شناسه یافت نشد",
IS_ACTIVE_SHOULD_BE_BOOLEAN = "وضعیت فعال بودن باید یک بولین باشد",
}
export const enum ProvinceMessage {
+3 -3
View File
@@ -1,15 +1,15 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsDateString, IsEnum, IsOptional, IsString } from "class-validator";
import { IsDateString, IsEnum, IsIn, IsOptional, IsString } from "class-validator";
import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { AdsMessage } from "../../../common/enums/message.enum";
import { AdsMessage, CategoryMessage } from "../../../common/enums/message.enum";
import { AdsDisplayLocation } from "../enums/ads-location.enum";
export class AdsSearchQueryDto extends PaginationDto {
@IsOptional()
@Type(() => Number)
// @IsBoolean({ message: CategoryMessage.IS_ACTIVE_SHOULD_BE_BOOLEAN })
@IsIn([1, 0], { message: CategoryMessage.IS_ACTIVE_SHOULD_BE_1_0 })
@ApiPropertyOptional({ description: "ads status", example: 1 })
isActive?: number;
@@ -1,77 +1,54 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, MinLength } from "class-validator";
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsDateString,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
Length,
MinLength,
} from "class-validator";
import { AnnouncementMessage } from "../../../common/enums/message.enum";
export class CreateAnnouncementDto {
@IsNotEmpty({ message: AnnouncementMessage.TITLE_IS_REQUIRED })
@IsString({ message: AnnouncementMessage.TITLE_STRING })
@ApiProperty({
description: "Title of the announcement",
example: "اطلاعیه جدید",
})
@Length(5, 100, { message: AnnouncementMessage.TITLE_LENGTH })
@ApiProperty({ description: "Title of the announcement", example: "اطلاعیه جدید" })
title: string;
@IsNotEmpty({ message: AnnouncementMessage.CONTENT_IS_REQUIRED })
@IsString({ message: AnnouncementMessage.CONTENT_IS_STRING })
@MinLength(10, { message: AnnouncementMessage.CONTENT_MUST_BE_LONGER })
@ApiProperty({
description: "Content of the announcement",
example: "اطلاعیه جدید برای کاربران",
})
@ApiProperty({ description: "Content of the announcement", example: "اطلاعیه جدید برای کاربران" })
content: string;
@IsOptional()
@IsNotEmpty({ message: AnnouncementMessage.PUBLISH_AT_IS_REQUIRED })
@ApiProperty({
description: "Publish date of the announcement",
example: "2023-10-01T10:00:00Z",
})
@IsDateString({}, { message: AnnouncementMessage.PUBLISH_AT_MUST_BE_DATE })
@ApiProperty({ description: "Publish date of the announcement", example: "2023-10-01T10:00:00Z" })
publishAt: Date;
@IsBoolean()
@ApiProperty({
description: "Is this announcement public?",
example: false,
})
isPublic: boolean;
@IsBoolean()
@ApiProperty({
description: "Is this announcement important?",
example: false,
})
@IsNotEmpty({ message: AnnouncementMessage.IMPORTANT_IS_REQUIRED })
@IsBoolean({ message: AnnouncementMessage.IMPORTANT_MUST_BE_BOOLEAN })
@ApiProperty({ description: "Is this announcement important?", example: false })
isImportant: boolean;
@IsOptional()
@IsNotEmpty({ message: AnnouncementMessage.SERVICE_MUST_BE_ID })
@IsUUID("4", { message: AnnouncementMessage.SERVICE_MUST_BE_UUID })
@ApiProperty({ description: "Service ID", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" })
serviceId: string;
@ArrayMinSize(1)
@IsUUID("4", { message: AnnouncementMessage.SERVICE_MUST_BE_UUID, each: true })
@ApiProperty({ type: [String], description: "Service IDs", example: ["d290f1ee-6c54-4b01-90e6-d701748f0851"] })
serviceIds: string[];
@IsOptional()
@IsArray()
@IsUUID("4", {
each: true,
message: AnnouncementMessage.GROUP_MUST_BE_UUID,
})
@ApiProperty({
type: [String],
description: "Array of group ids",
example: ["f7b1b3b1-1b1b-4b1b-8b1b-1b1b1b1b1b1b"],
})
groupIds: string[];
@IsOptional()
@IsArray()
@IsUUID("4", {
each: true,
message: AnnouncementMessage.USER_MUST_BE_UUID,
})
@ApiProperty({
type: [String],
description: "Array of users ids",
example: ["f7b1b3b1-1b1b-4b1b-8b1b-1b1b1b1b1b1b"],
})
@ArrayMinSize(1)
@IsArray({ message: AnnouncementMessage.USER_IDS_MUST_BE_ARRAY })
@IsUUID("4", { each: true, message: AnnouncementMessage.USER_MUST_BE_UUID })
@ApiProperty({ type: [String], description: "Array of users ids", example: ["f7b1b3b1-1b1b-4b1b-8b1b-1b1b1b1b1b1b"] })
userIds: string[];
}
@@ -1,7 +1,6 @@
import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common";
import { Controller, Get, Param, Query } from "@nestjs/common";
import { ApiProperty, ApiTags } from "@nestjs/swagger";
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";
@@ -16,13 +15,13 @@ import { PermissionEnum } from "../users/enums/permission.enum";
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) {
return await this.announcementService.createAnnouncement(createAnnouncementDto);
}
// @AuthGuards()
// @PermissionsDec(PermissionEnum.ANNOUNCEMENTS)
// @ApiProperty({ description: "Create a new announcement ===> login as admin" })
// @Post()
// async create(@Body() createAnnouncementDto: CreateAnnouncementDto) {
// return await this.announcementService.createAnnouncement(createAnnouncementDto);
// }
@AuthGuards()
@ApiProperty({ description: "Get all announcements ===> login as admin" })
@@ -33,9 +32,7 @@ export class AnnouncementController {
}
@AuthGuards()
@ApiProperty({
description: "Get all announcements ===> login as user",
})
@ApiProperty({ description: "Get all announcements ===> login as user" })
@Get("user")
async getAllAnnouncementsByUser(@Query() queryDto: SearchCriticismQueryDto, @UserDec() user: User) {
return await this.announcementService.getAllAnnouncementsByUser(queryDto, user.id);
@@ -1,10 +1,8 @@
import { Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany } from "typeorm";
import { Column, Entity, Index, ManyToOne, OneToMany } from "typeorm";
import { UserAnnouncement } from "./user-announcement.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { DanakService } from "../../danak-services/entities/danak-service.entity";
import { UserGroup } from "../../users/entities/user-group.entity";
import { User } from "../../users/entities/user.entity";
@Entity()
@Index(["isPublic", "createdAt"])
@@ -15,30 +13,22 @@ export class Announcement extends BaseEntity {
@Column({ type: "text" })
content: string;
@Column({ type: "timestamp", default: null, nullable: true })
publishAt: Date;
@Column({ type: "timestamp", nullable: true, default: null })
publishAt: Date | null;
//TODO: Add customer here
@ManyToOne(() => DanakService, (service) => service.announcements, { eager: true, onDelete: "CASCADE" })
service: DanakService;
targetService: DanakService;
@Column({ type: "boolean", default: false })
isImportant: boolean;
@ManyToMany(() => UserGroup, (group) => group.announcements)
targetGroups: UserGroup[];
@OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.announcement)
userAnnouncements: UserAnnouncement[];
@Column({ default: false })
@Column({ type: "boolean", default: false })
isPublic: boolean;
isPublicAnnouncement(): boolean {
return this.isPublic;
}
@ManyToMany(() => User, (user) => user.announcements)
@JoinTable({ name: "announcement_users" })
users: User[];
@OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.announcement)
userAnnouncements: UserAnnouncement[];
}
@@ -1,4 +1,4 @@
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
import { Column, Entity, ManyToOne } from "typeorm";
import { Announcement } from "./announcement.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
@@ -7,11 +7,9 @@ import { User } from "../../users/entities/user.entity"; // Assuming you have a
@Entity()
export class UserAnnouncement extends BaseEntity {
@ManyToOne(() => User, (user) => user.userAnnouncements, { onDelete: "CASCADE" })
@JoinColumn({ name: "userId" })
user: User;
@ManyToOne(() => Announcement, (announcement) => announcement.userAnnouncements, { onDelete: "CASCADE" })
@JoinColumn({ name: "announcementId" })
announcement: Announcement;
@Column({ type: "boolean", default: false })
@@ -1,10 +1,7 @@
import { BadRequestException, Injectable } from "@nestjs/common";
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 { AnnouncementMessage } from "../../../common/enums/message.enum";
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";
@@ -14,39 +11,8 @@ export class AnnouncementService {
constructor(
private readonly announcementRepository: AnnouncementRepository,
private readonly userAnnouncementRepository: UserAnnouncementRepository,
private readonly danakServicesService: DanakServicesService,
private readonly usersService: UsersService,
) {}
/***************************************** */
async createAnnouncement(createAnnouncementDto: CreateAnnouncementDto) {
let service;
let groups;
let users;
if (createAnnouncementDto.serviceId) {
service = await this.danakServicesService.findServiceById(createAnnouncementDto.serviceId);
}
if (createAnnouncementDto.groupIds) {
groups = await this.usersService.getUserGroupsByIds(createAnnouncementDto.groupIds);
}
if (createAnnouncementDto.userIds) {
users = await this.usersService.getUsersByIds(createAnnouncementDto.userIds);
}
//TODO: Create Queue for publishAT
const announcement = this.announcementRepository.create({
...createAnnouncementDto,
service,
targetGroups: groups?.userGroups,
users,
});
await this.announcementRepository.save(announcement);
return {
message: CommonMessage.CREATED,
announcement,
};
}
/***************************************** */
async getAllAnnouncements(queryDto: SearchAnnouncementQueryDto, userId: string) {
@@ -81,58 +47,6 @@ export class AnnouncementService {
/***************************************** */
// 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);
@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
import { IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator";
import { CriticismMessage } from "../../../common/enums/message.enum";
@@ -15,8 +15,8 @@ export class CreateCriticismDto {
content: string;
@IsOptional()
@IsNotEmpty()
@IsString({ each: true })
@IsNotEmpty({ message: CriticismMessage.FILES_IS_REQUIRED })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: CriticismMessage.FILES_SHOULD_BE_URL, each: true })
@ApiProperty({
description: "The ulr of the criticism files",
example: ["https://example.com/image1.png", "https://example.com/image2.png"],
@@ -8,7 +8,6 @@ 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 { User } from "../users/entities/user.entity";
import { PermissionEnum } from "../users/enums/permission.enum";
@Controller("criticisms")
@@ -19,8 +18,8 @@ export class CriticismController {
@AuthGuards()
@ApiOperation({ summary: "Create a new criticism" })
@Post()
create(@Body() createDto: CreateCriticismDto, @UserDec() user: User) {
return this.criticismService.create(createDto, user.id);
create(@Body() createDto: CreateCriticismDto, @UserDec("id") userId: string) {
return this.criticismService.create(createDto, userId);
}
@AuthGuards()
@@ -16,9 +16,10 @@ export class CriticismsService {
async create(createDto: CreateCriticismDto, userId: string) {
const { user } = await this.usersService.findOneById(userId);
const files = createDto.files.map((file) => {
return { fileUrl: file };
});
const files =
createDto.files?.map((file) => {
return { fileUrl: file };
}) || [];
const criticism = this.criticismRepository.create({
...createDto,
user,
@@ -1,6 +1,6 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsOptional, IsString, IsUUID } from "class-validator";
import { IsIn, IsOptional, IsString, IsUUID } from "class-validator";
import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { CategoryMessage } from "../../../common/enums/message.enum";
@@ -15,7 +15,7 @@ export class CategorySearchQueryDto {
export class CategoryListSearchQueryDto extends PaginationDto {
@IsOptional()
@Type(() => Number)
// @IsBoolean({ message: CategoryMessage.IS_ACTIVE_SHOULD_BE_BOOLEAN })
@IsIn([1, 0], { message: CategoryMessage.IS_ACTIVE_SHOULD_BE_1_0 })
@ApiPropertyOptional({ description: "Category status", example: 1 })
isActive?: number;
@@ -1,9 +1,9 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsOptional, IsString } from "class-validator";
import { IsIn, IsOptional, IsString } from "class-validator";
import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { CategoryMessage } from "../../../common/enums/message.enum";
import { CategoryMessage, ServiceMessage } from "../../../common/enums/message.enum";
export class DanakServicesSearchQueryDto extends PaginationDto {
@IsOptional()
@@ -13,7 +13,7 @@ export class DanakServicesSearchQueryDto extends PaginationDto {
@IsOptional()
@Type(() => Number)
// @IsBoolean({ message: CategoryMessage.IS_ACTIVE_SHOULD_BE_BOOLEAN })
@IsIn([1, 0], { message: ServiceMessage.IS_ACTIVE_SHOULD_BE_1_0 })
@ApiPropertyOptional({ description: "service status", example: 1 })
isActive?: number;
@@ -24,6 +24,7 @@ export class DanakServicesSearchQueryDto extends PaginationDto {
@IsOptional()
@Type(() => Number)
@IsIn([1, 0], { message: ServiceMessage.IS_SUGGEST_SHOULD_BE_1_0 })
@ApiPropertyOptional({ description: "is danak suggest this service", example: 1 })
isDanakSuggest?: number;
}
@@ -7,7 +7,6 @@ import { BaseEntity } from "../../../common/entities/base.entity";
import { Ads } from "../../ads/entities/ads.entity";
import { Announcement } from "../../announcements/entities/announcement.entity";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { UserQuickAccess } from "../../subscriptions/entities/user-quick-access.entity";
import { Ticket } from "../../tickets/entities/ticket.entity";
import { ServicesLanguage } from "../enums/services-language.enum";
@@ -62,7 +61,7 @@ export class DanakService extends BaseEntity {
@OneToMany(() => DanakServiceImage, (danakServiceImage) => danakServiceImage.danakService, { cascade: true })
images: DanakServiceImage[];
@OneToMany(() => Announcement, (announcement) => announcement.service)
@OneToMany(() => Announcement, (announcement) => announcement.targetService)
announcements: Announcement[];
@OneToMany(() => Ticket, (ticket) => ticket.danakService)
@@ -77,8 +76,6 @@ export class DanakService extends BaseEntity {
@OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.service)
reviews: DanakServiceReview[];
@OneToMany(() => UserQuickAccess, (quickAccess) => quickAccess.service)
quickAccess: UserQuickAccess[];
// @ManyToMany(() => Discount, (discount) => discount.services, { nullable: true })
// discounts?: Discount[];
}
@@ -104,7 +104,7 @@ export class DanakServicesService {
},
order: { createdAt: "DESC" },
});
if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST);
if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST_OR_HAS_NO_SERVICE);
return {
category,
};
@@ -1,6 +1,8 @@
export enum InvoiceStatus {
APPROVED = "APPROVED",
PENDING = "PENDING",
APPROVED = "APPROVED",
REJECTED = "REJECTED",
WAIT_PAYMENT = "WAIT_PAYMENT",
PAID = "PAID",
EXPIRED = "EXPIRED",
CANCELLED = "CANCELLED",
@@ -18,8 +18,6 @@ import { PermissionEnum } from "../users/enums/permission.enum";
export class InvoicesController {
constructor(private readonly invoiceService: InvoicesService) {}
/**************************************** */
@ApiOperation({ summary: "create an invoice ==> admin route" })
@PermissionsDec(PermissionEnum.INVOICES)
@AuthGuards()
@@ -28,8 +26,6 @@ export class InvoicesController {
return this.invoiceService.createInvoiceAdmin(createDto);
}
/**************************************** */
@ApiOperation({ summary: "get all invoices ==> admin route" })
@AuthGuards()
@PermissionsDec(PermissionEnum.INVOICES)
@@ -39,8 +35,6 @@ export class InvoicesController {
return this.invoiceService.getInvoices(queryDto);
}
/**************************************** */
@ApiOperation({ summary: "get all user invoices" })
@AuthGuards()
@Pagination()
@@ -49,8 +43,6 @@ export class InvoicesController {
return this.invoiceService.getUserInvoices(queryDto, userId);
}
/**************************************** */
@ApiOperation({ summary: "get single invoice by Id " })
@AuthGuards()
@Get(":id")
@@ -58,8 +50,6 @@ export class InvoicesController {
return this.invoiceService.getInvoiceById(paramDto.id, isAdmin, userId);
}
/**************************************** */
@ApiOperation({ summary: "approve request invoice by user" })
@AuthGuards()
@Post(":id/approve/request")
@@ -67,8 +57,6 @@ export class InvoicesController {
return this.invoiceService.approveRequest(paramDto.id, userId, updateDto);
}
/**************************************** */
@ApiOperation({ summary: "approve invoice by user" })
@AuthGuards()
@Patch(":id/approve")
@@ -76,8 +64,6 @@ export class InvoicesController {
return this.invoiceService.approveInvoiceByUser(paramDto.id, userId, verifyOtpDto);
}
/**************************************** */
@ApiOperation({ summary: "apply discount on invoice by user" })
@AuthGuards()
@Post(":id/apply-discount")
@@ -85,8 +71,6 @@ export class InvoicesController {
return this.invoiceService.applyDiscount(paramDto.id, applyDiscountDto, userId);
}
/**************************************** */
@ApiOperation({ summary: "cancel discount by user" })
@AuthGuards()
@Post(":id/cancel-discount")
@@ -94,8 +78,6 @@ export class InvoicesController {
return this.invoiceService.cancelDiscount(paramDto.id, userId);
}
/**************************************** */
@ApiOperation({ summary: "pay invoice by user" })
@AuthGuards()
@Post(":id/pay")
@@ -265,6 +265,11 @@ export class InvoicesService {
const invoice = await this.getPendingInvoiceByIdWithQueryRunner(invoiceId, user.id, queryRunner);
if (invoice.status === InvoiceStatus.REJECTED) throw new BadRequestException(InvoiceMessage.INVOICE_IS_REJECTED);
if (invoice.status === InvoiceStatus.EXPIRED) throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE);
if (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID);
if (invoice.status !== InvoiceStatus.APPROVED) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_PAID);
const userWallet = await this.walletsService.getWalletByUserId(user.id, queryRunner);
if (new Decimal(userWallet.balance).lessThan(invoice.totalPrice)) throw new BadRequestException(WalletMessage.INSUFFICIENT_BALANCE);
@@ -297,7 +302,6 @@ export class InvoicesService {
async getPendingInvoiceByIdWithQueryRunner(invoiceId: string, userId: string, queryRunner: QueryRunner) {
const invoice = await queryRunner.manager.findOneBy(Invoice, { user: { id: userId }, status: InvoiceStatus.PENDING, id: invoiceId });
console.log(invoice, invoiceId, userId);
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
return invoice;
}
@@ -1,6 +1,6 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsOptional, IsString } from "class-validator";
import { IsIn, IsOptional, IsString } from "class-validator";
import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { SubscriptionMessage } from "../../../common/enums/message.enum";
@@ -8,7 +8,7 @@ import { SubscriptionMessage } from "../../../common/enums/message.enum";
export class ServiceSubsQueryDto extends PaginationDto {
@IsOptional()
@Type(() => Number)
// @IsBoolean({ message: CategoryMessage.IS_ACTIVE_SHOULD_BE_BOOLEAN })
@IsIn([1, 0], { message: SubscriptionMessage.IS_ACTIVE_SHOULD_BE_1_0 })
@ApiPropertyOptional({ description: "Category status", example: 1 })
isActive?: number;
@@ -15,14 +15,14 @@ export class SubscribeServiceDto {
@ApiProperty({ description: "Name of business", example: "Test 1" })
businessName: string;
@IsNotEmpty()
@IsNotEmpty({ message: SubscriptionMessage.BUSINESS_PHONE_REQUIRED })
@IsPhoneNumber("IR", { message: SubscriptionMessage.BUSINESS_PHONE_INVALID })
@ApiProperty({ description: "Phone number of the business", example: "+98 234 567 8901", required: false })
businessPhone: string;
@IsNotEmpty()
@IsString()
@Length(5, 500, { message: SubscriptionMessage.DESCRIPTION_LENGTH })
@IsNotEmpty({ message: SubscriptionMessage.BUSINESS_DESCRIPTION_REQUIRED })
@IsString({ message: SubscriptionMessage.BUSINESS_DESCRIPTION_STRING })
@Length(5, 500, { message: SubscriptionMessage.BUSINESS_DESCRIPTION_LENGTH })
@ApiProperty({ description: "A short description of the business or service", example: "We offer the best subscription plans." })
description: string;
}
@@ -1,16 +1,20 @@
import { Column, Entity, ManyToOne, Unique } from "typeorm";
import { UserSubscription } from "./user-subscription.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { DanakService } from "../../danak-services/entities/danak-service.entity";
import { User } from "../../users/entities/user.entity";
@Entity()
@Unique(["user", "service"])
@Unique(["user", "userSubscription"])
export class UserQuickAccess extends BaseEntity {
@ManyToOne(() => User, (user) => user.quickAccesses, { nullable: false, onDelete: "SET NULL" })
@ManyToOne(() => User, (user) => user.quickAccesses, { nullable: false, onDelete: "CASCADE" })
user: User;
@ManyToOne(() => DanakService, { nullable: false, onDelete: "SET NULL" })
@ManyToOne(() => UserSubscription, { nullable: false, onDelete: "CASCADE" })
userSubscription: UserSubscription;
@ManyToOne(() => DanakService, { nullable: false, onDelete: "CASCADE" })
service: DanakService;
@Column({ type: "boolean", default: true })
@@ -33,4 +33,7 @@ export class UserSubscription extends BaseEntity {
@Column({ type: "boolean", default: false })
isCanceled: boolean;
// @OneToMany(() => UserQuickAccess, (quickAccess) => quickAccess.userSubscription)
// quickAccess: UserQuickAccess[];
}
@@ -225,11 +225,13 @@ export class SubscriptionsService {
if (!plan) throw new BadRequestException(SubscriptionMessage.NOT_FOUND);
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + plan.duration);
const startDate = dayjs().toDate();
const endDate = dayjs().add(plan.duration, "day").toDate();
// const startDate = new Date();
// const endDate = new Date();
// endDate.setDate(startDate.getDate() + plan.duration);
//
const userSubscription = queryRunner.manager.create(UserSubscription, { user, plan, startDate, endDate });
const userSubscription = queryRunner.manager.create(UserSubscription, { ...subscribeDto, user, plan, startDate, endDate });
await queryRunner.manager.save(UserSubscription, userSubscription);
@@ -237,18 +239,16 @@ export class SubscriptionsService {
let finalPrice = new Decimal(plan.price);
if (plan.discounts && plan.discounts.length > 0) {
plan.discounts.forEach((discount) => {
if (discount.isActive && new Date() >= discount.startDate && new Date() <= discount.endDate) {
if (discount.calculationType === DiscountCalculationType.PERCENTAGE) {
const discountAmount = finalPrice.mul(discount.amount).div(100);
finalPrice = finalPrice.sub(discountAmount);
} else if (discount.calculationType === DiscountCalculationType.FIXED) {
finalPrice = finalPrice.sub(discount.amount);
}
}
});
}
const activeDiscounts =
plan.discounts?.filter((discount) => discount.isActive && new Date() >= discount.startDate && new Date() <= discount.endDate) || [];
activeDiscounts.forEach((discount) => {
if (discount.calculationType === DiscountCalculationType.PERCENTAGE) {
finalPrice = finalPrice.sub(finalPrice.mul(discount.amount).div(100));
} else if (discount.calculationType === DiscountCalculationType.FIXED) {
finalPrice = finalPrice.sub(discount.amount);
}
});
plan.price = finalPrice;
@@ -264,8 +264,7 @@ export class SubscriptionsService {
await this.walletsService.createSubscriptionTransaction(finalPrice, userWallet.id, queryRunner);
//TODO: need queue handling for notification
const invoiceDueDate = new Date(userSubscription.endDate);
invoiceDueDate.setDate(userSubscription.endDate.getDate() - 3);
const invoiceDueDate = dayjs(userSubscription.endDate).subtract(3, "day").toDate();
const invoice = await this.invoicesService.createInvoiceForSubscription(userId, plan, invoiceDueDate, queryRunner);
userSubscription.status = SubscriptionStatus.ACTIVE;
@@ -419,19 +418,4 @@ export class SubscriptionsService {
const growthRate = ((currentYearCount - lastYearCount) / lastYearCount) * 100;
return Math.round(growthRate * 100) / 100;
}
/********************************* */
async activeUserSubscription(userId: string, serviceId: string) {
const activeSubscription = await this.userSubscriptionsRepository.findOne({
where: {
user: { id: userId },
plan: { service: { id: serviceId } },
status: SubscriptionStatus.ACTIVE,
},
relations: ["plan", "plan.service"],
});
console.log(activeSubscription);
return activeSubscription;
}
}
@@ -1,8 +1,7 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { SubscriptionsService } from "./subscriptions.service";
import { CommonMessage, ServiceMessage, UserMessage, UserQuickAccessMessage } from "../../../common/enums/message.enum";
import { DanakServicesService } from "../../danak-services/providers/danak-services.service";
import { CommonMessage, UserMessage, UserQuickAccessMessage } from "../../../common/enums/message.enum";
import { UsersService } from "../../users/providers/users.service";
import { UserQuickAccessRepository } from "../repositories/users-quick-access.repository";
@@ -11,32 +10,25 @@ export class UserQuickAccessService {
constructor(
private readonly quickAccessRepository: UserQuickAccessRepository,
private readonly usersService: UsersService,
private readonly danakServicesService: DanakServicesService,
private readonly subscriptionService: SubscriptionsService,
) {}
async addQuickAccess(userId: string, serviceId: string) {
async addQuickAccess(userId: string, userSubId: string) {
const { user } = await this.usersService.findOneById(userId);
if (!user) throw new NotFoundException(UserMessage.USER_NOT_FOUND);
const service = await this.danakServicesService.findServiceById(serviceId);
if (!service) throw new NotFoundException(ServiceMessage.SERVICE_NOT_EXIST);
const activeSubscription = await this.subscriptionService.activeUserSubscription(user.id, service.id);
if (!activeSubscription) {
throw new BadRequestException(UserQuickAccessMessage.USER_SUBS_NOT_FOUND);
}
const { userSubscription } = await this.subscriptionService.getUserSubscriptionById(userSubId, user.id);
const userQuickAccesses = await this.quickAccessRepository.count({ where: { user: { id: userId }, isActive: true } });
if (userQuickAccesses >= 4) {
throw new BadRequestException(UserQuickAccessMessage.QUICK_ACCESS_LIMIT_REACHED);
}
if (userQuickAccesses >= 4) throw new BadRequestException(UserQuickAccessMessage.QUICK_ACCESS_LIMIT_REACHED);
const existQuickAccess = await this.quickAccessRepository.findOne({
where: { user: { id: user.id }, userSubscription: { id: userSubId } },
});
const existQuickAccess = await this.quickAccessRepository.findOne({ where: { user: { id: user.id }, service: { id: service.id } } });
console.log(existQuickAccess);
if (existQuickAccess) throw new BadRequestException(UserQuickAccessMessage.QUICK_ACCESS_EXIST);
const quickAccess = this.quickAccessRepository.create({ user, service });
const quickAccess = this.quickAccessRepository.create({ user, userSubscription, service: userSubscription.plan.service });
await this.quickAccessRepository.save(quickAccess);
@@ -45,18 +37,22 @@ export class UserQuickAccessService {
quickAccess,
};
}
async removeQuickAccess(userId: string, serviceId: string) {
await this.quickAccessRepository.delete({ user: { id: userId }, service: { id: serviceId } });
//************************************** */
async removeQuickAccess(userId: string, userSubId: string) {
await this.quickAccessRepository.delete({ user: { id: userId }, userSubscription: { id: userSubId } });
return {
message: CommonMessage.DELETED,
};
}
//************************************** */
async getUserQuickAccess(userId: string) {
const quickAccesses = await this.quickAccessRepository.find({
where: { user: { id: userId }, isActive: true },
relations: ["service"],
relations: {
userSubscription: true,
service: true,
},
});
return { quickAccesses };
}
@@ -86,9 +86,9 @@ export class SubscriptionsController {
@ApiOperation({ summary: "add quick access a service ==> user route" })
@AuthGuards()
@Post(":serviceId/add-quick-access")
addQuickAccess(@Param() paramDto: ServiceIdParamDto, @UserDec("id") userId: string) {
return this.userQuickAccessService.addQuickAccess(userId, paramDto.serviceId);
@Post(":id/add-quick-access")
addQuickAccess(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
return this.userQuickAccessService.addQuickAccess(userId, paramDto.id);
}
@ApiOperation({ summary: "get quick access a service ==> user route" })
@@ -100,8 +100,8 @@ export class SubscriptionsController {
@ApiOperation({ summary: "remove quick access a service ==> user route" })
@AuthGuards()
@Delete(":serviceId/remove-quick-access")
removeQuickAccess(@Param() paramDto: ServiceIdParamDto, @UserDec("id") userId: string) {
return this.userQuickAccessService.removeQuickAccess(userId, paramDto.serviceId);
@Delete(":id/remove-quick-access")
removeQuickAccess(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
return this.userQuickAccessService.removeQuickAccess(userId, paramDto.id);
}
}
@@ -154,7 +154,7 @@ export class TicketsService {
const findOptions = {
where: {
user: { id: userId },
...(queryDto.status && { status: queryDto.status }),
...(queryDto.status ? { status: queryDto.status } : { status: Not(TicketStatus.CLOSED) }),
},
relations: {
category: true,
@@ -2,7 +2,6 @@ import { Column, Entity, JoinTable, ManyToMany, OneToMany } from "typeorm";
import { User } from "./user.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { Announcement } from "../../announcements/entities/announcement.entity";
import { TicketCategory } from "../../tickets/entities/ticket-category.entity";
@Entity()
@@ -17,9 +16,9 @@ export class UserGroup extends BaseEntity {
@JoinTable()
users: User[];
@ManyToMany(() => Announcement, (announcement) => announcement.targetGroups)
@JoinTable()
announcements: Announcement[];
// @ManyToMany(() => Announcement, (announcement) => announcement.targetGroups)
// @JoinTable()
// announcements: Announcement[];
@OneToMany(() => TicketCategory, (ticketCategory) => ticketCategory.group)
ticketCategories: TicketCategory[];
+2 -6
View File
@@ -8,7 +8,6 @@ import { UserGroup } from "./user-group.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { Address } from "../../address/entities/address.entity";
import { City } from "../../address/entities/city.entity";
import { Announcement } from "../../announcements/entities/announcement.entity";
import { UserAnnouncement } from "../../announcements/entities/user-announcement.entity";
import { Criticism } from "../../criticisms/entities/criticism.entity";
import { DanakServiceReview } from "../../danak-services/entities/danak-service-review.entity";
@@ -90,9 +89,6 @@ export class User extends BaseEntity {
@OneToMany(() => Criticism, (criticism) => criticism.user)
criticisms: Criticism[];
@OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.user)
userAnnouncements: UserAnnouncement[];
@OneToMany(() => UserSetting, (settings) => settings.user, { nullable: false })
settings: UserSetting;
@@ -130,8 +126,8 @@ export class User extends BaseEntity {
@ManyToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.users)
usageDiscounts: UsageDiscount[];
@ManyToMany(() => Announcement, (announcement) => announcement.users)
announcements: Announcement[];
@OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.user)
userAnnouncements: UserAnnouncement[];
@OneToMany(() => UserQuickAccess, (quickAccess) => quickAccess.user)
quickAccesses: UserQuickAccess[];
+1 -2
View File
@@ -374,9 +374,8 @@ export class UsersService {
///****************************************************** */
async getUsersByIds(ids: string[]) {
async findUsersByIds(ids: string[]) {
const users = await this.userRepository.find({ where: { id: In(ids) } });
if (!users) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
return users;
}
@@ -1,5 +1,6 @@
import { BadRequestException, Injectable } from "@nestjs/common";
// eslint-disable-next-line import/no-named-as-default
import dayjs from "dayjs";
import Decimal from "decimal.js";
import { QueryRunner } from "typeorm";
@@ -115,6 +116,7 @@ export class WalletsService {
.take(limit)
.getManyAndCount();
// Current month totals
const totals = await this.walletsTransactionRepo
.createQueryBuilder("transaction")
.leftJoin("transaction.wallet", "wallet")
@@ -125,14 +127,64 @@ export class WalletsService {
])
.getRawOne();
const now = dayjs();
const firstDayOfMonth = now.startOf("month").toDate();
const firstDayOfPrevMonth = now.subtract(1, "month").startOf("month").toDate();
const lastDayOfPrevMonth = now.subtract(1, "month").endOf("month").toDate();
const prevMonthTotals = await this.walletsTransactionRepo
.createQueryBuilder("transaction")
.leftJoin("transaction.wallet", "wallet")
.where("wallet.userId = :userId", { userId })
.andWhere("transaction.createdAt >= :startDate", { startDate: firstDayOfPrevMonth })
.andWhere("transaction.createdAt <= :endDate", { endDate: lastDayOfPrevMonth })
.select([
`COALESCE(SUM(CASE WHEN transaction.type = '${TransactionType.DEPOSIT}' THEN transaction.amount ELSE 0 END), 0) AS "totalDeposits"`,
`COALESCE(SUM(CASE WHEN transaction.type = '${TransactionType.WITHDRAWAL}' THEN transaction.amount ELSE 0 END), 0) AS "totalWithdrawals"`,
])
.getRawOne();
const wallet = await this.walletsRepository.findOneBy({ user: { id: userId } });
if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND);
const prevMonthWallet = await this.walletsTransactionRepo
.createQueryBuilder("transaction")
.leftJoin("transaction.wallet", "wallet")
.where("wallet.userId = :userId", { userId })
.andWhere("transaction.createdAt < :date", { date: firstDayOfMonth })
.select([
`COALESCE(SUM(CASE WHEN transaction.type = '${TransactionType.DEPOSIT}' THEN transaction.amount ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN transaction.type = '${TransactionType.WITHDRAWAL}' THEN transaction.amount ELSE 0 END), 0) AS "prevBalance"`,
])
.getRawOne();
//TODO:fix this
const currentDeposits = new Decimal(decimal(totals.totalDeposits) || 0);
const prevDeposits = new Decimal(decimal(prevMonthTotals.totalDeposits) || 0);
const depositsPercentChange = prevDeposits.isZero() ? null : currentDeposits.minus(prevDeposits).div(prevDeposits).mul(100);
const currentWithdrawals = new Decimal(decimal(totals.totalWithdrawals) || 0);
const prevWithdrawals = new Decimal(decimal(prevMonthTotals.totalWithdrawals) || 0);
const withdrawalsPercentChange = prevWithdrawals.isZero()
? null
: currentWithdrawals.minus(prevWithdrawals).div(prevWithdrawals).mul(100);
const currentBalance = wallet.balance;
const prevBalance = decimal(prevMonthWallet.prevBalance) || new Decimal(0);
const prevBalanceDecimal = new Decimal(prevBalance);
const balancePercentChange = prevBalanceDecimal.isZero()
? null
: currentBalance.minus(prevBalanceDecimal).div(prevBalanceDecimal).mul(100).toNumber();
return {
balance: wallet?.balance,
balance: currentBalance,
balancePercentChange,
transactions,
count,
totalDeposits: decimal(totals.totalDeposits) || 0,
totalWithdrawals: decimal(totals.totalWithdrawals) || 0,
totalDeposits: currentDeposits.toNumber(),
totalDepositsPercentChange: depositsPercentChange,
totalWithdrawals: currentWithdrawals.toNumber(),
totalWithdrawalsPercentChange: withdrawalsPercentChange,
paginate: true,
};
}