chore: complete create discount

This commit is contained in:
Matin
2025-02-12 14:25:39 +03:30
parent 22198f3240
commit 95b9c38031
11 changed files with 343 additions and 2 deletions
@@ -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 { DanakServiceImage } from "./danak-service-image.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { Announcement } from "../../announcements/entities/announcement.entity";
import { Discount } from "../../discounts/entities/discount.entity";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { Ticket } from "../../tickets/entities/ticket.entity";
import { ServicesLanguage } from "../enums/services-language.enum";
@@ -63,6 +64,9 @@ export class DanakService extends BaseEntity {
@OneToMany(() => SubscriptionPlan, (plan) => plan.service, { cascade: true })
subscriptionPlans: SubscriptionPlan[];
@ManyToMany(() => Discount, (discount) => discount.services, { nullable: true })
discounts?: Discount[];
}
// @ManyToMany(() => User, (user) => user.danakServices)
@@ -1,5 +1,5 @@
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 { 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);
}
//************************************ */
}
+15
View File
@@ -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);
}
}