refactor: the invoice and subscription flow of user

This commit is contained in:
mahyargdz
2025-03-01 17:08:02 +03:30
parent ada9c153e6
commit 7c1d444c21
9 changed files with 87 additions and 86 deletions
@@ -5,7 +5,7 @@ import { Column, Entity, ManyToOne } from "typeorm";
import { Invoice } from "./invoice.entity"; import { Invoice } from "./invoice.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 { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; import { UserSubscription } from "../../subscriptions/entities/user-subscription.entity";
@Entity() @Entity()
export class InvoiceItem extends BaseEntity { export class InvoiceItem extends BaseEntity {
@@ -27,6 +27,6 @@ export class InvoiceItem 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;
@ManyToOne(() => SubscriptionPlan, { nullable: true, onDelete: "RESTRICT" }) @ManyToOne(() => UserSubscription, { nullable: true, onDelete: "RESTRICT" })
subscriptionPlan: SubscriptionPlan | null; subscriptionPlan: UserSubscription | null;
} }
@@ -1,7 +1,5 @@
export enum InvoiceStatus { export enum InvoiceStatus {
PENDING = "PENDING", PENDING = "PENDING",
APPROVED = "APPROVED",
REJECTED = "REJECTED",
WAIT_PAYMENT = "WAIT_PAYMENT", WAIT_PAYMENT = "WAIT_PAYMENT",
PAID = "PAID", PAID = "PAID",
EXPIRED = "EXPIRED", EXPIRED = "EXPIRED",
@@ -11,6 +11,8 @@ import { DiscountRepository } from "../../discounts/repositories/discount.reposi
import { UsageDiscountRepository } from "../../discounts/repositories/usage-discount.repository"; import { UsageDiscountRepository } from "../../discounts/repositories/usage-discount.repository";
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 { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum";
import { UsersService } from "../../users/providers/users.service"; import { UsersService } from "../../users/providers/users.service";
import { OTPService } from "../../utils/providers/otp.service"; import { OTPService } from "../../utils/providers/otp.service";
import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { PaginationUtils } from "../../utils/providers/pagination.utils";
@@ -110,9 +112,13 @@ export class InvoicesService {
const invoice = await this.invoiceRepository.findOne({ where: { id: invoiceId, user: { id: userId } }, relations: { items: true } }); const invoice = await this.invoiceRepository.findOne({ where: { id: invoiceId, user: { id: userId } }, relations: { items: true } });
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
if (invoice.status === InvoiceStatus.APPROVED) throw new BadRequestException(InvoiceMessage.ALREADY_APPROVED); if (invoice.status === InvoiceStatus.WAIT_PAYMENT) throw new BadRequestException(InvoiceMessage.ALREADY_APPROVED);
if (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID); if (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID);
if (dayjs().isAfter(invoice.dueDate)) throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE); if (dayjs().isAfter(invoice.dueDate)) {
invoice.status = InvoiceStatus.EXPIRED;
await this.invoiceRepository.save(invoice);
throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE);
}
if (invoice.status !== InvoiceStatus.PENDING) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_APPROVED); if (invoice.status !== InvoiceStatus.PENDING) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_APPROVED);
@@ -149,14 +155,13 @@ export class InvoicesService {
const isValid = await this.otpService.verifyOtp(user.phone, code, "INVOICE_VERIFY"); const isValid = await this.otpService.verifyOtp(user.phone, code, "INVOICE_VERIFY");
if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP);
if (invoice.status === InvoiceStatus.REJECTED) throw new BadRequestException(InvoiceMessage.INVOICE_IS_REJECTED); if (invoice.status === InvoiceStatus.WAIT_PAYMENT) throw new BadRequestException(InvoiceMessage.ALREADY_APPROVED);
if (invoice.status === InvoiceStatus.APPROVED) throw new BadRequestException(InvoiceMessage.ALREADY_APPROVED);
if (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID); if (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID);
if (dayjs().isAfter(invoice.dueDate)) throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE); if (dayjs().isAfter(invoice.dueDate)) throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE);
if (invoice.status !== InvoiceStatus.PENDING) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_APPROVED); if (invoice.status !== InvoiceStatus.PENDING) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_APPROVED);
invoice.status = InvoiceStatus.APPROVED; invoice.status = InvoiceStatus.WAIT_PAYMENT;
await this.invoiceRepository.save(invoice); await this.invoiceRepository.save(invoice);
@@ -167,14 +172,27 @@ export class InvoicesService {
} }
///********************************** */ ///********************************** */
async createInvoiceForSubscription(userId: string, plan: SubscriptionPlan, dueDate: Date, queryRunner: QueryRunner) { async createInvoiceForSubscription(
userId: string,
plan: SubscriptionPlan,
userSub: UserSubscription,
dueDate: Date,
queryRunner: QueryRunner,
) {
const invoiceItems = [ const invoiceItems = [
{ name: plan.service.name, count: 1, unitPrice: plan.price, discount: 0, subscriptionPlan: plan, totalPrice: plan.price }, {
name: plan.service.name,
count: 1,
unitPrice: plan.price,
discount: 0,
subscriptionPlan: userSub,
totalPrice: plan.price,
},
]; ];
const invoice = queryRunner.manager.create(Invoice, { const invoice = queryRunner.manager.create(Invoice, {
totalPrice: plan.price, totalPrice: plan.price,
user: { id: userId }, user: { id: userId },
status: InvoiceStatus.PAID, status: InvoiceStatus.PENDING,
paidAt: new Date(), paidAt: new Date(),
dueDate, dueDate,
items: invoiceItems, items: invoiceItems,
@@ -238,7 +256,9 @@ export class InvoicesService {
.andWhere("invoice.user.id = :userId", { userId: userId }) .andWhere("invoice.user.id = :userId", { userId: userId })
.leftJoinAndSelect("invoice.items", "items") .leftJoinAndSelect("invoice.items", "items")
.leftJoinAndSelect("items.subscriptionPlan", "subscriptionPlan", "subscriptionPlan.id IS NOT NULL") .leftJoinAndSelect("items.subscriptionPlan", "subscriptionPlan", "subscriptionPlan.id IS NOT NULL")
.leftJoinAndSelect("subscriptionPlan.service", "service", "subscriptionPlan.id IS NOT NULL"); .leftJoinAndSelect("subscriptionPlan.plan", "plan", "subscriptionPlan.id IS NOT NULL")
.leftJoin("plan.service", "service", "subscriptionPlan.id IS NOT NULL")
.addSelect(["service.name", "service.id"]);
if (queryDto.status) { if (queryDto.status) {
queryBuilder.andWhere("invoice.status = :status", { status: queryDto.status }); queryBuilder.andWhere("invoice.status = :status", { status: queryDto.status });
@@ -265,27 +285,33 @@ export class InvoicesService {
const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner);
const invoice = await this.getPendingInvoiceByIdWithQueryRunner(invoiceId, user.id, queryRunner); const invoice = await this.getPendingInvoiceByIdWithQueryRunner(invoiceId, user.id, queryRunner);
console.log(invoice);
if (invoice.status === InvoiceStatus.REJECTED) throw new BadRequestException(InvoiceMessage.INVOICE_IS_REJECTED); if (invoice.status !== InvoiceStatus.WAIT_PAYMENT) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_PAID);
if (invoice.status === InvoiceStatus.EXPIRED) throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE);
if (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID);
if (invoice.status !== InvoiceStatus.APPROVED) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_PAID);
const userWallet = await this.walletsService.getWalletByUserId(user.id, queryRunner); const userWallet = await this.walletsService.getWalletByUserId(user.id, queryRunner);
if (new Decimal(userWallet.balance).lessThan(invoice.totalPrice)) throw new BadRequestException(WalletMessage.INSUFFICIENT_BALANCE); if (userWallet.balance < invoice.totalPrice) throw new BadRequestException(WalletMessage.INSUFFICIENT_BALANCE);
userWallet.balance = new Decimal(userWallet.balance).sub(invoice.totalPrice); userWallet.balance = new Decimal(userWallet.balance).sub(invoice.totalPrice);
await queryRunner.manager.save(Wallet, userWallet); await queryRunner.manager.save(Wallet, userWallet);
await this.walletsService.createInvoiceTransaction(invoice.totalPrice, userWallet.id, queryRunner); if (invoice.items[0]?.subscriptionPlan) {
const userSubscription = invoice.items[0].subscriptionPlan;
userSubscription.status = SubscriptionStatus.ACTIVE;
//
await queryRunner.manager.save(UserSubscription, userSubscription);
await this.walletsService.createSubscriptionTransaction(invoice.totalPrice, userWallet.id, queryRunner);
//
} else {
await this.walletsService.createInvoiceTransaction(invoice.totalPrice, userWallet.id, queryRunner);
}
invoice.status = InvoiceStatus.PAID; invoice.status = InvoiceStatus.PAID;
invoice.paidAt = new Date(); invoice.paidAt = dayjs().toDate();
await queryRunner.manager.save(Invoice, invoice); await queryRunner.manager.save(Invoice, invoice);
console.log(invoice);
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
return { return {
message: InvoiceMessage.INVOICE_PAID, message: InvoiceMessage.INVOICE_PAID,
@@ -302,7 +328,20 @@ export class InvoicesService {
//*********************************** */ //*********************************** */
async getPendingInvoiceByIdWithQueryRunner(invoiceId: string, userId: string, queryRunner: QueryRunner) { async getPendingInvoiceByIdWithQueryRunner(invoiceId: string, userId: string, queryRunner: QueryRunner) {
const invoice = await queryRunner.manager.findOneBy(Invoice, { user: { id: userId }, status: InvoiceStatus.PENDING, id: invoiceId }); const invoice = await queryRunner.manager.findOne(Invoice, {
where: {
user: { id: userId },
status: InvoiceStatus.WAIT_PAYMENT,
id: invoiceId,
},
relations: {
items: {
subscriptionPlan: {
plan: true,
},
},
},
});
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
return invoice; return invoice;
} }
@@ -107,7 +107,7 @@ export class NotificationsService {
//************************ */ //************************ */
async markAllAsRead(userId: string) { async markAllAsRead(userId: string) {
await this.notificationRepository.delete({ recipient: { id: userId } }); await this.notificationRepository.update({ recipient: { id: userId } }, { isRead: true });
return { return {
message: CommonMessage.UPDATE_SUCCESS, message: CommonMessage.UPDATE_SUCCESS,
}; };
@@ -28,12 +28,6 @@ export class UserSubscription extends BaseEntity {
@Column({ type: "text", nullable: false }) @Column({ type: "text", nullable: false })
description: string; description: string;
@Column({ type: "enum", enum: SubscriptionStatus, default: SubscriptionStatus.PENDING }) @Column({ type: "enum", enum: SubscriptionStatus, default: SubscriptionStatus.INACTIVE })
status: SubscriptionStatus; status: SubscriptionStatus;
@Column({ type: "boolean", default: false })
isCanceled: boolean;
// @OneToMany(() => UserQuickAccess, (quickAccess) => quickAccess.userSubscription)
// quickAccess: UserQuickAccess[];
} }
@@ -1,7 +1,7 @@
export enum SubscriptionStatus { export enum SubscriptionStatus {
ACTIVE = "ACTIVE", ACTIVE = "ACTIVE",
INACTIVE = "INACTIVE", INACTIVE = "INACTIVE",
PENDING = "PENDING", // PENDING = "PENDING",
CANCELED = "CANCELED", CANCELED = "CANCELED",
EXPIRED = "EXPIRED", EXPIRED = "EXPIRED",
} }
@@ -4,14 +4,14 @@ import dayjs from "dayjs";
import Decimal from "decimal.js"; import Decimal from "decimal.js";
import { DataSource, In } from "typeorm"; import { DataSource, In } from "typeorm";
import { ServiceMessage, SubscriptionMessage, WalletMessage } from "../../../common/enums/message.enum"; import { ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum";
import { DanakServicesService } from "../../danak-services/providers/danak-services.service"; import { DanakServicesService } from "../../danak-services/providers/danak-services.service";
import { DiscountCalculationType } from "../../discounts/enums/discount-type.enum"; import { DiscountCalculationType } from "../../discounts/enums/discount-type.enum";
import { InvoicesService } from "../../invoices/providers/invoices.service"; import { InvoicesService } from "../../invoices/providers/invoices.service";
import { UsersService } from "../../users/providers/users.service"; import { UsersService } from "../../users/providers/users.service";
import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { Wallet } from "../../wallets/entities/wallet.entity"; // import { Wallet } from "../../wallets/entities/wallet.entity";
import { WalletsService } from "../../wallets/providers/wallets.service"; // import { WalletsService } from "../../wallets/providers/wallets.service";
import { AddSubscriptionsToServiceDto } from "../DTO/create-subscription.dto"; import { AddSubscriptionsToServiceDto } from "../DTO/create-subscription.dto";
import { SearchUserSubsQueryDto } from "../DTO/search-user-subs-query.dto"; import { SearchUserSubsQueryDto } from "../DTO/search-user-subs-query.dto";
import { ServiceSubsQueryDto } from "../DTO/service-subs-query.dto"; import { ServiceSubsQueryDto } from "../DTO/service-subs-query.dto";
@@ -30,7 +30,7 @@ export class SubscriptionsService {
private readonly userSubscriptionsRepository: UserSubscriptionsRepository, private readonly userSubscriptionsRepository: UserSubscriptionsRepository,
private readonly invoicesService: InvoicesService, private readonly invoicesService: InvoicesService,
private readonly usersService: UsersService, private readonly usersService: UsersService,
private readonly walletsService: WalletsService, // private readonly walletsService: WalletsService,
private readonly danakServices: DanakServicesService, private readonly danakServices: DanakServicesService,
private readonly dataSource: DataSource, private readonly dataSource: DataSource,
) {} ) {}
@@ -227,47 +227,18 @@ export class SubscriptionsService {
const startDate = dayjs().toDate(); const startDate = dayjs().toDate();
const endDate = dayjs().add(plan.duration, "day").toDate(); const endDate = dayjs().add(plan.duration, "day").toDate();
// const startDate = new Date();
// const endDate = new Date();
// endDate.setDate(startDate.getDate() + plan.duration);
// //
const userSubscription = queryRunner.manager.create(UserSubscription, { ...subscribeDto, user, plan, startDate, endDate }); const userSubscription = queryRunner.manager.create(UserSubscription, { ...subscribeDto, user, plan, startDate, endDate });
await queryRunner.manager.save(UserSubscription, userSubscription); await queryRunner.manager.save(UserSubscription, userSubscription);
const userWallet = await this.walletsService.getWalletByUserId(user.id, queryRunner);
let finalPrice = new Decimal(plan.price);
const activeDiscounts =
plan.discounts?.filter((discount) => discount.isActive && new Date() >= discount.startDate && new Date() <= discount.endDate) || [];
activeDiscounts.forEach((discount) => {
if (discount.calculationType === DiscountCalculationType.PERCENTAGE) {
finalPrice = finalPrice.sub(finalPrice.mul(discount.amount).div(100));
} else if (discount.calculationType === DiscountCalculationType.FIXED) {
finalPrice = finalPrice.sub(discount.amount);
}
});
plan.price = finalPrice;
if (new Decimal(userWallet.balance).lessThan(finalPrice)) throw new BadRequestException(WalletMessage.INSUFFICIENT_BALANCE);
// if (userWallet.balance < plan.price) throw new BadRequestException(WalletMessage.INSUFFICIENT_BALANCE);
//
userWallet.balance = new Decimal(userWallet.balance).sub(finalPrice);
await queryRunner.manager.save(Wallet, userWallet);
// //
await this.walletsService.createSubscriptionTransaction(finalPrice, userWallet.id, queryRunner);
//TODO: need queue handling for notification //TODO: need queue handling for notification
const invoiceDueDate = dayjs(userSubscription.endDate).subtract(3, "day").toDate(); const invoiceDueDate = dayjs(userSubscription.startDate).add(5, "day").toDate();
const invoice = await this.invoicesService.createInvoiceForSubscription(userId, plan, invoiceDueDate, queryRunner); const invoice = await this.invoicesService.createInvoiceForSubscription(userId, plan, userSubscription, invoiceDueDate, queryRunner);
userSubscription.status = SubscriptionStatus.ACTIVE; userSubscription.status = SubscriptionStatus.INACTIVE;
await queryRunner.manager.save(UserSubscription, userSubscription); await queryRunner.manager.save(UserSubscription, userSubscription);
@@ -25,9 +25,8 @@ export class Ticket extends BaseEntity {
@Column({ type: "enum", enum: TicketPriority, nullable: false }) @Column({ type: "enum", enum: TicketPriority, nullable: false })
priority: TicketPriority; priority: TicketPriority;
//TODO:set this to not nullable
@ManyToOne(() => DanakService, (danakService) => danakService.tickets, { onDelete: "RESTRICT", nullable: true }) @ManyToOne(() => DanakService, (danakService) => danakService.tickets, { onDelete: "RESTRICT", nullable: true })
danakService: DanakService; danakService: DanakService | null;
@ManyToOne(() => User, (user) => user.tickets, { nullable: true }) @ManyToOne(() => User, (user) => user.tickets, { nullable: true })
assignedTo: User; assignedTo: User;
@@ -92,6 +92,21 @@ export class WalletsService {
return transaction; return transaction;
} }
//*********************************** */
async createInvoiceTransaction(amount: Decimal, walletId: string, queryRunner: QueryRunner) {
const transaction = queryRunner.manager.create(WalletTransaction, {
amount,
wallet: { id: walletId },
type: TransactionType.DEBIT,
description: WalletMessage.INVOICE_WALLET_TRANSFER,
});
await queryRunner.manager.save(WalletTransaction, transaction);
return transaction;
}
//*********************************** */ //*********************************** */
async getTransaction(userId: string, paginationDto: PaginationDto) { async getTransaction(userId: string, paginationDto: PaginationDto) {
@@ -189,19 +204,4 @@ export class WalletsService {
if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND); if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND);
return wallet; return wallet;
} }
//*********************************** */
async createInvoiceTransaction(amount: Decimal, walletId: string, queryRunner: QueryRunner) {
const transaction = queryRunner.manager.create(WalletTransaction, {
amount,
wallet: { id: walletId },
type: TransactionType.DEBIT,
description: WalletMessage.INVOICE_WALLET_TRANSFER,
});
await queryRunner.manager.save(WalletTransaction, transaction);
return transaction;
}
} }