fix : extend and upgrade user subscription
This commit is contained in:
@@ -200,6 +200,7 @@ export class RestaurantService {
|
||||
|
||||
async getRestaurantSubscription(subscriptionId: string) {
|
||||
try {
|
||||
console.log('subscriptionId', subscriptionId)
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.get(`${this.config.baseUrl}/super-admin/restaurants/subscription/${subscriptionId}`, {
|
||||
@@ -212,6 +213,7 @@ export class RestaurantService {
|
||||
}),
|
||||
),
|
||||
);
|
||||
console.log('data', data)
|
||||
return data;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError && error.response) {
|
||||
@@ -378,13 +380,13 @@ export class RestaurantService {
|
||||
}
|
||||
}
|
||||
|
||||
async upgradeSubscription(subscriptionId: string, newPlan: string, subscriptionEndDate: string) {
|
||||
async upgradeSubscription(subscriptionId: string, newPlan: 'base' | 'premium', subscriptionEndDate: Date) {
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.patch(`${this.config.baseUrl}/super-admin/restaurants/subscription/${subscriptionId}/upgrade`, {
|
||||
plan: newPlan,
|
||||
subscriptionEndDate,
|
||||
newPlan,
|
||||
subscriptionEndDate
|
||||
}, {
|
||||
headers: this.getHeaders(),
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Discount } from "../../discounts/entities/discount.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { RecurringPeriodEnum } from "../enums/invoice-recurring-period.enum";
|
||||
import { InvoiceStatus } from "../enums/invoice-status.enum";
|
||||
import { InvoicePurpose } from "../interfaces/external-invoice.interface";
|
||||
|
||||
@Entity()
|
||||
export class Invoice extends BaseEntity {
|
||||
@@ -66,7 +67,15 @@ export class Invoice extends BaseEntity {
|
||||
@ManyToOne(() => Discount, (discount) => discount.invoices, { nullable: true, onDelete: "RESTRICT" })
|
||||
discount: Discount | null;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: InvoicePurpose,
|
||||
nullable: false,
|
||||
})
|
||||
purpose: InvoicePurpose;
|
||||
|
||||
get isOverdue(): boolean {
|
||||
return this.status === InvoiceStatus.PENDING && new Date() > this.dueDate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,3 +9,10 @@ export interface IExternalInvoiceJob {
|
||||
timestamp: string;
|
||||
// callbackData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
export enum InvoicePurpose {
|
||||
NEW = 'new',
|
||||
RENEW = 'renew',
|
||||
UPGRADE = 'upgrade',
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import { UpdateInvoiceDto } from "../DTO/update-invoice.dto";
|
||||
import { Invoice } from "../entities/invoice.entity";
|
||||
import { RecurringPeriodEnum } from "../enums/invoice-recurring-period.enum";
|
||||
import { InvoiceStatus } from "../enums/invoice-status.enum";
|
||||
import { IExternalInvoiceJob } from "../interfaces/external-invoice.interface";
|
||||
import { IExternalInvoiceJob, InvoicePurpose } from "../interfaces/external-invoice.interface";
|
||||
import { InvoiceItemsRepository } from "../repositories/invoice-items.repository";
|
||||
import { InvoicesRepository } from "../repositories/invoices.repository";
|
||||
@Injectable()
|
||||
@@ -55,7 +55,7 @@ export class InvoicesService {
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly accessLogService: AccessLogService,
|
||||
private readonly restaurantService: RestaurantService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
///********************************** */
|
||||
|
||||
@@ -489,7 +489,9 @@ 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, purpose: InvoicePurpose) {
|
||||
|
||||
const discount = plan.directDiscount;
|
||||
const originalPrice = plan.originalPrice || plan.price;
|
||||
const finalPrice = plan.price;
|
||||
@@ -500,7 +502,7 @@ export class InvoicesService {
|
||||
unitPrice: originalPrice,
|
||||
discount: discount ? new Decimal(originalPrice).sub(finalPrice).toNumber() : 0,
|
||||
subscriptionPlan: userSub,
|
||||
plan: plan,
|
||||
plan,
|
||||
totalPrice: finalPrice,
|
||||
};
|
||||
|
||||
@@ -516,6 +518,7 @@ export class InvoicesService {
|
||||
dueDate,
|
||||
items: [invoiceItem],
|
||||
discount: discount || undefined,
|
||||
purpose
|
||||
});
|
||||
|
||||
await qryRnr.manager.save(Invoice, invoice);
|
||||
@@ -545,33 +548,33 @@ export class InvoicesService {
|
||||
}
|
||||
|
||||
//********************************** */
|
||||
async createInvoiceForSubscriptionUpgrade(user: User, plan: SubscriptionPlan, userSub: UserSubscription, dueDate: Date, qryRnr: QueryRunner, upgradeCost?: Decimal) {
|
||||
async createInvoiceForSubscriptionUpgrade(user: User, plan: SubscriptionPlan, userSub: UserSubscription, dueDate: Date, qryRnr: QueryRunner, upgradeCost: number) {
|
||||
const discount = plan.directDiscount;
|
||||
const originalPrice = upgradeCost || (plan.originalPrice || plan.price);
|
||||
const finalPrice = upgradeCost || plan.price;
|
||||
|
||||
|
||||
const invoiceItem = {
|
||||
name: `${plan.service.name} - Upgrade`,
|
||||
count: 1,
|
||||
unitPrice: originalPrice,
|
||||
discount: discount ? new Decimal(originalPrice).sub(finalPrice).toNumber() : 0,
|
||||
unitPrice: upgradeCost,
|
||||
discount: 0,
|
||||
subscriptionPlan: userSub,
|
||||
plan: plan,
|
||||
totalPrice: finalPrice,
|
||||
totalPrice: upgradeCost,
|
||||
};
|
||||
|
||||
const taxAmount = new Decimal(finalPrice).mul(0.1);
|
||||
const totalPrice = new Decimal(finalPrice).add(taxAmount);
|
||||
const taxAmount = new Decimal(upgradeCost).mul(0.1);
|
||||
const totalPrice = new Decimal(upgradeCost).add(taxAmount);
|
||||
|
||||
const invoice = qryRnr.manager.create(Invoice, {
|
||||
user,
|
||||
totalPrice: totalPrice,
|
||||
originalPrice: new Decimal(originalPrice).add(new Decimal(originalPrice).mul(0.1)),
|
||||
originalPrice: new Decimal(upgradeCost).add(new Decimal(upgradeCost).mul(0.1)),
|
||||
tax: taxAmount.toNumber(),
|
||||
status: InvoiceStatus.WAIT_PAYMENT,
|
||||
dueDate,
|
||||
items: [invoiceItem],
|
||||
discount: discount || undefined,
|
||||
purpose: InvoicePurpose.UPGRADE
|
||||
});
|
||||
|
||||
await qryRnr.manager.save(Invoice, invoice);
|
||||
@@ -798,15 +801,19 @@ export class InvoicesService {
|
||||
const invoiceItem = invoice.items[0];
|
||||
|
||||
// Check if this is an upgrade (subscription is ACTIVE and plan in invoice differs from current plan)
|
||||
const isUpgrade = userSubscription.status === SubscriptionStatus.ACTIVE &&
|
||||
invoiceItem.plan &&
|
||||
invoiceItem.plan.id !== userSubscription.plan.id;
|
||||
const purpose = invoice.purpose
|
||||
|
||||
if (isUpgrade) {
|
||||
|
||||
if (purpose == InvoicePurpose.UPGRADE) {
|
||||
// This is an upgrade - update subscription with the new plan
|
||||
const upgradePlan = invoiceItem.plan!;
|
||||
|
||||
userSubscription.plan = upgradePlan;
|
||||
const diffrence = upgradePlan.duration - userSubscription.plan.duration
|
||||
|
||||
if (diffrence > 0) {
|
||||
userSubscription.endDate = dayjs(userSubscription.endDate).add(diffrence, "day").toDate();
|
||||
}
|
||||
this.logger.log(`Subscription ${userSubscription.id} upgraded to plan ${upgradePlan.name}`);
|
||||
await this.scheduleNextRenewalJob(userSubscription);
|
||||
|
||||
@@ -818,7 +825,7 @@ export class InvoicesService {
|
||||
await this.restaurantService.upgradeSubscription(
|
||||
userSubscription.id,
|
||||
dmenuPlan,
|
||||
userSubscription.endDate.toISOString()
|
||||
userSubscription.endDate
|
||||
);
|
||||
this.logger.log(`External API call completed for dmenu upgrade: subscription ${userSubscription.id}`);
|
||||
} catch (error) {
|
||||
@@ -826,22 +833,46 @@ export class InvoicesService {
|
||||
// Don't fail the transaction if external API call fails
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular subscription logic (renewal or activation)
|
||||
const isRenewal = userSubscription.status === SubscriptionStatus.ACTIVE;
|
||||
} else if (purpose == InvoicePurpose.RENEW) {
|
||||
|
||||
if (isRenewal) {
|
||||
const newEndDate = dayjs(userSubscription.endDate).add(userSubscription.plan.duration, "day").toDate();
|
||||
userSubscription.endDate = newEndDate;
|
||||
if (userSubscription.status == SubscriptionStatus.ACTIVE) {
|
||||
userSubscription.endDate = dayjs(userSubscription.endDate).add(userSubscription.plan.duration, "day").toDate();
|
||||
|
||||
this.logger.log(`Subscription ${userSubscription.id} renewed until ${newEndDate}`);
|
||||
await this.scheduleNextRenewalJob(userSubscription);
|
||||
} else {
|
||||
userSubscription.status = SubscriptionStatus.ACTIVE;
|
||||
await this.scheduleNextRenewalJob(userSubscription);
|
||||
userSubscription.endDate = dayjs().add(userSubscription.plan.duration, "day").toDate();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoiceItem.plan) {
|
||||
userSubscription.plan = invoiceItem.plan
|
||||
}
|
||||
|
||||
this.logger.log(`Subscription ${userSubscription.id} renewed until ${userSubscription.endDate}`);
|
||||
await this.scheduleNextRenewalJob(userSubscription);
|
||||
|
||||
// Call external API for dmenu service upgrades
|
||||
if (userSubscription.plan.service.name.toLowerCase().includes('dmenu') || userSubscription.plan.service.slug?.toLowerCase().includes('dmenu')) {
|
||||
try {
|
||||
this.logger.log(`Calling external API for dmenu upgrade: subscription ${userSubscription.id}`);
|
||||
const dmenuPlan = userSubscription.plan.name.includes("دلیوری") ? "premium" : "base";
|
||||
await this.restaurantService.upgradeSubscription(
|
||||
userSubscription.id,
|
||||
dmenuPlan,
|
||||
userSubscription.endDate
|
||||
);
|
||||
this.logger.log(`External API call completed for dmenu upgrade: subscription ${userSubscription.id}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to call external API for dmenu upgrade: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
// Don't fail the transaction if external API call fails
|
||||
}
|
||||
}
|
||||
|
||||
} else if (purpose == InvoicePurpose.NEW) {
|
||||
userSubscription.status = SubscriptionStatus.ACTIVE;
|
||||
await this.scheduleNextRenewalJob(userSubscription);
|
||||
this.logger.log(`Subscription ${userSubscription.id} activated `);
|
||||
|
||||
}
|
||||
// if serivce is dmenu for renew or upgrade it must call dmenu api
|
||||
//
|
||||
await queryRunner.manager.save(UserSubscription, userSubscription);
|
||||
await this.walletsService.createSubscriptionTransaction(invoice.totalPrice, userWallet.id, queryRunner);
|
||||
@@ -942,7 +973,9 @@ export class InvoicesService {
|
||||
subscriptionPlan: {
|
||||
plan: true,
|
||||
},
|
||||
plan: true,
|
||||
plan: {
|
||||
service: true
|
||||
},
|
||||
supportPlan: {
|
||||
supportPlan: true,
|
||||
},
|
||||
@@ -1144,7 +1177,7 @@ export class InvoicesService {
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
private async getInvoiceByIdWithQueryRunner(invoiceId: string, userId: string, queryRunner: QueryRunner) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { INVOICE } from "../constants";
|
||||
import { Invoice } from "../entities/invoice.entity";
|
||||
import { InvoiceStatus } from "../enums/invoice-status.enum";
|
||||
import { InvoicesService } from "../providers/invoices.service";
|
||||
import { InvoicePurpose } from "../interfaces/external-invoice.interface";
|
||||
@Processor(INVOICE.QUEUE_NAME, { concurrency: 2 })
|
||||
export class InvoiceProcessor extends WorkerProcessor {
|
||||
constructor(
|
||||
@@ -136,7 +137,7 @@ export class InvoiceProcessor extends WorkerProcessor {
|
||||
invoice.currentRecurringCycle += 1;
|
||||
await queryRunner.manager.save(Invoice, invoice);
|
||||
|
||||
return this.invoicesService.createInvoiceForSubscription(invoice.user, plan, userSubscriptionPlan, dueDate, queryRunner);
|
||||
return this.invoicesService.createInvoiceForSubscription(invoice.user, plan, userSubscriptionPlan, dueDate, queryRunner,InvoicePurpose.NEW);
|
||||
}
|
||||
//********************************** */
|
||||
|
||||
@@ -199,6 +200,7 @@ export class InvoiceProcessor extends WorkerProcessor {
|
||||
userSubscription,
|
||||
renewalDueDate,
|
||||
queryRunner,
|
||||
InvoicePurpose.RENEW
|
||||
);
|
||||
|
||||
this.logger.log(`Renewal invoice created successfully for subscription ${userSubscriptionId} with id ${renewalInvoice.id}`);
|
||||
|
||||
@@ -29,6 +29,7 @@ import { SubscriptionStatus } from "../enums/subscription-status.enum";
|
||||
import { IBusinessProvisioningJob } from "../interfaces/IProvisioning-job-data";
|
||||
import { SubscriptionsPlanRepository } from "../repositories/subscriptions.repository";
|
||||
import { UserSubscriptionsRepository } from "../repositories/user-subscriptions.repository";
|
||||
import { InvoicePurpose } from "../../invoices/interfaces/external-invoice.interface";
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionsService {
|
||||
@@ -308,7 +309,7 @@ export class SubscriptionsService {
|
||||
await queryRunner.manager.save(UserSubscription, userSubscription);
|
||||
const invoiceDueDate = dayjs(userSubscription.startDate).add(INVOICE.DUEDATE, "day").toDate();
|
||||
|
||||
const invoice = await this.invoicesService.createInvoiceForSubscription(user, plan, userSubscription, invoiceDueDate, queryRunner);
|
||||
const invoice = await this.invoicesService.createInvoiceForSubscription(user, plan, userSubscription, invoiceDueDate, queryRunner,InvoicePurpose.NEW);
|
||||
userSubscription.status = SubscriptionStatus.INACTIVE;
|
||||
|
||||
await queryRunner.manager.save(UserSubscription, userSubscription);
|
||||
@@ -334,7 +335,7 @@ export class SubscriptionsService {
|
||||
}
|
||||
//************************************ */
|
||||
|
||||
async extendSubscribeToPlan(userSubscriptionId: string, dto: ExtendUSerSubscribeServiceDto, userId: string) {
|
||||
async renewUserSubscription(userSubscriptionId: string, dto: ExtendUSerSubscribeServiceDto, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
@@ -366,11 +367,11 @@ export class SubscriptionsService {
|
||||
|
||||
const invoiceDueDate = dayjs().add(INVOICE.DUEDATE, "day").toDate();
|
||||
|
||||
const invoice = await this.invoicesService.createInvoiceForSubscription(user, plan, userSubscription, invoiceDueDate, queryRunner);
|
||||
const invoice = await this.invoicesService.createInvoiceForSubscription(user, plan, userSubscription, invoiceDueDate, queryRunner,InvoicePurpose.RENEW);
|
||||
|
||||
userSubscription.plan = plan;
|
||||
// userSubscription.plan = plan;
|
||||
|
||||
await queryRunner.manager.save(userSubscription);
|
||||
// await queryRunner.manager.save(userSubscription);
|
||||
|
||||
await this.addProvisioningJob(userSubscription, plan, user);
|
||||
|
||||
@@ -415,28 +416,28 @@ export class SubscriptionsService {
|
||||
const newPlan = await this.subscriptionsPlanRepository.findById(planId);
|
||||
if (!newPlan) throw new BadRequestException(SubscriptionMessage.PLAN_NOT_FOUND);
|
||||
|
||||
console.log('new plan', newPlan.service.id)
|
||||
console.log('userSubscription.plan.service.id',userSubscription.plan.service.id)
|
||||
|
||||
if (userSubscription.plan.service.id !== newPlan.service.id) {
|
||||
throw new BadRequestException(SubscriptionMessage.SERVICE_MISMATCH);
|
||||
}
|
||||
|
||||
// Validate that new plan is more expensive than current plan
|
||||
if (new Decimal(newPlan.price).lessThanOrEqualTo(userSubscription.plan.price)) {
|
||||
if (new Decimal(newPlan.price).lessThan(userSubscription.plan.price)) {
|
||||
throw new BadRequestException(SubscriptionMessage.UPGRADE_TO_LOWER_OR_EQUAL_PLAN_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
// Calculate remaining days in current subscription
|
||||
const today = dayjs();
|
||||
const endDate = dayjs(userSubscription.endDate);
|
||||
const remainingDays = Math.max(0, endDate.diff(today, 'day'));
|
||||
const currentPlanRemainingDays = Math.max(0, endDate.diff(today, 'day'));
|
||||
console.log('🔢 CALCULATION LOGS:');
|
||||
console.log(`📅 Remaining days: ${remainingDays} (endDate: ${endDate.format('YYYY-MM-DD')}, today: ${today.format('YYYY-MM-DD')})`);
|
||||
console.log(`📅 Remaining days: ${currentPlanRemainingDays} (endDate: ${endDate.format('YYYY-MM-DD')}, today: ${today.format('YYYY-MM-DD')})`);
|
||||
|
||||
if (remainingDays <= 0) {
|
||||
if (currentPlanRemainingDays <= 0) {
|
||||
throw new BadRequestException(SubscriptionMessage.SUBSCRIPTION_EXPIRED_CANNOT_UPGRADE);
|
||||
}
|
||||
|
||||
const remainingDays = newPlan.duration - currentPlanRemainingDays
|
||||
// Calculate daily rates
|
||||
const currentPlanDuration = userSubscription.plan.duration;
|
||||
const newPlanDuration = newPlan.duration;
|
||||
@@ -451,13 +452,13 @@ export class SubscriptionsService {
|
||||
console.log(`📈 Daily price difference: ${dailyPriceDifference.toNumber()} IRR/day`);
|
||||
|
||||
// Calculate prorated upgrade cost for remaining days
|
||||
const proratedUpgradeCost = dailyPriceDifference.mul(remainingDays);
|
||||
console.log(`🧮 Prorated upgrade cost: ${dailyPriceDifference.toNumber()} IRR/day × ${remainingDays} days = ${proratedUpgradeCost.toNumber()} IRR`);
|
||||
const proratedUpgradeCost = dailyPriceDifference.mul(currentPlanRemainingDays).add(newDailyRate.mul(remainingDays))
|
||||
console.log(`🧮 Prorated upgrade cost: ${dailyPriceDifference.toNumber()} IRR/day × ${currentPlanRemainingDays} days = ${proratedUpgradeCost.toNumber()} IRR`);
|
||||
|
||||
// Ensure minimum charge amount
|
||||
const minimumCharge = new Decimal(1000); // 1000 IRR minimum
|
||||
const finalUpgradeCost = Decimal.max(proratedUpgradeCost, minimumCharge);
|
||||
console.log(`✅ Final upgrade cost: max(${proratedUpgradeCost.toNumber()} IRR, ${minimumCharge.toNumber()} IRR minimum) = ${finalUpgradeCost.toNumber()} IRR`);
|
||||
const finalUpgradeCost = + Decimal.max(proratedUpgradeCost, minimumCharge).toNumber().toFixed(0)
|
||||
console.log(`✅ Final upgrade cost: max(${proratedUpgradeCost.toNumber()} IRR, ${minimumCharge.toNumber()} IRR minimum) = ${finalUpgradeCost} IRR`);
|
||||
|
||||
const user = await this.usersService.findOneByIdWithQueryRunner(userSubscription.user.id, queryRunner);
|
||||
|
||||
@@ -473,8 +474,8 @@ export class SubscriptionsService {
|
||||
);
|
||||
|
||||
// Update the subscription to the new plan
|
||||
userSubscription.plan = newPlan;
|
||||
await queryRunner.manager.save(UserSubscription, userSubscription);
|
||||
// userSubscription.plan = newPlan;
|
||||
// await queryRunner.manager.save(UserSubscription, userSubscription);
|
||||
|
||||
// Add provisioning job for the new plan
|
||||
await this.addProvisioningJob(userSubscription, newPlan, user);
|
||||
@@ -485,10 +486,10 @@ export class SubscriptionsService {
|
||||
userSubscription,
|
||||
invoice,
|
||||
upgradeDetails: {
|
||||
remainingDays,
|
||||
remainingDays: currentPlanRemainingDays,
|
||||
currentPlanPrice: userSubscription.plan.price,
|
||||
newPlanPrice: newPlan.price,
|
||||
proratedUpgradeCost: finalUpgradeCost.toNumber(),
|
||||
proratedUpgradeCost: finalUpgradeCost,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -639,7 +640,7 @@ export class SubscriptionsService {
|
||||
.leftJoinAndSelect("userSubscription.user", "user")
|
||||
.leftJoinAndSelect("userSubscription.plan", "plan")
|
||||
.leftJoin("plan.service", "service")
|
||||
.addSelect(["service.name", "service.title"]);
|
||||
.addSelect(["service.name", "service.title"]);
|
||||
|
||||
if (queryDto.q) {
|
||||
queryBuilder.andWhere(
|
||||
|
||||
@@ -89,7 +89,7 @@ export class SubscriptionsController {
|
||||
@Body() subscribeDto: ExtendUSerSubscribeServiceDto,
|
||||
@UserDec("id") userId: string,
|
||||
) {
|
||||
return this.subscriptionService.extendSubscribeToPlan(paramDto.userSubscriptionId, subscribeDto, userId);
|
||||
return this.subscriptionService.renewUserSubscription(paramDto.userSubscriptionId, subscribeDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Upgrade subscription plan ==> user route" })
|
||||
|
||||
Reference in New Issue
Block a user