update: add target service to the announement get by id for admin
This commit is contained in:
@@ -516,6 +516,8 @@ export const enum DiscountMessage {
|
||||
MAX_USER_USAGE_REACHED = "شما قبلا از حداکثر تعداد مجاز این کد تخفیف استفاده کردهاید",
|
||||
NOT_APPLICABLE = "این کد تخفیف برای محصولات انتخاب شده معتبر نیست",
|
||||
VALUE_REQUIRED = "مقدار تخفیف الزامی است",
|
||||
IS_ACTIVE_REQUIRED = "وضعیت فعال بودن الزامی است",
|
||||
CODE_GENERATION_FAILED = "ایجاد کد تخفیف با خطا مواجه شد",
|
||||
}
|
||||
|
||||
export const enum EmailMessage {
|
||||
|
||||
@@ -13,7 +13,7 @@ export function databaseConfigs(): TypeOrmModuleAsyncOptions {
|
||||
username: configService.getOrThrow<string>("DB_USER"),
|
||||
password: configService.getOrThrow<string>("DB_PASS"),
|
||||
autoLoadEntities: true,
|
||||
synchronize: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : false,
|
||||
synchronize: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
|
||||
logging: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
|
||||
migrationsTableName: "typeorm_migrations",
|
||||
migrationsRun: false,
|
||||
|
||||
@@ -55,4 +55,11 @@ export class AnnouncementController {
|
||||
getAnnouncementById(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
return this.announcementService.getAnnouncementById(paramDto.id, userId);
|
||||
}
|
||||
|
||||
@ApiProperty({ description: "Get one announcements with id (admin route)" })
|
||||
@PermissionsDec(PermissionEnum.ANNOUNCEMENTS)
|
||||
@Get(":id/admin")
|
||||
getAnnouncementByIdAdmin(@Param() paramDto: ParamDto) {
|
||||
return this.announcementService.getAnnouncementByIdAdmin(paramDto.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,16 @@ export class AnnouncementService {
|
||||
return { announcement };
|
||||
}
|
||||
|
||||
/***************************************** */
|
||||
async getAnnouncementByIdAdmin(id: string) {
|
||||
const announcement = await this.announcementRepository.findOne({ where: { id }, relations: { targetService: true } });
|
||||
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||
//
|
||||
return {
|
||||
announcement,
|
||||
};
|
||||
}
|
||||
|
||||
/***************************************** */
|
||||
|
||||
async getAnnouncementByService(queryDto: SearchAnnouncementQueryDto, serviceId: string) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { DanakServiceReview } from "./danak-service-review.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { Ads } from "../../ads/entities/ads.entity";
|
||||
import { Announcement } from "../../announcements/entities/announcement.entity";
|
||||
import { DiscountService } from "../../discounts/entities/discount-product.entity";
|
||||
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||
import { Ticket } from "../../tickets/entities/ticket.entity";
|
||||
import { ServicesLanguage } from "../enums/services-language.enum";
|
||||
@@ -77,9 +76,6 @@ export class DanakService extends BaseEntity {
|
||||
@OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.service)
|
||||
reviews: DanakServiceReview[];
|
||||
|
||||
@OneToMany(() => DiscountService, (discountService) => discountService.danakService)
|
||||
discountServices: DiscountService[];
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsBoolean, IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, Min, ValidateIf } from "class-validator";
|
||||
import { IsArray, IsBoolean, IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateIf } from "class-validator";
|
||||
|
||||
import { DiscountMessage } from "../../../common/enums/message.enum";
|
||||
import { DiscountType } from "../enums/discount-type.enum";
|
||||
@@ -39,25 +38,10 @@ export class CreateDiscountDto {
|
||||
@ApiProperty({ description: "The end date of the discount", example: "2023-06-30T23:59:59Z" })
|
||||
endDate: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: DiscountMessage.IS_ACTIVE_REQUIRED })
|
||||
@IsBoolean({ message: DiscountMessage.IS_ACTIVE_MUST_BE_A_BOOLEAN })
|
||||
@ApiProperty({ description: "Indicates if the discount is active", example: true })
|
||||
isActive?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber({}, { message: "Max usage must be a number" })
|
||||
@Min(0, { message: "Max usage must be at least 0" })
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ description: "Maximum number of times this discount can be used (null for unlimited)", example: 1000, required: false })
|
||||
maxUsage?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber({}, { message: "Max usage per user must be a number" })
|
||||
@Min(0, { message: "Max usage per user must be at least 0" })
|
||||
@Type(() => Number)
|
||||
@ValidateIf((o) => !o.direct)
|
||||
@ApiPropertyOptional({ description: "Maximum number of times a single user can use this discount (null for unlimited)", example: 1 })
|
||||
maxUsagePerUser?: number;
|
||||
isActive: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: DiscountMessage.TARGET_PRODUCTS_MUST_BE_AN_ARRAY })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Post } from "@nestjs/common";
|
||||
import { Body, Controller, Post } from "@nestjs/common";
|
||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { CreateDiscountDto } from "./DTO/create-discount.dto";
|
||||
@@ -22,12 +22,4 @@ export class DiscountsController {
|
||||
create(@Body() createDiscountDto: CreateDiscountDto) {
|
||||
return this.discountsService.create(createDiscountDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get discount by ID" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.DISCOUNTS)
|
||||
@Get(":id")
|
||||
findOne(@Param("id") id: string) {
|
||||
return this.discountsService.findById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,17 @@ import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { DiscountsController } from "./discounts.controller";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { DiscountService } from "./entities/discount-product.entity";
|
||||
import { DirectDiscount } from "./entities/direct-discount.entity";
|
||||
import { Discount } from "./entities/discount.entity";
|
||||
import { UsageDiscount } from "./entities/usage-discount.entity";
|
||||
import { DiscountsService } from "./providers/discounts.service";
|
||||
import { DiscountServicesRepository } from "./repositories/discount-services.repository";
|
||||
import { DiscountRepository } from "./repositories/discounts.repository";
|
||||
import { UsageDiscountRepository } from "./repositories/usage-discounts.repository";
|
||||
import { DiscountValidator } from "./utils/discount-validator";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Discount, UsageDiscount, DiscountService]), UsersModule],
|
||||
imports: [TypeOrmModule.forFeature([Discount, UsageDiscount, DirectDiscount]), UsersModule],
|
||||
controllers: [DiscountsController],
|
||||
providers: [DiscountsService, DiscountRepository, UsageDiscountRepository, DiscountServicesRepository, DiscountValidator],
|
||||
exports: [DiscountsService, DiscountValidator],
|
||||
providers: [DiscountsService, DiscountRepository, UsageDiscountRepository],
|
||||
exports: [DiscountsService],
|
||||
})
|
||||
export class DiscountsModule {}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm";
|
||||
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
||||
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||
import { DiscountType } from "../enums/discount-type.enum";
|
||||
@Entity()
|
||||
export class DirectDiscount extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 150 })
|
||||
title: string;
|
||||
|
||||
@Column({ type: "enum", enum: DiscountType, default: DiscountType.PERCENTAGE })
|
||||
type: DiscountType;
|
||||
|
||||
@Column({ type: "decimal", precision: 16, scale: 2, transformer: new DecimalTransformer() })
|
||||
value: number;
|
||||
|
||||
@Column({ type: "timestamptz" })
|
||||
startDate: Date;
|
||||
|
||||
@Column({ type: "timestamptz" })
|
||||
endDate: Date;
|
||||
|
||||
@Column({ type: "boolean", default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@DeleteDateColumn({ nullable: true })
|
||||
deletedAt?: Date;
|
||||
|
||||
@OneToMany(() => SubscriptionPlan, (subscriptionPlan) => subscriptionPlan.directDiscount)
|
||||
subscriptionPlans: SubscriptionPlan[];
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Entity, ManyToOne } from "typeorm";
|
||||
|
||||
import { Discount } from "./discount.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { DanakService } from "../../danak-services/entities/danak-service.entity";
|
||||
|
||||
@Entity()
|
||||
export class DiscountService extends BaseEntity {
|
||||
@ManyToOne(() => Discount, (discount) => discount.discountServices, { nullable: false, onDelete: "CASCADE" })
|
||||
discount: Discount;
|
||||
|
||||
@ManyToOne(() => DanakService, (danakService) => danakService.discountServices, { nullable: false })
|
||||
danakService: DanakService;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Column, Entity, OneToMany } from "typeorm";
|
||||
import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm";
|
||||
|
||||
import { DiscountService } from "./discount-product.entity";
|
||||
import { UsageDiscount } from "./usage-discount.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
||||
@@ -32,18 +31,9 @@ export class Discount extends BaseEntity {
|
||||
@Column({ type: "boolean", default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@Column({ type: "boolean", default: false })
|
||||
direct: boolean;
|
||||
|
||||
@Column({ type: "int", nullable: true })
|
||||
maxUsage: number;
|
||||
|
||||
@Column({ type: "int", nullable: true })
|
||||
maxUsagePerUser: number;
|
||||
|
||||
@OneToMany(() => DiscountService, (discountService) => discountService.discount, { cascade: true })
|
||||
discountServices: DiscountService[];
|
||||
|
||||
@OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount)
|
||||
usages: UsageDiscount[];
|
||||
|
||||
@DeleteDateColumn({ nullable: true })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
|
||||
@@ -1,46 +1,33 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import dayjs from "dayjs";
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import Decimal from "decimal.js";
|
||||
import { DataSource, QueryRunner } from "typeorm";
|
||||
|
||||
import { DiscountMessage, InvoiceMessage } from "../../../common/enums/message.enum";
|
||||
import { Invoice } from "../../invoices/entities/invoice.entity";
|
||||
import { InvoiceStatus } from "../../invoices/enums/invoice-status.enum";
|
||||
import { DiscountMessage } from "../../../common/enums/message.enum";
|
||||
import { CreateDiscountDto } from "../DTO/create-discount.dto";
|
||||
import { Discount } from "../entities/discount.entity";
|
||||
import { UsageDiscount } from "../entities/usage-discount.entity";
|
||||
import { DiscountType } from "../enums/discount-type.enum";
|
||||
// import { DiscountServicesRepository } from "../repositories/discount-services.repository";
|
||||
import { DiscountRepository } from "../repositories/discounts.repository";
|
||||
import { UsageDiscountRepository } from "../repositories/usage-discounts.repository";
|
||||
import { DiscountValidator } from "../utils/discount-validator";
|
||||
|
||||
@Injectable()
|
||||
export class DiscountsService {
|
||||
private MAX_ATTEMPTS = 10;
|
||||
|
||||
constructor(
|
||||
private readonly discountRepository: DiscountRepository,
|
||||
// private readonly discountServicesRepository: DiscountServicesRepository,
|
||||
private readonly usageDiscountRepository: UsageDiscountRepository,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly discountValidator: DiscountValidator,
|
||||
) {}
|
||||
|
||||
async create(createDiscountDto: CreateDiscountDto) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
const { direct } = createDiscountDto;
|
||||
|
||||
try {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const discount = queryRunner.manager.create(this.discountRepository.target, createDiscountDto);
|
||||
|
||||
if (!direct) {
|
||||
const code = await this.generateDiscountCode();
|
||||
discount.code = code;
|
||||
if (!createDiscountDto.direct) {
|
||||
await this.createCodeBasedDiscount(createDiscountDto, queryRunner);
|
||||
} else {
|
||||
await this.createDirectBasedDiscount(createDiscountDto, queryRunner);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
@@ -56,271 +43,52 @@ export class DiscountsService {
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
const discount = await this.discountRepository.findById(id);
|
||||
private async createCodeBasedDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) {
|
||||
const discount = queryRunner.manager.create(this.discountRepository.target, createDiscountDto);
|
||||
|
||||
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||
let attempts = 0;
|
||||
let code: string;
|
||||
let existCode;
|
||||
|
||||
return { discount };
|
||||
}
|
||||
do {
|
||||
if (attempts >= this.MAX_ATTEMPTS) throw new BadRequestException(DiscountMessage.CODE_GENERATION_FAILED);
|
||||
|
||||
async validateDiscountCode(code: string, userId: string, invoice: Invoice, queryRunner: QueryRunner) {
|
||||
// Find the discount by code
|
||||
const discount = await queryRunner.manager.findOne(Discount, {
|
||||
where: {
|
||||
code,
|
||||
isActive: true,
|
||||
direct: false,
|
||||
},
|
||||
relations: ["discountServices"],
|
||||
});
|
||||
|
||||
if (!discount) {
|
||||
throw new BadRequestException(DiscountMessage.INVALID_CODE);
|
||||
}
|
||||
|
||||
// Check if the discount is currently active (time period)
|
||||
const now = dayjs();
|
||||
if (now.isBefore(dayjs(discount.startDate)) || now.isAfter(dayjs(discount.endDate))) {
|
||||
throw new BadRequestException(DiscountMessage.EXPIRED);
|
||||
}
|
||||
|
||||
// Check if the discount has reached its maximum usage
|
||||
if (discount.maxUsage !== null && discount.usedCount >= discount.maxUsage) {
|
||||
throw new BadRequestException(DiscountMessage.MAX_USAGE_REACHED);
|
||||
}
|
||||
|
||||
// Check if the user has reached their maximum usage for this discount
|
||||
if (discount.maxUsagePerUser !== null) {
|
||||
const userUsageCount = await this.usageDiscountRepository.countByUserAndDiscount(userId, discount.id);
|
||||
if (userUsageCount >= discount.maxUsagePerUser) {
|
||||
throw new BadRequestException(DiscountMessage.MAX_USER_USAGE_REACHED);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the discount applies to the items in the invoice
|
||||
if (!this.discountValidator.validateItemsForDiscount(invoice, discount)) {
|
||||
throw new BadRequestException(DiscountMessage.NOT_APPLICABLE);
|
||||
}
|
||||
code = await this.generateDiscountCode();
|
||||
existCode = await this.findDiscountByCode(code, queryRunner);
|
||||
attempts++;
|
||||
} while (existCode && attempts < this.MAX_ATTEMPTS);
|
||||
|
||||
discount.code = code;
|
||||
discount.value = this.calculateDiscountValue(createDiscountDto);
|
||||
await queryRunner.manager.save(discount);
|
||||
return discount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a discount code to an invoice
|
||||
* @param invoiceId The invoice ID
|
||||
* @param code The discount code
|
||||
* @param userId The user ID
|
||||
* @returns The updated invoice
|
||||
*/
|
||||
async applyDiscountToInvoice(invoiceId: string, code: string, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// Get the invoice
|
||||
const invoice = await queryRunner.manager.findOne(Invoice, {
|
||||
where: {
|
||||
id: invoiceId,
|
||||
user: { id: userId },
|
||||
status: InvoiceStatus.PENDING,
|
||||
},
|
||||
relations: ["items", "items.subscriptionPlan", "items.subscriptionPlan.plan", "items.subscriptionPlan.plan.service"],
|
||||
});
|
||||
|
||||
if (!invoice) {
|
||||
throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
||||
//**************************** */
|
||||
private async createDirectBasedDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) {
|
||||
const discount = queryRunner.manager.create(this.discountRepository.target, createDiscountDto);
|
||||
discount.value = this.calculateDiscountValue(createDiscountDto);
|
||||
await queryRunner.manager.save(discount);
|
||||
return discount;
|
||||
}
|
||||
//**************************** */
|
||||
|
||||
// Check if the invoice already has a discount
|
||||
if (invoice.discount) {
|
||||
throw new BadRequestException(InvoiceMessage.ALREADY_HAS_DISCOUNT);
|
||||
private async findDiscountByCode(code: string, queryRunner: QueryRunner) {
|
||||
const discount = await queryRunner.manager.findOne(this.discountRepository.target, { where: { code } });
|
||||
return discount;
|
||||
}
|
||||
//**************************** */
|
||||
|
||||
// Validate the discount code
|
||||
const discount = await this.validateDiscountCode(code, userId, invoice, queryRunner);
|
||||
|
||||
// Calculate the discount amount
|
||||
const discountAmount = this.discountValidator.calculateDiscountAmount(invoice, discount);
|
||||
|
||||
// Calculate the new total price
|
||||
const newTotalPrice = this.discountValidator.calculateDiscountedTotalPrice(invoice, discountAmount);
|
||||
|
||||
// Update the invoice
|
||||
invoice.discount = discount;
|
||||
invoice.totalPrice = new Decimal(newTotalPrice);
|
||||
|
||||
await queryRunner.manager.save(Invoice, invoice);
|
||||
|
||||
// Record the discount usage
|
||||
const usageDiscount = queryRunner.manager.create(UsageDiscount, {
|
||||
discount,
|
||||
user: { id: userId },
|
||||
appliedAmount: discountAmount,
|
||||
usedAt: new Date(),
|
||||
});
|
||||
|
||||
await queryRunner.manager.save(UsageDiscount, usageDiscount);
|
||||
|
||||
// Increment the discount usage count
|
||||
discount.usedCount += 1;
|
||||
await queryRunner.manager.save(Discount, discount);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
message: InvoiceMessage.DISCOUNT_APPLIED,
|
||||
invoice,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a discount applied to an invoice
|
||||
* @param invoiceId The invoice ID
|
||||
* @param userId The user ID
|
||||
* @returns The updated invoice
|
||||
*/
|
||||
async cancelInvoiceDiscount(invoiceId: string, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// Get the invoice
|
||||
const invoice = await queryRunner.manager.findOne(Invoice, {
|
||||
where: {
|
||||
id: invoiceId,
|
||||
user: { id: userId },
|
||||
status: InvoiceStatus.PENDING,
|
||||
},
|
||||
relations: ["discount", "items"],
|
||||
});
|
||||
|
||||
if (!invoice) {
|
||||
throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
||||
}
|
||||
|
||||
// Check if the invoice has a discount
|
||||
if (!invoice.discount) {
|
||||
throw new BadRequestException(InvoiceMessage.NO_DISCOUNT);
|
||||
}
|
||||
|
||||
// Find the usage record
|
||||
const usageDiscount = await this.usageDiscountRepository.findByUserAndDiscount(userId, invoice.discount.id);
|
||||
|
||||
if (usageDiscount) {
|
||||
// Delete the usage record
|
||||
await queryRunner.manager.remove(usageDiscount);
|
||||
|
||||
// Decrement the discount usage count
|
||||
invoice.discount.usedCount -= 1;
|
||||
await queryRunner.manager.save(Discount, invoice.discount);
|
||||
}
|
||||
|
||||
// Recalculate the total price without the discount
|
||||
// This requires recalculating based on the original item prices
|
||||
const originalTotalPrice = invoice.items.reduce((sum, item) => {
|
||||
const itemTotal = new Decimal(item.unitPrice)
|
||||
.mul(item.count)
|
||||
.minus(new Decimal(item.unitPrice).mul(item.count).mul(item.discount).div(100));
|
||||
return sum.add(itemTotal);
|
||||
}, new Decimal(0));
|
||||
|
||||
// Add tax to the total
|
||||
const totalWithTax = originalTotalPrice.add(invoice.tax || 0);
|
||||
|
||||
// Remove the discount from the invoice
|
||||
// invoice.discount = null;
|
||||
invoice.totalPrice = new Decimal(totalWithTax);
|
||||
|
||||
await queryRunner.manager.save(Invoice, invoice);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
message: InvoiceMessage.DISCOUNT_CANCELED,
|
||||
invoice,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a direct discount to a service price
|
||||
* When calculating service prices, this discount should be considered
|
||||
* @param serviceId The service ID
|
||||
* @returns The discount information for the service
|
||||
*/
|
||||
async getDirectDiscountForService(serviceId: string) {
|
||||
// Find active direct discounts for this service
|
||||
const discounts = await this.discountRepository
|
||||
.createQueryBuilder("discount")
|
||||
.innerJoin("discount.discountServices", "ds")
|
||||
.where("discount.isActive = :isActive", { isActive: true })
|
||||
.andWhere("discount.direct = :direct", { direct: true })
|
||||
.andWhere("discount.startDate <= :now", { now: new Date() })
|
||||
.andWhere("discount.endDate >= :now", { now: new Date() })
|
||||
.andWhere("ds.danakService.id = :serviceId", { serviceId })
|
||||
.getMany();
|
||||
|
||||
if (!discounts || discounts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For simplicity, we're returning the first valid discount
|
||||
// In a more complex system, you might want to apply the best discount
|
||||
return discounts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the discounted price for a service
|
||||
* @param serviceId The service ID
|
||||
* @param originalPrice The original price
|
||||
* @returns The discounted price and discount info
|
||||
*/
|
||||
async calculateDiscountedPrice(serviceId: string, originalPrice: number) {
|
||||
const discount = await this.getDirectDiscountForService(serviceId);
|
||||
|
||||
if (!discount) {
|
||||
return {
|
||||
originalPrice,
|
||||
discountedPrice: originalPrice,
|
||||
discount: null,
|
||||
};
|
||||
}
|
||||
|
||||
let discountedPrice: number;
|
||||
|
||||
if (discount.type === DiscountType.PERCENTAGE) {
|
||||
discountedPrice = new Decimal(originalPrice)
|
||||
.mul(100 - discount.value)
|
||||
.div(100)
|
||||
.toNumber();
|
||||
} else {
|
||||
discountedPrice = Math.max(0, originalPrice - discount.value);
|
||||
}
|
||||
|
||||
return {
|
||||
originalPrice,
|
||||
discountedPrice,
|
||||
discount,
|
||||
};
|
||||
}
|
||||
|
||||
//*************************** */
|
||||
private async generateDiscountCode() {
|
||||
const code = randomBytes(8).toString("hex");
|
||||
private async generateDiscountCode(length: number = 8) {
|
||||
const code = randomBytes(length).toString("hex");
|
||||
return code;
|
||||
}
|
||||
//**************************** */
|
||||
|
||||
private calculateDiscountValue(createDiscountDto: CreateDiscountDto): number {
|
||||
if (createDiscountDto.type === DiscountType.PERCENTAGE) return createDiscountDto.value;
|
||||
if (createDiscountDto.type === DiscountType.FIXED_AMOUNT) return createDiscountDto.value;
|
||||
|
||||
throw new BadRequestException(DiscountMessage.INVALID_DISCOUNT_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { DiscountService } from "../entities/discount-product.entity";
|
||||
@Injectable()
|
||||
export class DiscountServicesRepository extends Repository<DiscountService> {
|
||||
constructor(@InjectRepository(DiscountService) repository: Repository<DiscountService>) {
|
||||
super(repository.target, repository.manager, repository.queryRunner);
|
||||
}
|
||||
|
||||
async createMany(discountId: string, productIds: string[]): Promise<DiscountService[]> {
|
||||
const discountServices = productIds.map((productId) =>
|
||||
this.create({
|
||||
discount: { id: discountId },
|
||||
danakService: { id: productId },
|
||||
}),
|
||||
);
|
||||
return this.save(discountServices);
|
||||
}
|
||||
|
||||
async findByDiscountId(discountId: string): Promise<DiscountService[]> {
|
||||
return this.find({
|
||||
where: { discount: { id: discountId } },
|
||||
});
|
||||
}
|
||||
|
||||
async findByProductId(productId: string): Promise<DiscountService[]> {
|
||||
return this.find({
|
||||
where: { danakService: { id: productId } },
|
||||
relations: ["discount"],
|
||||
});
|
||||
}
|
||||
|
||||
async deleteByDiscountId(discountId: string): Promise<void> {
|
||||
await this.createQueryBuilder().delete().where("discountId = :discountId", { discountId }).execute();
|
||||
}
|
||||
|
||||
async hasProductForDiscount(discountId: string, productId: string): Promise<boolean> {
|
||||
const count = await this.count({
|
||||
where: {
|
||||
discount: { id: discountId },
|
||||
danakService: { id: productId },
|
||||
},
|
||||
});
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
async findDiscountIdsForProduct(productId: string): Promise<string[]> {
|
||||
const discountServices = await this.find({
|
||||
where: { danakService: { id: productId } },
|
||||
select: ["discount"],
|
||||
relations: ["discount"],
|
||||
});
|
||||
return discountServices.map((dp) => dp.discount.id);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import Decimal from "decimal.js";
|
||||
|
||||
import { InvoiceItem } from "../../invoices/entities/invoice-item.entity";
|
||||
import { Invoice } from "../../invoices/entities/invoice.entity";
|
||||
import { Discount } from "../entities/discount.entity";
|
||||
import { DiscountType } from "../enums/discount-type.enum";
|
||||
|
||||
@Injectable()
|
||||
export class DiscountValidator {
|
||||
/**
|
||||
* Validates if the items in an invoice match with the services targeted by the discount
|
||||
* @param invoice The invoice to check
|
||||
* @param discount The discount to apply
|
||||
* @returns true if valid, false otherwise
|
||||
*/
|
||||
public validateItemsForDiscount(invoice: Invoice, discount: Discount): boolean {
|
||||
// If discount has no target services, it applies to all
|
||||
if (!discount.discountServices || discount.discountServices.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any item in the invoice matches the targeted services
|
||||
const targetServiceIds = discount.discountServices.map((ds) => ds.danakService.id);
|
||||
|
||||
// This implementation assumes there's a way to map from invoice items to services
|
||||
// You'll need to adapt this based on your data model
|
||||
const hasMatchingItem = invoice.items.some((item) => {
|
||||
// For subscription-based items
|
||||
if (item.subscriptionPlan && item.subscriptionPlan.plan && item.subscriptionPlan.plan.service) {
|
||||
return targetServiceIds.includes(item.subscriptionPlan.plan.service.id);
|
||||
}
|
||||
|
||||
// For other types of items - implement according to your data model
|
||||
// This is a placeholder - you need to adapt this to your specific implementation
|
||||
return false;
|
||||
});
|
||||
|
||||
return hasMatchingItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the discount amount to apply to the invoice
|
||||
* @param invoice The invoice to apply the discount to
|
||||
* @param discount The discount to apply
|
||||
* @returns The discount amount
|
||||
*/
|
||||
public calculateDiscountAmount(invoice: Invoice, discount: Discount): Decimal {
|
||||
if (discount.type === DiscountType.PERCENTAGE) {
|
||||
return new Decimal(invoice.totalPrice).mul(discount.value).div(100);
|
||||
} else {
|
||||
// For fixed amount, cap at the invoice total
|
||||
return Decimal.min(new Decimal(discount.value), new Decimal(invoice.totalPrice));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the discounted price for an item
|
||||
* @param item The item to calculate discount for
|
||||
* @param discount The discount to apply
|
||||
* @returns The discounted price
|
||||
*/
|
||||
public calculateItemDiscountedPrice(item: InvoiceItem, discount: Discount): Decimal {
|
||||
const originalPrice = new Decimal(item.unitPrice).mul(item.count);
|
||||
|
||||
if (discount.type === DiscountType.PERCENTAGE) {
|
||||
return originalPrice.mul(new Decimal(100).minus(discount.value)).div(100);
|
||||
} else {
|
||||
// For fixed amount discounts, proportionally distribute the discount among items
|
||||
// This is a simple implementation - you might want a more complex distribution logic
|
||||
const discountAmount = Decimal.min(new Decimal(discount.value), originalPrice);
|
||||
return originalPrice.minus(discountAmount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the updated total price after applying a discount
|
||||
* @param invoice The invoice to calculate for
|
||||
* @param discountAmount The discount amount to apply
|
||||
* @returns The new total price as a number
|
||||
*/
|
||||
public calculateDiscountedTotalPrice(invoice: Invoice, discountAmount: Decimal): number {
|
||||
return new Decimal(invoice.totalPrice).minus(discountAmount).toNumber();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query } from "@nestjs/common";
|
||||
import { ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { ApplyDiscountDto } from "./DTO/apply-discount.dto";
|
||||
import { CreateInvoiceDto } from "./DTO/create-invoice.dto";
|
||||
import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "./DTO/invoices-search-query.dto";
|
||||
import { UpdateInvoiceDto } from "./DTO/update-invoice.dto";
|
||||
@@ -12,16 +11,12 @@ import { PermissionsDec } from "../../common/decorators/permission.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
import { VerifyOtpWithUserId } from "../auth/DTO/verify-otp.dto";
|
||||
import { DiscountsService } from "../discounts/providers/discounts.service";
|
||||
import { PermissionEnum } from "../users/enums/permission.enum";
|
||||
|
||||
@Controller("invoices")
|
||||
@AuthGuards()
|
||||
export class InvoicesController {
|
||||
constructor(
|
||||
private readonly invoiceService: InvoicesService,
|
||||
private readonly discountsService: DiscountsService,
|
||||
) {}
|
||||
constructor(private readonly invoiceService: InvoicesService) {}
|
||||
|
||||
@ApiOperation({ summary: "create an invoice ==> admin route" })
|
||||
@PermissionsDec(PermissionEnum.INVOICES)
|
||||
@@ -76,15 +71,15 @@ export class InvoicesController {
|
||||
return this.invoiceService.payInvoice(paramDto.id, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "apply discount on invoice by user" })
|
||||
@Post(":id/apply-discount")
|
||||
async applyDiscount(@Param() paramDto: ParamDto, @Body() applyDiscountDto: ApplyDiscountDto, @UserDec("id") userId: string) {
|
||||
return this.discountsService.applyDiscountToInvoice(paramDto.id, applyDiscountDto.code, userId);
|
||||
}
|
||||
// @ApiOperation({ summary: "apply discount on invoice by user" })
|
||||
// @Post(":id/apply-discount")
|
||||
// async applyDiscount(@Param() paramDto: ParamDto, @Body() applyDiscountDto: ApplyDiscountDto, @UserDec("id") userId: string) {
|
||||
// return this.discountsService.applyDiscountToInvoice(paramDto.id, applyDiscountDto.code, userId);
|
||||
// }
|
||||
|
||||
@ApiOperation({ summary: "cancel discount by user" })
|
||||
@Post(":id/cancel-discount")
|
||||
cancelDiscount(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
return this.discountsService.cancelInvoiceDiscount(paramDto.id, userId);
|
||||
}
|
||||
// @ApiOperation({ summary: "cancel discount by user" })
|
||||
// @Post(":id/cancel-discount")
|
||||
// cancelDiscount(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
// return this.discountsService.cancelInvoiceDiscount(paramDto.id, userId);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ReferralRewardsRepository } from "../repositories/referral-rewards.repo
|
||||
import { ReferralsRepository } from "../repositories/referrals.repository";
|
||||
@Injectable()
|
||||
export class ReferralsService {
|
||||
private MAX_ATTEMPTS = 10;
|
||||
constructor(
|
||||
private readonly referralsRepository: ReferralsRepository,
|
||||
private readonly referralCodesRepository: ReferralCodesRepository,
|
||||
@@ -25,13 +26,12 @@ export class ReferralsService {
|
||||
|
||||
if (existReferralCode) throw new BadRequestException(ReferralMessage.CODE_EXIST_FOR_USER);
|
||||
|
||||
const MAX_ATTEMPTS = 10;
|
||||
let attempts = 0;
|
||||
let code: string;
|
||||
let existCode;
|
||||
|
||||
do {
|
||||
if (attempts >= MAX_ATTEMPTS) {
|
||||
if (attempts >= this.MAX_ATTEMPTS) {
|
||||
throw new BadRequestException(ReferralMessage.CODE_GENERATION_FAILED);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { UserSubscription } from "./user-subscription.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
||||
import { DanakService } from "../../danak-services/entities/danak-service.entity";
|
||||
// import { Discount } from "../../discounts/entities/discount.entity";
|
||||
import { DirectDiscount } from "../../discounts/entities/direct-discount.entity";
|
||||
|
||||
@Entity()
|
||||
@Index(["service", "name"], { unique: true })
|
||||
@@ -19,7 +19,7 @@ export class SubscriptionPlan extends BaseEntity {
|
||||
|
||||
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
|
||||
price: Decimal;
|
||||
//
|
||||
|
||||
@Column({ type: "boolean", default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@@ -29,6 +29,6 @@ export class SubscriptionPlan extends BaseEntity {
|
||||
@OneToMany(() => UserSubscription, (userSubscription) => userSubscription.plan)
|
||||
userSubscriptions: UserSubscription[];
|
||||
|
||||
// @ManyToMany(() => Discount, (discount) => discount.subscriptionPlans)
|
||||
// discounts: Discount[];
|
||||
@ManyToOne(() => DirectDiscount, (directDiscount) => directDiscount.subscriptionPlans, { nullable: true })
|
||||
directDiscount?: DirectDiscount;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user