feat: implement recurring invoice system for subscription renewals

- Add SUBSCRIPTION_RENEWAL_JOB_NAME and related constants for renewal jobs
- Implement createSubscriptionRenewalInvoice in invoice processor
- Add automatic renewal invoice creation 7 days before subscription expiry
- Handle subscription renewals in payInvoice method with period extension
- Add scheduleNextRenewalJob method for continuous renewal scheduling
- Prevent duplicate renewal invoices and jobs
- Skip renewal jobs for free plans
- Send notifications for renewal opportunities
- Fix duplicate job scheduling issue by removing from subscribeToPlan

Features:
- Automatic renewal invoice creation before expiry
- Subscription period extension on renewal payment
- Continuous renewal cycle scheduling
- Comprehensive error handling and validation
- Integration with existing notification system
This commit is contained in:
mahyargdz
2025-08-17 10:00:12 +03:30
parent ca0510f2c2
commit 72b1edbe25
4 changed files with 144 additions and 7 deletions
@@ -564,7 +564,19 @@ export class InvoicesService {
if (invoice.items[0]?.subscriptionPlan && !invoice.isExternal) {
const userSubscription = invoice.items[0].subscriptionPlan;
userSubscription.status = SubscriptionStatus.ACTIVE;
const isRenewal = userSubscription.status === SubscriptionStatus.ACTIVE;
if (isRenewal) {
const newEndDate = dayjs(userSubscription.endDate).add(userSubscription.plan.duration, "day").toDate();
userSubscription.endDate = newEndDate;
this.logger.log(`Subscription ${userSubscription.id} renewed until ${newEndDate}`);
await this.scheduleNextRenewalJob(userSubscription);
} else {
userSubscription.status = SubscriptionStatus.ACTIVE;
await this.scheduleNextRenewalJob(userSubscription);
}
//
await queryRunner.manager.save(UserSubscription, userSubscription);
@@ -1023,4 +1035,37 @@ export class InvoicesService {
this.logger.log(`Calculated recurring delay: ${delayMs}ms for period: ${recurringPeriod}`);
return delayMs;
}
//*********************************** */
private async scheduleNextRenewalJob(userSubscription: UserSubscription) {
if (userSubscription.plan.isFree) {
this.logger.debug(`Skipping renewal job for free plan subscription ${userSubscription.id}`);
return;
}
const renewalJobDate = dayjs(userSubscription.endDate).subtract(INVOICE.SUBSCRIPTION_RENEWAL_DAYS_BEFORE_EXPIRY, "day");
const delay = renewalJobDate.diff(dayjs());
if (delay <= 0) {
this.logger.warn(`Subscription ${userSubscription.id} expires too soon to schedule renewal job`);
return;
}
this.logger.debug(
`Scheduling renewal job for subscription ${userSubscription.id} to run on ${renewalJobDate.format("YYYY-MM-DD HH:mm:ss")}`,
);
await this.invoiceQueue.add(
INVOICE.SUBSCRIPTION_RENEWAL_JOB_NAME,
{
userSubscriptionId: userSubscription.id,
},
{
delay,
attempts: INVOICE.SUBSCRIPTION_RENEWAL_JOB_ATTEMPTS,
backoff: { type: "exponential", delay: INVOICE.SUBSCRIPTION_RENEWAL_JOB_BACKOFF },
priority: INVOICE.SUBSCRIPTION_RENEWAL_JOB_PRIORITY,
},
);
}
}