update: add target service to the announement get by id for admin

This commit is contained in:
mahyargdz
2025-04-17 10:49:57 +03:30
parent f9cb3cf653
commit cd77db93d0
17 changed files with 123 additions and 506 deletions
+2
View File
@@ -516,6 +516,8 @@ export const enum DiscountMessage {
MAX_USER_USAGE_REACHED = "شما قبلا از حداکثر تعداد مجاز این کد تخفیف استفاده کرده‌اید", MAX_USER_USAGE_REACHED = "شما قبلا از حداکثر تعداد مجاز این کد تخفیف استفاده کرده‌اید",
NOT_APPLICABLE = "این کد تخفیف برای محصولات انتخاب شده معتبر نیست", NOT_APPLICABLE = "این کد تخفیف برای محصولات انتخاب شده معتبر نیست",
VALUE_REQUIRED = "مقدار تخفیف الزامی است", VALUE_REQUIRED = "مقدار تخفیف الزامی است",
IS_ACTIVE_REQUIRED = "وضعیت فعال بودن الزامی است",
CODE_GENERATION_FAILED = "ایجاد کد تخفیف با خطا مواجه شد",
} }
export const enum EmailMessage { export const enum EmailMessage {
+1 -1
View File
@@ -13,7 +13,7 @@ export function databaseConfigs(): TypeOrmModuleAsyncOptions {
username: configService.getOrThrow<string>("DB_USER"), username: configService.getOrThrow<string>("DB_USER"),
password: configService.getOrThrow<string>("DB_PASS"), password: configService.getOrThrow<string>("DB_PASS"),
autoLoadEntities: true, 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, logging: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
migrationsTableName: "typeorm_migrations", migrationsTableName: "typeorm_migrations",
migrationsRun: false, migrationsRun: false,
@@ -55,4 +55,11 @@ export class AnnouncementController {
getAnnouncementById(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { getAnnouncementById(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
return this.announcementService.getAnnouncementById(paramDto.id, userId); 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 }; 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) { 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 { BaseEntity } from "../../../common/entities/base.entity";
import { Ads } from "../../ads/entities/ads.entity"; import { Ads } from "../../ads/entities/ads.entity";
import { Announcement } from "../../announcements/entities/announcement.entity"; import { Announcement } from "../../announcements/entities/announcement.entity";
import { DiscountService } from "../../discounts/entities/discount-product.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";
@@ -77,9 +76,6 @@ export class DanakService extends BaseEntity {
@OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.service) @OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.service)
reviews: DanakServiceReview[]; reviews: DanakServiceReview[];
@OneToMany(() => DiscountService, (discountService) => discountService.danakService)
discountServices: DiscountService[];
@DeleteDateColumn() @DeleteDateColumn()
deletedAt?: Date; deletedAt?: Date;
} }
@@ -1,6 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer"; import { IsArray, IsBoolean, IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateIf } from "class-validator";
import { IsArray, IsBoolean, IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, Min, ValidateIf } from "class-validator";
import { DiscountMessage } from "../../../common/enums/message.enum"; import { DiscountMessage } from "../../../common/enums/message.enum";
import { DiscountType } from "../enums/discount-type.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" }) @ApiProperty({ description: "The end date of the discount", example: "2023-06-30T23:59:59Z" })
endDate: string; endDate: string;
@IsOptional() @IsNotEmpty({ message: DiscountMessage.IS_ACTIVE_REQUIRED })
@IsBoolean({ message: DiscountMessage.IS_ACTIVE_MUST_BE_A_BOOLEAN }) @IsBoolean({ message: DiscountMessage.IS_ACTIVE_MUST_BE_A_BOOLEAN })
@ApiProperty({ description: "Indicates if the discount is active", example: true }) @ApiProperty({ description: "Indicates if the discount is active", example: true })
isActive?: boolean; 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;
@IsOptional() @IsOptional()
@IsNotEmpty({ message: DiscountMessage.TARGET_PRODUCTS_MUST_BE_AN_ARRAY }) @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 { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { CreateDiscountDto } from "./DTO/create-discount.dto"; import { CreateDiscountDto } from "./DTO/create-discount.dto";
@@ -22,12 +22,4 @@ export class DiscountsController {
create(@Body() createDiscountDto: CreateDiscountDto) { create(@Body() createDiscountDto: CreateDiscountDto) {
return this.discountsService.create(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);
}
} }
+4 -6
View File
@@ -3,19 +3,17 @@ import { TypeOrmModule } from "@nestjs/typeorm";
import { DiscountsController } from "./discounts.controller"; import { DiscountsController } from "./discounts.controller";
import { UsersModule } from "../users/users.module"; 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 { Discount } from "./entities/discount.entity";
import { UsageDiscount } from "./entities/usage-discount.entity"; import { UsageDiscount } from "./entities/usage-discount.entity";
import { DiscountsService } from "./providers/discounts.service"; import { DiscountsService } from "./providers/discounts.service";
import { DiscountServicesRepository } from "./repositories/discount-services.repository";
import { DiscountRepository } from "./repositories/discounts.repository"; import { DiscountRepository } from "./repositories/discounts.repository";
import { UsageDiscountRepository } from "./repositories/usage-discounts.repository"; import { UsageDiscountRepository } from "./repositories/usage-discounts.repository";
import { DiscountValidator } from "./utils/discount-validator";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Discount, UsageDiscount, DiscountService]), UsersModule], imports: [TypeOrmModule.forFeature([Discount, UsageDiscount, DirectDiscount]), UsersModule],
controllers: [DiscountsController], controllers: [DiscountsController],
providers: [DiscountsService, DiscountRepository, UsageDiscountRepository, DiscountServicesRepository, DiscountValidator], providers: [DiscountsService, DiscountRepository, UsageDiscountRepository],
exports: [DiscountsService, DiscountValidator], exports: [DiscountsService],
}) })
export class DiscountsModule {} 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 { UsageDiscount } from "./usage-discount.entity";
import { BaseEntity } from "../../../common/entities/base.entity"; import { BaseEntity } from "../../../common/entities/base.entity";
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
@@ -32,18 +31,9 @@ export class Discount extends BaseEntity {
@Column({ type: "boolean", default: true }) @Column({ type: "boolean", default: true })
isActive: boolean; 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) @OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount)
usages: UsageDiscount[]; usages: UsageDiscount[];
@DeleteDateColumn({ nullable: true })
deletedAt?: Date;
} }
@@ -1,46 +1,33 @@
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { BadRequestException, Injectable } from "@nestjs/common"; 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 { DataSource, QueryRunner } from "typeorm";
import { DiscountMessage, InvoiceMessage } from "../../../common/enums/message.enum"; import { DiscountMessage } from "../../../common/enums/message.enum";
import { Invoice } from "../../invoices/entities/invoice.entity";
import { InvoiceStatus } from "../../invoices/enums/invoice-status.enum";
import { CreateDiscountDto } from "../DTO/create-discount.dto"; 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 { DiscountType } from "../enums/discount-type.enum";
// import { DiscountServicesRepository } from "../repositories/discount-services.repository";
import { DiscountRepository } from "../repositories/discounts.repository"; import { DiscountRepository } from "../repositories/discounts.repository";
import { UsageDiscountRepository } from "../repositories/usage-discounts.repository";
import { DiscountValidator } from "../utils/discount-validator";
@Injectable() @Injectable()
export class DiscountsService { export class DiscountsService {
private MAX_ATTEMPTS = 10;
constructor( constructor(
private readonly discountRepository: DiscountRepository, private readonly discountRepository: DiscountRepository,
// private readonly discountServicesRepository: DiscountServicesRepository,
private readonly usageDiscountRepository: UsageDiscountRepository,
private readonly dataSource: DataSource, private readonly dataSource: DataSource,
private readonly discountValidator: DiscountValidator,
) {} ) {}
async create(createDiscountDto: CreateDiscountDto) { async create(createDiscountDto: CreateDiscountDto) {
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
const { direct } = createDiscountDto;
try { try {
await queryRunner.connect(); await queryRunner.connect();
await queryRunner.startTransaction(); await queryRunner.startTransaction();
const discount = queryRunner.manager.create(this.discountRepository.target, createDiscountDto); if (!createDiscountDto.direct) {
await this.createCodeBasedDiscount(createDiscountDto, queryRunner);
if (!direct) { } else {
const code = await this.generateDiscountCode(); await this.createDirectBasedDiscount(createDiscountDto, queryRunner);
discount.code = code;
} }
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
@@ -56,271 +43,52 @@ export class DiscountsService {
} }
} }
async findById(id: string) { private async createCodeBasedDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) {
const discount = await this.discountRepository.findById(id); 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) { code = await this.generateDiscountCode();
// Find the discount by code existCode = await this.findDiscountByCode(code, queryRunner);
const discount = await queryRunner.manager.findOne(Discount, { attempts++;
where: { } while (existCode && attempts < this.MAX_ATTEMPTS);
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);
}
discount.code = code;
discount.value = this.calculateDiscountValue(createDiscountDto);
await queryRunner.manager.save(discount);
return discount; return discount;
} }
/** //**************************** */
* Applies a discount code to an invoice private async createDirectBasedDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) {
* @param invoiceId The invoice ID const discount = queryRunner.manager.create(this.discountRepository.target, createDiscountDto);
* @param code The discount code discount.value = this.calculateDiscountValue(createDiscountDto);
* @param userId The user ID await queryRunner.manager.save(discount);
* @returns The updated invoice return discount;
*/
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);
} }
//**************************** */
// Check if the invoice already has a discount private async findDiscountByCode(code: string, queryRunner: QueryRunner) {
if (invoice.discount) { const discount = await queryRunner.manager.findOne(this.discountRepository.target, { where: { code } });
throw new BadRequestException(InvoiceMessage.ALREADY_HAS_DISCOUNT); return discount;
} }
//**************************** */
// Validate the discount code private async generateDiscountCode(length: number = 8) {
const discount = await this.validateDiscountCode(code, userId, invoice, queryRunner); const code = randomBytes(length).toString("hex");
// 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");
return code; 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();
}
}
+11 -16
View File
@@ -1,7 +1,6 @@
import { Body, Controller, Get, Param, Patch, Post, Query } from "@nestjs/common"; import { Body, Controller, Get, Param, Patch, Post, Query } from "@nestjs/common";
import { ApiOperation } from "@nestjs/swagger"; import { ApiOperation } from "@nestjs/swagger";
import { ApplyDiscountDto } from "./DTO/apply-discount.dto";
import { CreateInvoiceDto } from "./DTO/create-invoice.dto"; import { CreateInvoiceDto } from "./DTO/create-invoice.dto";
import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "./DTO/invoices-search-query.dto"; import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "./DTO/invoices-search-query.dto";
import { UpdateInvoiceDto } from "./DTO/update-invoice.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 { UserDec } from "../../common/decorators/user.decorator";
import { ParamDto } from "../../common/DTO/param.dto"; import { ParamDto } from "../../common/DTO/param.dto";
import { VerifyOtpWithUserId } from "../auth/DTO/verify-otp.dto"; import { VerifyOtpWithUserId } from "../auth/DTO/verify-otp.dto";
import { DiscountsService } from "../discounts/providers/discounts.service";
import { PermissionEnum } from "../users/enums/permission.enum"; import { PermissionEnum } from "../users/enums/permission.enum";
@Controller("invoices") @Controller("invoices")
@AuthGuards() @AuthGuards()
export class InvoicesController { export class InvoicesController {
constructor( constructor(private readonly invoiceService: InvoicesService) {}
private readonly invoiceService: InvoicesService,
private readonly discountsService: DiscountsService,
) {}
@ApiOperation({ summary: "create an invoice ==> admin route" }) @ApiOperation({ summary: "create an invoice ==> admin route" })
@PermissionsDec(PermissionEnum.INVOICES) @PermissionsDec(PermissionEnum.INVOICES)
@@ -76,15 +71,15 @@ export class InvoicesController {
return this.invoiceService.payInvoice(paramDto.id, userId); return this.invoiceService.payInvoice(paramDto.id, userId);
} }
@ApiOperation({ summary: "apply discount on invoice by user" }) // @ApiOperation({ summary: "apply discount on invoice by user" })
@Post(":id/apply-discount") // @Post(":id/apply-discount")
async applyDiscount(@Param() paramDto: ParamDto, @Body() applyDiscountDto: ApplyDiscountDto, @UserDec("id") userId: string) { // async applyDiscount(@Param() paramDto: ParamDto, @Body() applyDiscountDto: ApplyDiscountDto, @UserDec("id") userId: string) {
return this.discountsService.applyDiscountToInvoice(paramDto.id, applyDiscountDto.code, userId); // return this.discountsService.applyDiscountToInvoice(paramDto.id, applyDiscountDto.code, userId);
} // }
@ApiOperation({ summary: "cancel discount by user" }) // @ApiOperation({ summary: "cancel discount by user" })
@Post(":id/cancel-discount") // @Post(":id/cancel-discount")
cancelDiscount(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { // cancelDiscount(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
return this.discountsService.cancelInvoiceDiscount(paramDto.id, userId); // return this.discountsService.cancelInvoiceDiscount(paramDto.id, userId);
} // }
} }
@@ -14,6 +14,7 @@ import { ReferralRewardsRepository } from "../repositories/referral-rewards.repo
import { ReferralsRepository } from "../repositories/referrals.repository"; import { ReferralsRepository } from "../repositories/referrals.repository";
@Injectable() @Injectable()
export class ReferralsService { export class ReferralsService {
private MAX_ATTEMPTS = 10;
constructor( constructor(
private readonly referralsRepository: ReferralsRepository, private readonly referralsRepository: ReferralsRepository,
private readonly referralCodesRepository: ReferralCodesRepository, private readonly referralCodesRepository: ReferralCodesRepository,
@@ -25,13 +26,12 @@ export class ReferralsService {
if (existReferralCode) throw new BadRequestException(ReferralMessage.CODE_EXIST_FOR_USER); if (existReferralCode) throw new BadRequestException(ReferralMessage.CODE_EXIST_FOR_USER);
const MAX_ATTEMPTS = 10;
let attempts = 0; let attempts = 0;
let code: string; let code: string;
let existCode; let existCode;
do { do {
if (attempts >= MAX_ATTEMPTS) { if (attempts >= this.MAX_ATTEMPTS) {
throw new BadRequestException(ReferralMessage.CODE_GENERATION_FAILED); 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 { BaseEntity } from "../../../common/entities/base.entity";
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
import { DanakService } from "../../danak-services/entities/danak-service.entity"; 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() @Entity()
@Index(["service", "name"], { unique: true }) @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() }) @Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
price: Decimal; price: Decimal;
//
@Column({ type: "boolean", default: true }) @Column({ type: "boolean", default: true })
isActive: boolean; isActive: boolean;
@@ -29,6 +29,6 @@ export class SubscriptionPlan extends BaseEntity {
@OneToMany(() => UserSubscription, (userSubscription) => userSubscription.plan) @OneToMany(() => UserSubscription, (userSubscription) => userSubscription.plan)
userSubscriptions: UserSubscription[]; userSubscriptions: UserSubscription[];
// @ManyToMany(() => Discount, (discount) => discount.subscriptionPlans) @ManyToOne(() => DirectDiscount, (directDiscount) => directDiscount.subscriptionPlans, { nullable: true })
// discounts: Discount[]; directDiscount?: DirectDiscount;
} }