feat: cancel/resume LS subscriptions

This commit is contained in:
Ahmed Bouhuolia
2024-08-24 20:46:30 +02:00
parent 278d61ce61
commit 67a8610328
10 changed files with 108 additions and 39 deletions

View File

@@ -5,7 +5,7 @@ import { PlanSubscription } from '@/system/models';
import { ServiceError } from '@/exceptions';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { ERRORS, IOrganizationSubscriptionCanceled } from './types';
import { ERRORS, IOrganizationSubscriptionCancelled } from './types';
@Service()
export class LemonCancelSubscription {
@@ -18,12 +18,15 @@ export class LemonCancelSubscription {
* @param {number} subscriptionId
* @returns {Promise<void>}
*/
public async cancelSubscription(tenantId: number) {
public async cancelSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
configureLemonSqueezy();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: 'main',
slug: subscriptionSlug,
});
if (!subscription) {
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
@@ -35,13 +38,10 @@ export class LemonCancelSubscription {
if (cancelledSub.error) {
throw new Error(cancelledSub.error.message);
}
await PlanSubscription.query().findById(subscriptionId).patch({
canceledAt: new Date(),
});
// Triggers `onSubscriptionCanceled` event.
// Triggers `onSubscriptionCancelled` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionCanceled,
{ tenantId, subscriptionId } as IOrganizationSubscriptionCanceled
events.subscription.onSubscriptionCancel,
{ tenantId, subscriptionId } as IOrganizationSubscriptionCancelled
);
}
}

View File

