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 { BaseEntity } from "../../../common/entities/base.entity";
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { UserSubscription } from "../../subscriptions/entities/user-subscription.entity";
@Entity()
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() })
totalPrice: Decimal;
@ManyToOne(() => SubscriptionPlan, { nullable: true, onDelete: "RESTRICT" })
subscriptionPlan: SubscriptionPlan | null;
@ManyToOne(() => UserSubscription, { nullable: true, onDelete: "RESTRICT" })
subscriptionPlan: UserSubscription | null;
}
@@ -1,7 +1,5 @@
export enum InvoiceStatus {
PENDING = "PENDING",
APPROVED = "APPROVED",
REJECTED = "REJECTED",
WAIT_PAYMENT = "WAIT_PAYMENT",
PAID = "PAID",
EXPIRED = "EXPIRED",
@@ -11,6 +11,8 @@ import { DiscountRepository } from "../../discounts/repositories/discount.reposi
import { UsageDiscountRepository } from "../../discounts/repositories/usage-discount.repository";
import { NotificationsService } from "../../notifications/providers/notifications.service";
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 { OTPService } from "../../utils/providers/otp.service";
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 } });
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 (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);
@@ -149,14 +155,13 @@ export class InvoicesService {
const isValid = await this.otpService.verifyOtp(user.phone, code, "INVOICE_VERIFY");
if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP);
if (invoice.status === InvoiceStatus.REJECTED) throw new BadRequestException(InvoiceMessage.INVOICE_IS_REJECTED);
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 (dayjs().isAfter(invoice.dueDate)) throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE);
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);
@@ -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 = [
{ 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, {
totalPrice: plan.price,
user: { id: userId },
status: InvoiceStatus.PAID,
status: InvoiceStatus.PENDING,
paidAt: new Date(),
dueDate,
items: invoiceItems,
@@ -238,7 +256,9 @@ export class InvoicesService {
.andWhere("invoice.user.id = :userId", { userId: userId })
.leftJoinAndSelect("invoice.items", "items")
.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) {
queryBuilder.andWhere("invoice.status = :status", { status: queryDto.status });
@@ -265,27 +285,33 @@ export class InvoicesService {
const user = await this.usersService.findOneByIdWithQueryRunner(userId, 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.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);
if (invoice.status !== InvoiceStatus.WAIT_PAYMENT) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_PAID);
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);
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.paidAt = new Date();
invoice.paidAt = dayjs().toDate();
await queryRunner.manager.save(Invoice, invoice);
console.log(invoice);
await queryRunner.commitTransaction();
return {
message: InvoiceMessage.INVOICE_PAID,
@@ -302,7 +328,20 @@ export class InvoicesService {
//*********************************** */
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);
return invoice;
}
@@ -107,7 +107,7 @@ export class NotificationsService {
//************************ */
async markAllAsRead(userId: string) {
await this.notificationRepository.delete({ recipient: { id: userId } });
await this.notificationRepository.update({ recipient: { id: userId } }, { isRead: true });
return {
message: CommonMessage.UPDATE_SUCCESS,
};
@@ -28,12 +28,6 @@ export class UserSubscription extends BaseEntity {
@Column({ type: "text", nullable: false })
description: string;
@Column({ type: "enum", enum: SubscriptionStatus, default: SubscriptionStatus.PENDING })
@Column({ type: "enum", enum: SubscriptionStatus, default: SubscriptionStatus.INACTIVE })
status: SubscriptionStatus;
@Column({ type: "boolean", default: false })
isCanceled: boolean;
// @OneToMany(() => UserQuickAccess, (quickAccess) => quickAccess.userSubscription)
// quickAccess: UserQuickAccess[];
}
@@ -1,7 +1,7 @@
export enum SubscriptionStatus {
ACTIVE = "ACTIVE",
INACTIVE = "INACTIVE",
PENDING = "PENDING",
// PENDING = "PENDING",
CANCELED = "CANCELED",
EXPIRED = "EXPIRED",
}
@@ -4,14 +4,14 @@ import dayjs from "dayjs";
import Decimal from "decimal.js";
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 { DiscountCalculationType } from "../../discounts/enums/discount-type.enum";
import { InvoicesService } from "../../invoices/providers/invoices.service";
import { UsersService } from "../../users/providers/users.service";
import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { Wallet } from "../../wallets/entities/wallet.entity";
import { WalletsService } from "../../wallets/providers/wallets.service";
// import { Wallet } from "../../wallets/entities/wallet.entity";
// import { WalletsService } from "../../wallets/providers/wallets.service";
import { AddSubscriptionsToServiceDto } from "../DTO/create-subscription.dto";
import { SearchUserSubsQueryDto } from "../DTO/search-user-subs-query.dto";
import { ServiceSubsQueryDto } from "../DTO/service-subs-query.dto";
@@ -30,7 +30,7 @@ export class SubscriptionsService {
private readonly userSubscriptionsRepository: UserSubscriptionsRepository,
private readonly invoicesService: InvoicesService,
private readonly usersService: UsersService,
private readonly walletsService: WalletsService,
// private readonly walletsService: WalletsService,
private readonly danakServices: DanakServicesService,
private readonly dataSource: DataSource,
) {}
@@ -227,47 +227,18 @@ export class SubscriptionsService {
const startDate = dayjs().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 });
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
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);
userSubscription.status = SubscriptionStatus.ACTIVE;
const invoice = await this.invoicesService.createInvoiceForSubscription(userId, plan, userSubscription, invoiceDueDate, queryRunner);
userSubscription.status = SubscriptionStatus.INACTIVE;
await queryRunner.manager.save(UserSubscription, userSubscription);
@@ -25,9 +25,8 @@ export class Ticket extends BaseEntity {
@Column({ type: "enum", enum: TicketPriority, nullable: false })
priority: TicketPriority;
//TODO:set this to not nullable
@ManyToOne(() => DanakService, (danakService) => danakService.tickets, { onDelete: "RESTRICT", nullable: true })
danakService: DanakService;
danakService: DanakService | null;
@ManyToOne(() => User, (user) => user.tickets, { nullable: true })
assignedTo: User;
@@ -92,6 +92,21 @@ export class WalletsService {
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) {
@@ -189,19 +204,4 @@ export class WalletsService {
if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND);
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;
}
}