chore: add discount logic to apply to the plan price and ...
This commit is contained in:
@@ -471,6 +471,11 @@ export const enum InvoiceMessage {
|
|||||||
DISCOUNT_CODE_LENGTH = "طول کد تخفیف باید بین ۱ تا ۱۰۰ کاراکتر باشد",
|
DISCOUNT_CODE_LENGTH = "طول کد تخفیف باید بین ۱ تا ۱۰۰ کاراکتر باشد",
|
||||||
ALREADY_HAS_DISCOUNT = "این صورت حساب قبلا دارای تخفیف است",
|
ALREADY_HAS_DISCOUNT = "این صورت حساب قبلا دارای تخفیف است",
|
||||||
NO_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 {
|
export const enum LearningMessage {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm";
|
|||||||
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";
|
||||||
|
import { Invoice } from "../../invoices/entities/invoice.entity";
|
||||||
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||||
import { DiscountApplicationType } from "../enums/discount-application-type.enum";
|
import { DiscountApplicationType } from "../enums/discount-application-type.enum";
|
||||||
import { DiscountType } from "../enums/discount-type.enum";
|
import { DiscountType } from "../enums/discount-type.enum";
|
||||||
@@ -45,4 +46,7 @@ export class Discount extends BaseEntity {
|
|||||||
|
|
||||||
@OneToMany(() => SubscriptionPlan, (subscriptionPlan) => subscriptionPlan.directDiscount)
|
@OneToMany(() => SubscriptionPlan, (subscriptionPlan) => subscriptionPlan.directDiscount)
|
||||||
subscriptionPlans: SubscriptionPlan[];
|
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 { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { Decimal } from "decimal.js";
|
||||||
import { DataSource, QueryRunner } from "typeorm";
|
import { DataSource, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
import { DiscountMessage } from "../../../common/enums/message.enum";
|
import { DiscountMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||||
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
|
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
|
||||||
import { CreateDiscountDto } from "../DTO/create-discount.dto";
|
import { CreateDiscountDto } from "../DTO/create-discount.dto";
|
||||||
import { SearchDiscountQueryDto } from "../DTO/search-discount-query.dto";
|
import { SearchDiscountQueryDto } from "../DTO/search-discount-query.dto";
|
||||||
@@ -142,6 +144,7 @@ export class DiscountsService {
|
|||||||
|
|
||||||
for (const subscriptionPlan of subscriptionPlans) {
|
for (const subscriptionPlan of subscriptionPlans) {
|
||||||
subscriptionPlan.directDiscount = discount;
|
subscriptionPlan.directDiscount = discount;
|
||||||
|
await this.updatePlanPrice(subscriptionPlan, discount);
|
||||||
await queryRunner.manager.save(subscriptionPlan);
|
await queryRunner.manager.save(subscriptionPlan);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,9 +235,22 @@ export class DiscountsService {
|
|||||||
|
|
||||||
for (const subscriptionPlan of subscriptionPlans) {
|
for (const subscriptionPlan of subscriptionPlans) {
|
||||||
subscriptionPlan.directDiscount = discount;
|
subscriptionPlan.directDiscount = discount;
|
||||||
|
await this.updatePlanPrice(subscriptionPlan, discount);
|
||||||
await queryRunner.manager.save(subscriptionPlan);
|
await queryRunner.manager.save(subscriptionPlan);
|
||||||
}
|
}
|
||||||
|
|
||||||
await queryRunner.manager.save(discount);
|
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() })
|
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
|
||||||
totalPrice: Decimal;
|
totalPrice: Decimal;
|
||||||
|
|
||||||
|
@Column({ type: "decimal", precision: 16, scale: 2, nullable: true, transformer: new DecimalTransformer() })
|
||||||
|
originalPrice?: Decimal;
|
||||||
|
|
||||||
@Column({ type: "enum", enum: InvoiceStatus, default: InvoiceStatus.PENDING })
|
@Column({ type: "enum", enum: InvoiceStatus, default: InvoiceStatus.PENDING })
|
||||||
status: InvoiceStatus;
|
status: InvoiceStatus;
|
||||||
|
|
||||||
@@ -40,8 +43,8 @@ export class Invoice extends BaseEntity {
|
|||||||
@OneToMany(() => InvoiceItem, (invoiceItem) => invoiceItem.invoice, { cascade: true })
|
@OneToMany(() => InvoiceItem, (invoiceItem) => invoiceItem.invoice, { cascade: true })
|
||||||
items: InvoiceItem[];
|
items: InvoiceItem[];
|
||||||
|
|
||||||
@ManyToOne(() => Discount, { nullable: true, onDelete: "SET NULL" })
|
@ManyToOne(() => Discount, (discount) => discount.invoices, { nullable: true, onDelete: "RESTRICT" })
|
||||||
discount: Discount;
|
discount?: Discount;
|
||||||
|
|
||||||
get isOverdue(): boolean {
|
get isOverdue(): boolean {
|
||||||
return this.status === InvoiceStatus.PENDING && new Date() > this.dueDate;
|
return this.status === InvoiceStatus.PENDING && new Date() > this.dueDate;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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";
|
||||||
@@ -71,15 +72,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.invoiceService.applyDiscount(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.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 { AuthMessage, InvoiceMessage, WalletMessage } from "../../../common/enums/message.enum";
|
||||||
import { VerifyOtpWithUserId } from "../../auth/DTO/verify-otp.dto";
|
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 { NotificationsService } from "../../notifications/providers/notifications.service";
|
||||||
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||||
import { UserSubscription } from "../../subscriptions/entities/user-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) {
|
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 = {
|
const invoiceItem = {
|
||||||
name: plan.service.name,
|
name: plan.service.name,
|
||||||
count: 1,
|
count: 1,
|
||||||
unitPrice: plan.price,
|
unitPrice: originalPrice,
|
||||||
discount: 0,
|
discount: discount ? new Decimal(originalPrice).sub(finalPrice).toNumber() : 0,
|
||||||
subscriptionPlan: userSub,
|
subscriptionPlan: userSub,
|
||||||
totalPrice: plan.price,
|
totalPrice: finalPrice,
|
||||||
};
|
};
|
||||||
|
|
||||||
const basePrice = plan.price;
|
const taxAmount = new Decimal(finalPrice).mul(0.1);
|
||||||
const taxAmount = new Decimal(basePrice).mul(0.1);
|
const totalPrice = new Decimal(finalPrice).add(taxAmount);
|
||||||
const totalPrice = new Decimal(basePrice).add(taxAmount);
|
|
||||||
|
|
||||||
const invoice = qryRnr.manager.create(Invoice, {
|
const invoice = qryRnr.manager.create(Invoice, {
|
||||||
user,
|
user,
|
||||||
totalPrice: totalPrice,
|
totalPrice: totalPrice,
|
||||||
|
originalPrice: new Decimal(originalPrice).add(new Decimal(originalPrice).mul(0.1)),
|
||||||
tax: taxAmount.toNumber(),
|
tax: taxAmount.toNumber(),
|
||||||
status: InvoiceStatus.WAIT_PAYMENT,
|
status: InvoiceStatus.WAIT_PAYMENT,
|
||||||
dueDate,
|
dueDate,
|
||||||
items: [invoiceItem],
|
items: [invoiceItem],
|
||||||
|
discount: discount || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await qryRnr.manager.save(Invoice, invoice);
|
await qryRnr.manager.save(Invoice, invoice);
|
||||||
@@ -345,12 +352,12 @@ export class InvoicesService {
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
invoice = await this.invoiceRepository.findOne({
|
invoice = await this.invoiceRepository.findOne({
|
||||||
where: { id: invoiceId },
|
where: { id: invoiceId },
|
||||||
relations: { items: { subscriptionPlan: true }, user: true },
|
relations: { items: { subscriptionPlan: true }, user: true, discount: true },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
invoice = await this.invoiceRepository.findOne({
|
invoice = await this.invoiceRepository.findOne({
|
||||||
where: { id: invoiceId, user: { id: userId } },
|
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;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
//*********************************** */
|
//*********************************** */
|
||||||
|
|
||||||
async countUserInvoices(userId: string) {
|
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}`);
|
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() })
|
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
|
||||||
price: Decimal;
|
price: Decimal;
|
||||||
|
|
||||||
|
@Column({ type: "decimal", precision: 16, scale: 2, nullable: true, transformer: new DecimalTransformer() })
|
||||||
|
originalPrice?: Decimal;
|
||||||
|
|
||||||
@Column({ type: "boolean", default: true })
|
@Column({ type: "boolean", default: true })
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export class SubscriptionsService {
|
|||||||
where: { id: subscribeDto.planId, service: { id: serviceId } },
|
where: { id: subscribeDto.planId, service: { id: serviceId } },
|
||||||
relations: {
|
relations: {
|
||||||
service: true,
|
service: true,
|
||||||
|
directDiscount: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user