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:
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user