@@ -18,25 +18,30 @@ export class LemonChangeSubscriptionPlan {
* @param {number} newVariantId - New variant id.
* @returns {Promise<void>}
*/
public async changeSubscriptionPlan(tenantId: number, newVariantId: number) {
public async changeSubscriptionPlan(
tenantId: number,
newVariantId: number,
subscriptionSlug: string = 'main'
) {
configureLemonSqueezy();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: 'main',
slug: subscriptionSlug,
});
const lemonSubscriptionId = subscription.lemonSubscriptionId;
// Send request to Lemon Squeezy to change the subscription.
const updatedSub = await updateSubscription(lemonSubscriptionId, {
variantId: newVariantId,
invoiceImmediately: true,
});
if (updatedSub.error) {
throw new ServiceError('SOMETHING_WENT_WRONG');
}
// Triggers `onSubscriptionPlanChanged` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionPlanChanged,
events.subscription.onSubscriptionPlanChange,
{
tenantId,
lemonSubscriptionId,

View File

@@ -14,15 +14,16 @@ export class LemonResumeSubscription {
/**
* Resumes the main subscription of the given tenant.
* @param {number} tenantId -
* @param {number} tenantId - Tenant id.
* @param {string} subscriptionSlug - Subscription slug by default main subscription.
* @returns {Promise<void>}
*/
public async resumeSubscription(tenantId: number) {
public async resumeSubscription(tenantId: number, subscriptionSlug: string = 'main') {
configureLemonSqueezy();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: 'main',
slug: subscriptionSlug,
});
if (!subscription) {
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
@@ -33,15 +34,11 @@ export class LemonResumeSubscription {
cancelled: false,
});
if (returnedSub.error) {
throw new ServiceError('');
throw new ServiceError(ٌٌُERRORS.SOMETHING_WENT_WRONG_WITH_LS);
}
// Update the subscription of the organization.
await PlanSubscription.query().findById(subscriptionId).patch({
canceledAt: null,
});
// Triggers `onSubscriptionCanceled` event.
// Triggers `onSubscriptionResume` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionResumed,
events.subscription.onSubscriptionResume,
{ tenantId, subscriptionId } as IOrganizationSubscriptionResumed
);
}

View File

@@ -59,6 +59,7 @@ export class LemonSqueezyWebhooks {
const userId = eventBody.meta.custom_data?.user_id;
const tenantId = eventBody.meta.custom_data?.tenant_id;
const subscriptionSlug = 'main';
if (!webhookHasMeta(eventBody)) {
throw new Error("Event body is missing the 'meta' property.");
@@ -68,13 +69,13 @@ export class LemonSqueezyWebhooks {
if (webhookEvent === 'subscription_payment_success') {
await this.subscriptionService.markSubscriptionPaymentSucceed(
tenantId,
'main'
subscriptionSlug
);
// Marks the main subscription payment as failed.
} else if (webhookEvent === 'subscription_payment_failed') {
await this.subscriptionService.markSubscriptionPaymentFailed(
tenantId,
'main'
subscriptionSlug
);
}
// Save subscription invoices; eventBody is a SubscriptionInvoice
@@ -100,20 +101,25 @@ export class LemonSqueezyWebhooks {
await this.subscriptionService.newSubscribtion(
tenantId,
plan.slug,
'main',
subscriptionSlug,
{ lemonSqueezyId: subscriptionId }
);
// Cancel the given subscription of the organization.
} else if (webhookEvent === 'subscription_cancelled') {
await this.subscriptionService.cancelSubscription(
tenantId,
plan.slug
subscriptionSlug
);
} else if (webhookEvent === 'subscription_plan_changed') {
await this.subscriptionService.subscriptionPlanChanged(
tenantId,
plan.slug,
'main'
subscriptionSlug
);
} else if (webhookEvent === 'subscription_resumed') {
await this.subscriptionService.resumeSubscription(
tenantId,
subscriptionSlug
);
}
} else if (webhookEvent.startsWith('order_')) {

View File

@@ -82,6 +82,48 @@ export class Subscription {
throw new ServiceError(ERRORS.SUBSCRIPTION_ALREADY_CANCELED);
}
await subscription.$query().patch({ canceledAt: new Date() });
// Triggers `onSubscriptionCancelled` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionCancelled,
{
tenantId,
subscriptionSlug,
}
);
}
/**
* Resumes the given tenant subscription.
* @param {number} tenantId
* @param {string} subscriptionSlug - Subscription slug by deafult main subscription.
* @returns {Promise<void>}
*/
async resumeSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
const tenant = await Tenant.query().findById(tenantId).throwIfNotFound();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: subscriptionSlug,
});
// Throw error early if the subscription is not exist.
if (!subscription) {
throw new ServiceError(ERRORS.SUBSCRIPTION_NOT_EXIST);
}
// Throw error early if the subscription is not cancelled.
if (!subscription.canceled()) {
throw new ServiceError(ERRORS.SUBSCRIPTION_ALREADY_ACTIVE);
}
await subscription.$query().patch({ canceledAt: null });
// Triggers `onSubscriptionResumed` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionResumed,
{ tenantId, subscriptionSlug }
);
}
/**

View File

@@ -20,8 +20,14 @@ export class SubscriptionApplication {
* @param {string} id
* @returns {Promise<void>}
*/
public cancelSubscription(tenantId: number, id: string) {
return this.cancelSubscriptionService.cancelSubscription(tenantId, id);
public cancelSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
return this.cancelSubscriptionService.cancelSubscription(
tenantId,
subscriptionSlug
);
}
/**
@@ -29,8 +35,14 @@ export class SubscriptionApplication {
* @param {number} tenantId
* @returns {Promise<void>}
*/
public resumeSubscription(tenantId: number) {
return this.resumeSubscriptionService.resumeSubscription(tenantId);
public resumeSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
return this.resumeSubscriptionService.resumeSubscription(
tenantId,
subscriptionSlug
);
}
/**

View File

@@ -1,8 +1,8 @@
import { Inject, Service } from 'typedi';
import { IAuthSignedUpEventPayload } from '@/interfaces';
import events from '@/subscribers/events';
import config from '@/config';
import { Subscription } from '../Subscription';
import { Inject, Service } from 'typedi';
@Service()
export class SubscribeFreeOnSignupCommunity {

View File

@@ -3,6 +3,8 @@ export const ERRORS = {
'SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT',
SUBSCRIPTION_NOT_EXIST: 'SUBSCRIPTION_NOT_EXIST',
SUBSCRIPTION_ALREADY_CANCELED: 'SUBSCRIPTION_ALREADY_CANCELED',
SUBSCRIPTION_ALREADY_ACTIVE: 'SUBSCRIPTION_ALREADY_ACTIVE',
SOMETHING_WENT_WRONG_WITH_LS: 'SOMETHING_WENT_WRONG_WITH_LS'
};
export interface IOrganizationSubscriptionChanged {
@@ -11,7 +13,7 @@ export interface IOrganizationSubscriptionChanged {
newVariantId: number;
}
export interface IOrganizationSubscriptionCanceled {
export interface IOrganizationSubscriptionCancelled {
tenantId: number;
subscriptionId: string;
}