chore: add discount logic to apply to the plan price and ...

This commit is contained in:
mahyargdz
2025-04-19 12:36:33 +03:30
parent f9e44fc29a
commit bf3402cc7a
8 changed files with 144 additions and 20 deletions
+5
View File
@@ -471,6 +471,11 @@ export const enum InvoiceMessage {
DISCOUNT_CODE_LENGTH = "طول کد تخفیف باید بین ۱ تا ۱۰۰ کاراکتر باشد",
ALREADY_HAS_DISCOUNT = "این صورت حساب قبلا دارای تخفیف است",
NO_DISCOUNT = "این صورت حساب دارای تخفیف نیست",
NOT_AUTHORIZED = "شما مجاز به انجام این عملیات نیستید",
INVALID_DISCOUNT_CODE = "کد تخفیف نامعتبر است",
ORIGINAL_PRICE_NOT_FOUND = "قیمت اصلی صورت حساب یافت نشد",
NOT_FOUND_BY_ID_OR_NOT_BELONG_TO_USER = "صورت حساب یافت نشد یا متعلق به کاربر نمی باشد",
DISCOUNT_ALREADY_APPLIED = "تخفیف قبلا برای این صورت حساب اعمال شده است",
}
export const enum LearningMessage {
@@ -3,6 +3,7 @@ import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm";
import { UsageDiscount } from "./usage-discount.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
import { Invoice } from "../../invoices/entities/invoice.entity";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { DiscountApplicationType } from "../enums/discount-application-type.enum";
import { DiscountType } from "../enums/discount-type.enum";
@@ -45,4 +46,7 @@ export class Discount extends BaseEntity {
@OneToMany(() => SubscriptionPlan, (subscriptionPlan) => subscriptionPlan.directDiscount)
subscriptionPlans: SubscriptionPlan[];
@OneToMany(() => Invoice, (invoice) => invoice.discount)
invoices: Invoice[];
}
@@ -2,9 +2,11 @@ import { randomBytes } from "node:crypto";
import { BadRequestException, Injectable } from "@nestjs/common";
import dayjs from "dayjs";
import { Decimal } from "decimal.js";
import { DataSource, QueryRunner } from "typeorm";
import { DiscountMessage } from "../../../common/enums/message.enum";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
import { CreateDiscountDto } from "../DTO/create-discount.dto";
import { SearchDiscountQueryDto } from "../DTO/search-discount-query.dto";
@@ -142,6 +144,7 @@ export class DiscountsService {
for (const subscriptionPlan of subscriptionPlans) {
subscriptionPlan.directDiscount = discount;
await this.updatePlanPrice(subscriptionPlan, discount);
await queryRunner.manager.save(subscriptionPlan);
}
@@ -232,9 +235,22 @@ export class DiscountsService {
for (const subscriptionPlan of subscriptionPlans) {
subscriptionPlan.directDiscount = discount;
await this.updatePlanPrice(subscriptionPlan, discount);
await queryRunner.manager.save(subscriptionPlan);
}
await queryRunner.manager.save(discount);
}
//************************************ */
private async updatePlanPrice(subscriptionPlan: SubscriptionPlan, discount: Discount) {
if (!subscriptionPlan.originalPrice) subscriptionPlan.originalPrice = new Decimal(subscriptionPlan.price);
let discountAmount: Decimal;
if (discount.type === DiscountType.PERCENTAGE) {
discountAmount = subscriptionPlan.originalPrice.mul(discount.value).div(100);
} else {
discountAmount = new Decimal(discount.value);
}
subscriptionPlan.price = subscriptionPlan.originalPrice.sub(discountAmount);
}
}
@@ -20,6 +20,9 @@ export class Invoice extends BaseEntity {
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
totalPrice: Decimal;
@Column({ type: "decimal", precision: 16, scale: 2, nullable: true, transformer: new DecimalTransformer() })
originalPrice?: Decimal;
@Column({ type: "enum", enum: InvoiceStatus, default: InvoiceStatus.PENDING })
status: InvoiceStatus;
@@ -40,8 +43,8 @@ export class Invoice extends BaseEntity {
@OneToMany(() => InvoiceItem, (invoiceItem) => invoiceItem.invoice, { cascade: true })
items: InvoiceItem[];
@ManyToOne(() => Discount, { nullable: true, onDelete: "SET NULL" })
discount: Discount;
@ManyToOne(() => Discount, (discount) => discount.invoices, { nullable: true, onDelete: "RESTRICT" })
discount?: Discount;
get isOverdue(): boolean {
return this.status === InvoiceStatus.PENDING && new Date() > this.dueDate;
+12 -11
View File
@@ -1,6 +1,7 @@
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";
@@ -71,15 +72,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: "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: "apply discount on invoice by user" })
@Post(":id/apply-discount")
async applyDiscount(@Param() paramDto: ParamDto, @Body() applyDiscountDto: ApplyDiscountDto, @UserDec("id") userId: string) {
return this.invoiceService.applyDiscount(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.invoiceService.cancelDiscount(paramDto.id, userId);
}
}
@@ -9,6 +9,8 @@ import { Between, DataSource, QueryRunner } from "typeorm";
import { AuthMessage, InvoiceMessage, WalletMessage } from "../../../common/enums/message.enum";
import { VerifyOtpWithUserId } from "../../auth/DTO/verify-otp.dto";
import { Discount } from "../../discounts/entities/discount.entity";
import { DiscountType } from "../../discounts/enums/discount-type.enum";
import { NotificationsService } from "../../notifications/providers/notifications.service";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { UserSubscription } from "../../subscriptions/entities/user-subscription.entity";
@@ -267,26 +269,31 @@ export class InvoicesService {
}
///********************************** */
async createInvoiceForSubscription(user: User, plan: SubscriptionPlan, userSub: UserSubscription, dueDate: Date, qryRnr: QueryRunner) {
const discount = plan.directDiscount;
const originalPrice = plan.originalPrice || plan.price;
const finalPrice = plan.price;
const invoiceItem = {
name: plan.service.name,
count: 1,
unitPrice: plan.price,
discount: 0,
unitPrice: originalPrice,
discount: discount ? new Decimal(originalPrice).sub(finalPrice).toNumber() : 0,
subscriptionPlan: userSub,
totalPrice: plan.price,
totalPrice: finalPrice,
};
const basePrice = plan.price;
const taxAmount = new Decimal(basePrice).mul(0.1);
const totalPrice = new Decimal(basePrice).add(taxAmount);
const taxAmount = new Decimal(finalPrice).mul(0.1);
const totalPrice = new Decimal(finalPrice).add(taxAmount);
const invoice = qryRnr.manager.create(Invoice, {
user,
totalPrice: totalPrice,
originalPrice: new Decimal(originalPrice).add(new Decimal(originalPrice).mul(0.1)),
tax: taxAmount.toNumber(),
status: InvoiceStatus.WAIT_PAYMENT,
dueDate,
items: [invoiceItem],
discount: discount || undefined,
});
await qryRnr.manager.save(Invoice, invoice);
@@ -345,12 +352,12 @@ export class InvoicesService {
if (isAdmin) {
invoice = await this.invoiceRepository.findOne({
where: { id: invoiceId },
relations: { items: { subscriptionPlan: true }, user: true },
relations: { items: { subscriptionPlan: true }, user: true, discount: true },
});
} else {
invoice = await this.invoiceRepository.findOne({
where: { id: invoiceId, user: { id: userId } },
relations: { items: { subscriptionPlan: true } },
relations: { items: { subscriptionPlan: true }, discount: true },
});
}
@@ -487,6 +494,7 @@ export class InvoicesService {
});
return count;
}
//*********************************** */
async countUserInvoices(userId: string) {
@@ -558,4 +566,87 @@ export class InvoicesService {
this.logger.log(`Scheduled recurring invoice for user ${createDto.userId} with interval ${createDto.recurringPeriod}`);
}
}
//*********************************** */
async applyDiscount(invoiceId: string, discountCode: string, userId: string) {
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const invoice = await this.getInvoiceByIdWithQueryRunner(invoiceId, userId, queryRunner);
if (invoice.discount) throw new BadRequestException(InvoiceMessage.DISCOUNT_ALREADY_APPLIED);
const discount = await queryRunner.manager.findOne(Discount, {
where: { code: discountCode, isActive: true },
});
if (!discount) throw new BadRequestException(InvoiceMessage.INVALID_DISCOUNT_CODE);
if (!invoice.originalPrice) invoice.originalPrice = new Decimal(invoice.totalPrice);
let discountAmount: Decimal;
if (discount.type === DiscountType.PERCENTAGE) {
discountAmount = invoice.originalPrice.mul(discount.value).div(100);
} else {
discountAmount = new Decimal(discount.value);
}
invoice.totalPrice = invoice.originalPrice.sub(discountAmount);
invoice.discount = discount;
await queryRunner.manager.save(invoice);
await queryRunner.commitTransaction();
return {
message: InvoiceMessage.DISCOUNT_APPLIED,
invoice,
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
//*********************************** */
async cancelDiscount(invoiceId: string, userId: string) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const invoice = await this.getInvoiceByIdWithQueryRunner(invoiceId, userId, queryRunner);
if (!invoice.discount) throw new BadRequestException(InvoiceMessage.NO_DISCOUNT);
if (!invoice.originalPrice) throw new BadRequestException(InvoiceMessage.ORIGINAL_PRICE_NOT_FOUND);
invoice.totalPrice = invoice.originalPrice;
invoice.discount = undefined;
await queryRunner.manager.save(invoice);
await queryRunner.commitTransaction();
return {
message: InvoiceMessage.DISCOUNT_CANCELED,
invoice,
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
//*********************************** */
private async getInvoiceByIdWithQueryRunner(invoiceId: string, userId: string, queryRunner: QueryRunner) {
const invoice = await queryRunner.manager.findOne(Invoice, {
where: { id: invoiceId, user: { id: userId } },
relations: { user: true, discount: true },
});
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID_OR_NOT_BELONG_TO_USER);
return invoice;
}
}
@@ -20,6 +20,9 @@ export class SubscriptionPlan extends BaseEntity {
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
price: Decimal;
@Column({ type: "decimal", precision: 16, scale: 2, nullable: true, transformer: new DecimalTransformer() })
originalPrice?: Decimal;
@Column({ type: "boolean", default: true })
isActive: boolean;
@@ -202,6 +202,7 @@ export class SubscriptionsService {
where: { id: subscribeDto.planId, service: { id: serviceId } },
relations: {
service: true,
directDiscount: true,
},
});