chore: complete create discount
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
//************************************ */
|
||||
}
|
||||
@@ -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