refactor: the invoice and subscription flow of user
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user