From 0ba027d4e3094c2cd05da204a5a866d98e3a5090 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sun, 1 Feb 2026 11:54:51 +0330 Subject: [PATCH] fix : extend and upgrade user subscription --- .../dmenu/providers/restaurant.service.ts | 8 +- .../invoices/entities/invoice.entity.ts | 9 ++ .../interfaces/external-invoice.interface.ts | 7 ++ .../invoices/providers/invoices.service.ts | 95 +++++++++++++------ .../invoices/queue/invoice.processor.ts | 4 +- .../providers/subscriptions.service.ts | 41 ++++---- .../subscriptions/subscriptions.controller.ts | 2 +- 7 files changed, 110 insertions(+), 56 deletions(-) diff --git a/src/modules/dmenu/providers/restaurant.service.ts b/src/modules/dmenu/providers/restaurant.service.ts index 676a98c..938a259 100644 --- a/src/modules/dmenu/providers/restaurant.service.ts +++ b/src/modules/dmenu/providers/restaurant.service.ts @@ -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(), }) diff --git a/src/modules/invoices/entities/invoice.entity.ts b/src/modules/invoices/entities/invoice.entity.ts index 6740fb7..c878bc4 100755 --- a/src/modules/invoices/entities/invoice.entity.ts +++ b/src/modules/invoices/entities/invoice.entity.ts @@ -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; } + } diff --git a/src/modules/invoices/interfaces/external-invoice.interface.ts b/src/modules/invoices/interfaces/external-invoice.interface.ts index 5af7e1f..4acbd19 100644 --- a/src/modules/invoices/interfaces/external-invoice.interface.ts +++ b/src/modules/invoices/interfaces/external-invoice.interface.ts @@ -9,3 +9,10 @@ export interface IExternalInvoiceJob { timestamp: string; // callbackData?: Record; } + + +export enum InvoicePurpose { + NEW = 'new', + RENEW = 'renew', + UPGRADE = 'upgrade', +} diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 4058ad5..1ea4985 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -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) { diff --git a/src/modules/invoices/queue/invoice.processor.ts b/src/modules/invoices/queue/invoice.processor.ts index 54f7cf7..528ebae 100755 --- a/src/modules/invoices/queue/invoice.processor.ts +++ b/src/modules/invoices/queue/invoice.processor.ts @@ -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}`); diff --git a/src/modules/subscriptions/providers/subscriptions.service.ts b/src/modules/subscriptions/providers/subscriptions.service.ts index 1612f50..85515aa 100755 --- a/src/modules/subscriptions/providers/subscriptions.service.ts +++ b/src/modules/subscriptions/providers/subscriptions.service.ts @@ -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( diff --git a/src/modules/subscriptions/subscriptions.controller.ts b/src/modules/subscriptions/subscriptions.controller.ts index 59672d4..ed7f816 100755 --- a/src/modules/subscriptions/subscriptions.controller.ts +++ b/src/modules/subscriptions/subscriptions.controller.ts @@ -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" })