chore: complete create discount
This commit is contained in:
@@ -17,6 +17,7 @@ import { AuthModule } from "./modules/auth/auth.module";
|
|||||||
import { ContactUsModule } from "./modules/contact-us/contact-us.module";
|
import { ContactUsModule } from "./modules/contact-us/contact-us.module";
|
||||||
import { CriticismModule } from "./modules/criticisms/criticisms.module";
|
import { CriticismModule } from "./modules/criticisms/criticisms.module";
|
||||||
import { DanakServicesModule } from "./modules/danak-services/danak-services.module";
|
import { DanakServicesModule } from "./modules/danak-services/danak-services.module";
|
||||||
|
import { DiscountModule } from "./modules/discounts/discounts.module";
|
||||||
import { InvoicesModule } from "./modules/invoices/invoices.module";
|
import { InvoicesModule } from "./modules/invoices/invoices.module";
|
||||||
import { LearningModule } from "./modules/learnings/learning.module";
|
import { LearningModule } from "./modules/learnings/learning.module";
|
||||||
import { NotificationModule } from "./modules/notifications/notifications.module";
|
import { NotificationModule } from "./modules/notifications/notifications.module";
|
||||||
@@ -52,6 +53,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module";
|
|||||||
InvoicesModule,
|
InvoicesModule,
|
||||||
SubscriptionsModule,
|
SubscriptionsModule,
|
||||||
LearningModule,
|
LearningModule,
|
||||||
|
DiscountModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|||||||
@@ -330,3 +330,25 @@ export const enum LearningMessage {
|
|||||||
CATEGORY_MUST_BE_ID = "دستهبندی باید شناسه باشد",
|
CATEGORY_MUST_BE_ID = "دستهبندی باید شناسه باشد",
|
||||||
CATEGORY_MUST_BE_UUID = "دستهبندی باید UUID باشد",
|
CATEGORY_MUST_BE_UUID = "دستهبندی باید UUID باشد",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum DiscountMessage {
|
||||||
|
TYPE_REQUIRED = "وارد کردن نوع تخفیف الزامی است",
|
||||||
|
TYPE_INVALID = "نوع تخفیف نامعتبر است",
|
||||||
|
CALCULATION_TYPE_REQUIRED = "وارد کردن نوع محاسبه تخفیف الزامی است",
|
||||||
|
CALCULATION_TYPE_INVALID = "نوع محاسبه تخفیف نامعتبر است",
|
||||||
|
AMOUNT_REQUIRED = "وارد کردن مقدار تخفیف الزامی است",
|
||||||
|
AMOUNT_INVALID = "مقدار تخفیف باید عدد باشد",
|
||||||
|
TITLE_REQUIRED = "وارد کردن مقدار عنوان الزامی است",
|
||||||
|
TITLE_INVALID = "مقدار عنوان باید یک رشته باشد",
|
||||||
|
CODE_REQUIRED = "وارد کردن کد تخفیف الزامی است",
|
||||||
|
CODE_INVALID = "کد تخفیف نامعتبر است",
|
||||||
|
START_DATE_REQUIRED = "وارد کردن تاریخ شروع الزامی است",
|
||||||
|
START_DATE_INVALID = "تاریخ شروع نامعتبر است",
|
||||||
|
END_DATE_REQUIRED = "وارد کردن تاریخ پایان الزامی است",
|
||||||
|
END_DATE_INVALID = "تاریخ پایان نامعتبر است",
|
||||||
|
SERVICES_INVALID = "لیست خدمات باید آرایهای از اعداد باشد",
|
||||||
|
SERVICES_DUPLICATE = "خدمات نباید تکراری باشند",
|
||||||
|
SERVICES_UUID_INVALID = "ایدی سرویس معتبرنیست",
|
||||||
|
NOT_FOUND = "کد تخفیف با این مشخصات یافت نشد",
|
||||||
|
CODE_EXIST = "کد تخفیف قبلا ایجاد شده است",
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Column, Entity, ManyToOne, OneToMany } from "typeorm";
|
import { Column, Entity, ManyToMany, ManyToOne, OneToMany } from "typeorm";
|
||||||
|
|
||||||
import { DanakServiceCategory } from "./danak-service-category.entity";
|
import { DanakServiceCategory } from "./danak-service-category.entity";
|
||||||
import { DanakServiceImage } from "./danak-service-image.entity";
|
import { DanakServiceImage } from "./danak-service-image.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
import { Announcement } from "../../announcements/entities/announcement.entity";
|
import { Announcement } from "../../announcements/entities/announcement.entity";
|
||||||
|
import { Discount } from "../../discounts/entities/discount.entity";
|
||||||
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||||
import { Ticket } from "../../tickets/entities/ticket.entity";
|
import { Ticket } from "../../tickets/entities/ticket.entity";
|
||||||
import { ServicesLanguage } from "../enums/services-language.enum";
|
import { ServicesLanguage } from "../enums/services-language.enum";
|
||||||
@@ -63,6 +64,9 @@ export class DanakService extends BaseEntity {
|
|||||||
|
|
||||||
@OneToMany(() => SubscriptionPlan, (plan) => plan.service, { cascade: true })
|
@OneToMany(() => SubscriptionPlan, (plan) => plan.service, { cascade: true })
|
||||||
subscriptionPlans: SubscriptionPlan[];
|
subscriptionPlans: SubscriptionPlan[];
|
||||||
|
|
||||||
|
@ManyToMany(() => Discount, (discount) => discount.services, { nullable: true })
|
||||||
|
discounts?: Discount[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ManyToMany(() => User, (user) => user.danakServices)
|
// @ManyToMany(() => User, (user) => user.danakServices)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import { FindOptionsWhere, IsNull } from "typeorm";
|
import { FindOptionsWhere, In, IsNull } from "typeorm";
|
||||||
|
|
||||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||||
import { CategoryMessage, CommonMessage, ServiceMessage } from "../../../common/enums/message.enum";
|
import { CategoryMessage, CommonMessage, ServiceMessage } from "../../../common/enums/message.enum";
|
||||||
@@ -200,4 +200,54 @@ export class DanakServicesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/******************************************** */
|
/******************************************** */
|
||||||
|
|
||||||
|
async findServicesByIds(ids: string[]) {
|
||||||
|
const services = await this.danakServicesRepository.findBy({ id: In(ids) });
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************** */
|
||||||
|
|
||||||
|
// async getDiscountedPrice(serviceId: string) {
|
||||||
|
// const service = await this.danakServicesRepository.findOne({
|
||||||
|
// where: {
|
||||||
|
// id: serviceId,
|
||||||
|
// },
|
||||||
|
// relations: ["discount"],
|
||||||
|
// });
|
||||||
|
// if (!service) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID);
|
||||||
|
|
||||||
|
// const now = new Date();
|
||||||
|
|
||||||
|
// const serviceDiscount = service.discounts.filter(
|
||||||
|
// (d) => d.type === DiscountType.DIRECT && d.isActive && d.startDate <= now && d.endDate >= now,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const globalDiscounts = await this.discountRepository
|
||||||
|
// .createQueryBuilder("discount")
|
||||||
|
// .leftJoin("discount.services", "service")
|
||||||
|
// .where("discount.type = :discountType", { discountType: DiscountType.DIRECT })
|
||||||
|
// .andWhere("discount.isActive = true")
|
||||||
|
// .andWhere("discount.startDate <= :now", { now })
|
||||||
|
// .andWhere("discount.endDate >= :now", { now })
|
||||||
|
// .andWhere("service.id IS NULL")
|
||||||
|
// .getMany();
|
||||||
|
|
||||||
|
// const applicableDiscounts = [...serviceDiscount, ...globalDiscounts];
|
||||||
|
// if (applicableDiscounts.length === 0) return service.;
|
||||||
|
// let maxDiscountValue = 0;
|
||||||
|
// for (const discount of applicableDiscounts) {
|
||||||
|
// let discountValue = 0;
|
||||||
|
// if (discount.calculationType === DiscountCalculationType.FIXED) {
|
||||||
|
// discountValue = discount.amount;
|
||||||
|
// } else if (discount.calculationType === DiscountCalculationType.PERCENTAGE) {
|
||||||
|
// discountValue = service.price * (discount.amount / 100);
|
||||||
|
// }
|
||||||
|
// if (discountValue > maxDiscountValue) {
|
||||||
|
// maxDiscountValue = discountValue;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// const discountedPrice = service.price - maxDiscountValue;
|
||||||
|
// return discountedPrice > 0 ? discountedPrice : 0;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { ArrayUnique, IsArray, IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID } from "class-validator";
|
||||||
|
|
||||||
|
import { DiscountMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { DiscountCalculationType, DiscountType } from "../enums/discount-type.enum";
|
||||||
|
|
||||||
|
export class CreateDiscountDto {
|
||||||
|
@ApiProperty({ description: "title of discount", type: String, example: "discount 1" })
|
||||||
|
@IsNotEmpty({ message: DiscountMessage.TITLE_REQUIRED })
|
||||||
|
@IsString({ message: DiscountMessage.TITLE_INVALID })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "discount type", enum: DiscountType, example: DiscountType.CODE })
|
||||||
|
@IsNotEmpty({ message: DiscountMessage.TYPE_REQUIRED })
|
||||||
|
@IsEnum(DiscountType, { message: DiscountMessage.TYPE_INVALID })
|
||||||
|
type: DiscountType;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "calculation type", enum: DiscountCalculationType, example: DiscountCalculationType.FIXED })
|
||||||
|
@IsNotEmpty({ message: DiscountMessage.CALCULATION_TYPE_REQUIRED })
|
||||||
|
@IsEnum(DiscountCalculationType, { message: DiscountMessage.CALCULATION_TYPE_INVALID })
|
||||||
|
calculationType: DiscountCalculationType;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "amount of discount", type: Number, example: 100 })
|
||||||
|
@IsNotEmpty({ message: DiscountMessage.AMOUNT_REQUIRED })
|
||||||
|
@IsNumber({}, { message: DiscountMessage.AMOUNT_INVALID })
|
||||||
|
amount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "start discount date", type: String, format: "date-time", example: "2025-01-01T00:00:00Z" })
|
||||||
|
@IsNotEmpty({ message: DiscountMessage.START_DATE_REQUIRED })
|
||||||
|
@IsDateString({}, { message: DiscountMessage.START_DATE_INVALID })
|
||||||
|
startDate: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "end discount date", type: String, format: "date-time", example: "2025-12-31T23:59:59Z" })
|
||||||
|
@IsNotEmpty({ message: DiscountMessage.END_DATE_REQUIRED })
|
||||||
|
@IsDateString({}, { message: DiscountMessage.END_DATE_INVALID })
|
||||||
|
endDate: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "لیست آیدیهای خدمات", type: [String], example: ["f47ac10b-58cc-4372-a567-0e02b2c3d479"] })
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray({ message: DiscountMessage.SERVICES_INVALID })
|
||||||
|
@ArrayUnique({ message: DiscountMessage.SERVICES_DUPLICATE })
|
||||||
|
@IsUUID("4", { each: true, message: DiscountMessage.SERVICES_UUID_INVALID })
|
||||||
|
services?: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Body, Controller, Get, Param, Patch, Post } from "@nestjs/common";
|
||||||
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { CreateDiscountDto } from "./DTO/create-discount.dto";
|
||||||
|
import { DiscountService } from "./providers/discounts.service";
|
||||||
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
|
import { Roles } from "../../common/decorators/roles.decorator";
|
||||||
|
import { ParamDto } from "../../common/DTO/param.dto";
|
||||||
|
import { RoleEnum } from "../users/enums/role.enum";
|
||||||
|
|
||||||
|
@ApiTags("discounts")
|
||||||
|
@AuthGuards()
|
||||||
|
@Controller("discounts")
|
||||||
|
export class DiscountController {
|
||||||
|
constructor(private readonly discountService: DiscountService) {}
|
||||||
|
|
||||||
|
//************************************ */
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Create a new discount" })
|
||||||
|
@Roles(RoleEnum.ADMIN)
|
||||||
|
@Post()
|
||||||
|
async create(@Body() createDiscountDto: CreateDiscountDto) {
|
||||||
|
return this.discountService.create(createDiscountDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
//************************************ */
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Retrieve all discounts" })
|
||||||
|
@Roles(RoleEnum.ADMIN)
|
||||||
|
@Get()
|
||||||
|
async findAll() {
|
||||||
|
return this.discountService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
//************************************ */
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Retrieve a discount by ID" })
|
||||||
|
@Roles(RoleEnum.ADMIN)
|
||||||
|
@Get(":id")
|
||||||
|
async findOne(@Param() paramDto: ParamDto) {
|
||||||
|
return this.discountService.findOne(paramDto.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//************************************ */
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Toggle active/deactive discount" })
|
||||||
|
@Roles(RoleEnum.ADMIN)
|
||||||
|
@Patch(":id/toggle")
|
||||||
|
async toggleActive(@Param() paramDto: ParamDto) {
|
||||||
|
return this.discountService.toggleActive(paramDto.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//************************************ */
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
|
||||||
|
import { DiscountController } from "./discounts.controller";
|
||||||
|
import { Discount } from "./entities/discount.entity";
|
||||||
|
import { DiscountService } from "./providers/discounts.service";
|
||||||
|
import { DiscountRepository } from "./repositories/discount.repository";
|
||||||
|
import { DanakServicesModule } from "../danak-services/danak-services.module";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Discount]), DanakServicesModule],
|
||||||
|
providers: [DiscountService, DiscountRepository],
|
||||||
|
controllers: [DiscountController],
|
||||||
|
})
|
||||||
|
export class DiscountModule {}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import Decimal from "decimal.js";
|
||||||
|
import { Column, Entity, JoinTable, ManyToMany } from "typeorm";
|
||||||
|
|
||||||
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
||||||
|
import { DanakService } from "../../danak-services/entities/danak-service.entity";
|
||||||
|
import { DiscountCalculationType, DiscountType } from "../enums/discount-type.enum";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Discount extends BaseEntity {
|
||||||
|
@Column({ type: "varchar", length: 200, nullable: false })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Column({ type: "enum", enum: DiscountType })
|
||||||
|
type: DiscountType;
|
||||||
|
|
||||||
|
@Column({ type: "enum", enum: DiscountCalculationType })
|
||||||
|
calculationType: DiscountCalculationType;
|
||||||
|
|
||||||
|
@Column({ type: "decimal", precision: 10, scale: 2, nullable: false, transformer: new DecimalTransformer() })
|
||||||
|
amount: Decimal;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
code?: string;
|
||||||
|
|
||||||
|
@Column({ type: "timestamptz" })
|
||||||
|
startDate: Date;
|
||||||
|
|
||||||
|
@Column({ type: "timestamptz" })
|
||||||
|
endDate: Date;
|
||||||
|
|
||||||
|
@ManyToMany(() => DanakService, (danakService) => danakService.discounts, { nullable: true })
|
||||||
|
@JoinTable()
|
||||||
|
services?: DanakService[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export enum DiscountType {
|
||||||
|
DIRECT = "DIRECT",
|
||||||
|
CODE = "CODE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DiscountCalculationType {
|
||||||
|
FIXED = "FIXED",
|
||||||
|
PERCENTAGE = "PERCENTAGE",
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
|
||||||
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { CommonMessage, DiscountMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { DanakServicesService } from "../../danak-services/providers/danak-services.service";
|
||||||
|
import { CreateDiscountDto } from "../DTO/create-discount.dto";
|
||||||
|
import { DiscountRepository } from "../repositories/discount.repository";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DiscountService {
|
||||||
|
constructor(
|
||||||
|
private readonly discountRepository: DiscountRepository,
|
||||||
|
private readonly danakServicesService: DanakServicesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/******************************************** */
|
||||||
|
|
||||||
|
async create(createDiscountDto: CreateDiscountDto) {
|
||||||
|
const { services, ...discountData } = createDiscountDto;
|
||||||
|
|
||||||
|
const code = await this.generateUniqueCouponCode();
|
||||||
|
|
||||||
|
const discount = this.discountRepository.create({
|
||||||
|
...discountData,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (services && services.length) {
|
||||||
|
const foundServices = await this.danakServicesService.findServicesByIds(services);
|
||||||
|
discount.services = foundServices;
|
||||||
|
}
|
||||||
|
await this.discountRepository.save(discount);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: CommonMessage.CREATED,
|
||||||
|
discount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************** */
|
||||||
|
|
||||||
|
async findAll() {
|
||||||
|
const services = await this.discountRepository.find({ relations: ["services"] });
|
||||||
|
return { services };
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************** */
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const discount = await this.discountRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ["services"],
|
||||||
|
});
|
||||||
|
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||||
|
return { discount };
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************** */
|
||||||
|
|
||||||
|
async toggleActive(id: string) {
|
||||||
|
const discount = await this.discountRepository.findOne({ where: { id } });
|
||||||
|
if (!discount) {
|
||||||
|
throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
discount.isActive = !discount.isActive;
|
||||||
|
|
||||||
|
const updatedDiscount = await this.discountRepository.save(discount);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: CommonMessage.UPDATE_SUCCESS,
|
||||||
|
updatedDiscount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************** */
|
||||||
|
|
||||||
|
async generateUniqueCouponCode(): Promise<string> {
|
||||||
|
const code = this.generateCouponCode();
|
||||||
|
const exists = await this.discountRepository.findOne({ where: { code } });
|
||||||
|
if (exists) return this.generateUniqueCouponCode();
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************** */
|
||||||
|
|
||||||
|
private generateCouponCode(length = 8) {
|
||||||
|
return randomBytes(length).toString("hex").slice(0, length).toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
|
||||||
|
import { Discount } from "../entities/discount.entity";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DiscountRepository extends Repository<Discount> {
|
||||||
|
constructor(@InjectRepository(Discount) discountRepository: Repository<Discount>) {
|
||||||
|
super(discountRepository.target, discountRepository.manager, discountRepository.queryRunner);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user