fix : extend and upgrade user subscription

This commit is contained in:
2026-02-01 11:54:51 +03:30
parent 3a80ce021d
commit 0ba027d4e3
7 changed files with 110 additions and 56 deletions
@@ -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" })