mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
feat: cancel/resume LS subscriptions
This commit is contained in:
@@ -113,7 +113,7 @@ export class SubscriptionController extends BaseController {
|
|||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.subscriptionApp.cancelSubscription(tenantId, '455610');
|
await this.subscriptionApp.cancelSubscription(tenantId);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { PlanSubscription } from '@/system/models';
|
|||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import { ERRORS, IOrganizationSubscriptionCanceled } from './types';
|
import { ERRORS, IOrganizationSubscriptionCancelled } from './types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class LemonCancelSubscription {
|
export class LemonCancelSubscription {
|
||||||
@@ -18,12 +18,15 @@ export class LemonCancelSubscription {
|
|||||||
* @param {number} subscriptionId
|
* @param {number} subscriptionId
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async cancelSubscription(tenantId: number) {
|
public async cancelSubscription(
|
||||||
|
tenantId: number,
|
||||||
|
subscriptionSlug: string = 'main'
|
||||||
|
) {
|
||||||
configureLemonSqueezy();
|
configureLemonSqueezy();
|
||||||
|
|
||||||
const subscription = await PlanSubscription.query().findOne({
|
const subscription = await PlanSubscription.query().findOne({
|
||||||
tenantId,
|
tenantId,
|
||||||
slug: 'main',
|
slug: subscriptionSlug,
|
||||||
});
|
});
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
|
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
|
||||||
@@ -35,13 +38,10 @@ export class LemonCancelSubscription {
|
|||||||
if (cancelledSub.error) {
|
if (cancelledSub.error) {
|
||||||
throw new Error(cancelledSub.error.message);
|
throw new Error(cancelledSub.error.message);
|
||||||
}
|
}
|
||||||
await PlanSubscription.query().findById(subscriptionId).patch({
|
// Triggers `onSubscriptionCancelled` event.
|
||||||
canceledAt: new Date(),
|
|
||||||
});
|
|
||||||
// Triggers `onSubscriptionCanceled` event.
|
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.subscription.onSubscriptionCanceled,
|
events.subscription.onSubscriptionCancel,
|
||||||
{ tenantId, subscriptionId } as IOrganizationSubscriptionCanceled
|
{ tenantId, subscriptionId } as IOrganizationSubscriptionCancelled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,25 +18,30 @@ export class LemonChangeSubscriptionPlan {
|
|||||||
* @param {number} newVariantId - New variant id.
|
* @param {number} newVariantId - New variant id.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async changeSubscriptionPlan(tenantId: number, newVariantId: number) {
|
public async changeSubscriptionPlan(
|
||||||
|
tenantId: number,
|
||||||
|
newVariantId: number,
|
||||||
|
subscriptionSlug: string = 'main'
|
||||||
|
) {
|
||||||
configureLemonSqueezy();
|
configureLemonSqueezy();
|
||||||
|
|
||||||
const subscription = await PlanSubscription.query().findOne({
|
const subscription = await PlanSubscription.query().findOne({
|
||||||
tenantId,
|
tenantId,
|
||||||
slug: 'main',
|
slug: subscriptionSlug,
|
||||||
});
|
});
|
||||||
const lemonSubscriptionId = subscription.lemonSubscriptionId;
|
const lemonSubscriptionId = subscription.lemonSubscriptionId;
|
||||||
|
|
||||||
// Send request to Lemon Squeezy to change the subscription.
|
// Send request to Lemon Squeezy to change the subscription.
|
||||||
const updatedSub = await updateSubscription(lemonSubscriptionId, {
|
const updatedSub = await updateSubscription(lemonSubscriptionId, {
|
||||||
variantId: newVariantId,
|
variantId: newVariantId,
|
||||||
|
invoiceImmediately: true,
|
||||||
});
|
});
|
||||||
if (updatedSub.error) {
|
if (updatedSub.error) {
|
||||||
throw new ServiceError('SOMETHING_WENT_WRONG');
|
throw new ServiceError('SOMETHING_WENT_WRONG');
|
||||||
}
|
}
|
||||||
// Triggers `onSubscriptionPlanChanged` event.
|
// Triggers `onSubscriptionPlanChanged` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.subscription.onSubscriptionPlanChanged,
|
events.subscription.onSubscriptionPlanChange,
|
||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
lemonSubscriptionId,
|
lemonSubscriptionId,
|
||||||
|
|||||||
@@ -14,15 +14,16 @@ export class LemonResumeSubscription {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resumes the main subscription of the given tenant.
|
* 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>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async resumeSubscription(tenantId: number) {
|
public async resumeSubscription(tenantId: number, subscriptionSlug: string = 'main') {
|
||||||
configureLemonSqueezy();
|
configureLemonSqueezy();
|
||||||
|
|
||||||
const subscription = await PlanSubscription.query().findOne({
|
const subscription = await PlanSubscription.query().findOne({
|
||||||
tenantId,
|
tenantId,
|
||||||
slug: 'main',
|
slug: subscriptionSlug,
|
||||||
});
|
});
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
|
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
|
||||||
@@ -33,15 +34,11 @@ export class LemonResumeSubscription {
|
|||||||
cancelled: false,
|
cancelled: false,
|
||||||
});
|
});
|
||||||
if (returnedSub.error) {
|
if (returnedSub.error) {
|
||||||
throw new ServiceError('');
|
throw new ServiceError(ٌٌُERRORS.SOMETHING_WENT_WRONG_WITH_LS);
|
||||||
}
|
}
|
||||||
// Update the subscription of the organization.
|
// Triggers `onSubscriptionResume` event.
|
||||||
await PlanSubscription.query().findById(subscriptionId).patch({
|
|
||||||
canceledAt: null,
|
|
||||||
});
|
|
||||||
// Triggers `onSubscriptionCanceled` event.
|
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.subscription.onSubscriptionResumed,
|
events.subscription.onSubscriptionResume,
|
||||||
{ tenantId, subscriptionId } as IOrganizationSubscriptionResumed
|
{ tenantId, subscriptionId } as IOrganizationSubscriptionResumed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export class LemonSqueezyWebhooks {
|
|||||||
|
|
||||||
const userId = eventBody.meta.custom_data?.user_id;
|
const userId = eventBody.meta.custom_data?.user_id;
|
||||||
const tenantId = eventBody.meta.custom_data?.tenant_id;
|
const tenantId = eventBody.meta.custom_data?.tenant_id;
|
||||||
|
const subscriptionSlug = 'main';
|
||||||
|
|
||||||
if (!webhookHasMeta(eventBody)) {
|
if (!webhookHasMeta(eventBody)) {
|
||||||
throw new Error("Event body is missing the 'meta' property.");
|
throw new Error("Event body is missing the 'meta' property.");
|
||||||
@@ -68,13 +69,13 @@ export class LemonSqueezyWebhooks {
|
|||||||
if (webhookEvent === 'subscription_payment_success') {
|
if (webhookEvent === 'subscription_payment_success') {
|
||||||
await this.subscriptionService.markSubscriptionPaymentSucceed(
|
await this.subscriptionService.markSubscriptionPaymentSucceed(
|
||||||
tenantId,
|
tenantId,
|
||||||
'main'
|
subscriptionSlug
|
||||||
);
|
);
|
||||||
// Marks the main subscription payment as failed.
|
// Marks the main subscription payment as failed.
|
||||||
} else if (webhookEvent === 'subscription_payment_failed') {
|
} else if (webhookEvent === 'subscription_payment_failed') {
|
||||||
await this.subscriptionService.markSubscriptionPaymentFailed(
|
await this.subscriptionService.markSubscriptionPaymentFailed(
|
||||||
tenantId,
|
tenantId,
|
||||||
'main'
|
subscriptionSlug
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Save subscription invoices; eventBody is a SubscriptionInvoice
|
// Save subscription invoices; eventBody is a SubscriptionInvoice
|
||||||
@@ -100,20 +101,25 @@ export class LemonSqueezyWebhooks {
|
|||||||
await this.subscriptionService.newSubscribtion(
|
await this.subscriptionService.newSubscribtion(
|
||||||
tenantId,
|
tenantId,
|
||||||
plan.slug,
|
plan.slug,
|
||||||
'main',
|
subscriptionSlug,
|
||||||
{ lemonSqueezyId: subscriptionId }
|
{ lemonSqueezyId: subscriptionId }
|
||||||
);
|
);
|
||||||
// Cancel the given subscription of the organization.
|
// Cancel the given subscription of the organization.
|
||||||
} else if (webhookEvent === 'subscription_cancelled') {
|
} else if (webhookEvent === 'subscription_cancelled') {
|
||||||
await this.subscriptionService.cancelSubscription(
|
await this.subscriptionService.cancelSubscription(
|
||||||
tenantId,
|
tenantId,
|
||||||
plan.slug
|
subscriptionSlug
|
||||||
);
|
);
|
||||||
} else if (webhookEvent === 'subscription_plan_changed') {
|
} else if (webhookEvent === 'subscription_plan_changed') {
|
||||||
await this.subscriptionService.subscriptionPlanChanged(
|
await this.subscriptionService.subscriptionPlanChanged(
|
||||||
tenantId,
|
tenantId,
|
||||||
plan.slug,
|
plan.slug,
|
||||||
'main'
|
subscriptionSlug
|
||||||
|
);
|
||||||
|
} else if (webhookEvent === 'subscription_resumed') {
|
||||||
|
await this.subscriptionService.resumeSubscription(
|
||||||
|
tenantId,
|
||||||
|
subscriptionSlug
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (webhookEvent.startsWith('order_')) {
|
} else if (webhookEvent.startsWith('order_')) {
|
||||||
|
|||||||
@@ -82,6 +82,48 @@ export class Subscription {
|
|||||||
throw new ServiceError(ERRORS.SUBSCRIPTION_ALREADY_CANCELED);
|
throw new ServiceError(ERRORS.SUBSCRIPTION_ALREADY_CANCELED);
|
||||||
}
|
}
|
||||||
await subscription.$query().patch({ canceledAt: new Date() });
|
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 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,8 +20,14 @@ export class SubscriptionApplication {
|
|||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public cancelSubscription(tenantId: number, id: string) {
|
public cancelSubscription(
|
||||||
return this.cancelSubscriptionService.cancelSubscription(tenantId, id);
|
tenantId: number,
|
||||||
|
subscriptionSlug: string = 'main'
|
||||||
|
) {
|
||||||
|
return this.cancelSubscriptionService.cancelSubscription(
|
||||||
|
tenantId,
|
||||||
|
subscriptionSlug
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,8 +35,14 @@ export class SubscriptionApplication {
|
|||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public resumeSubscription(tenantId: number) {
|
public resumeSubscription(
|
||||||
return this.resumeSubscriptionService.resumeSubscription(tenantId);
|
tenantId: number,
|
||||||
|
subscriptionSlug: string = 'main'
|
||||||
|
) {
|
||||||
|
return this.resumeSubscriptionService.resumeSubscription(
|
||||||
|
tenantId,
|
||||||
|
subscriptionSlug
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import { IAuthSignedUpEventPayload } from '@/interfaces';
|
import { IAuthSignedUpEventPayload } from '@/interfaces';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { Subscription } from '../Subscription';
|
import { Subscription } from '../Subscription';
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SubscribeFreeOnSignupCommunity {
|
export class SubscribeFreeOnSignupCommunity {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export const ERRORS = {
|
|||||||
'SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT',
|
'SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT',
|
||||||
SUBSCRIPTION_NOT_EXIST: 'SUBSCRIPTION_NOT_EXIST',
|
SUBSCRIPTION_NOT_EXIST: 'SUBSCRIPTION_NOT_EXIST',
|
||||||
SUBSCRIPTION_ALREADY_CANCELED: 'SUBSCRIPTION_ALREADY_CANCELED',
|
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 {
|
export interface IOrganizationSubscriptionChanged {
|
||||||
@@ -11,7 +13,7 @@ export interface IOrganizationSubscriptionChanged {
|
|||||||
newVariantId: number;
|
newVariantId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IOrganizationSubscriptionCanceled {
|
export interface IOrganizationSubscriptionCancelled {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
subscriptionId: string;
|
subscriptionId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,12 +46,17 @@ export default {
|
|||||||
* Organization subscription.
|
* Organization subscription.
|
||||||
*/
|
*/
|
||||||
subscription: {
|
subscription: {
|
||||||
onSubscriptionCanceled: 'onSubscriptionCanceled',
|
onSubscriptionCancel: 'onSubscriptionCancel',
|
||||||
onSubscriptionResumed: 'onSubscriptionResumed',
|
|
||||||
onSubscriptionPlanChanged: 'onSubscriptionPlanChanged',
|
|
||||||
onSubscribed: 'onOrganizationSubscribed',
|
|
||||||
|
|
||||||
onSubscriptionCancelled: 'onSubscriptionCancelled',
|
onSubscriptionCancelled: 'onSubscriptionCancelled',
|
||||||
|
|
||||||
|
onSubscriptionResume: 'onSubscriptionResume',
|
||||||
|
onSubscriptionResumed: 'onSubscriptionResumed',
|
||||||
|
|
||||||
|
onSubscriptionPlanChange: 'onSubscriptionPlanChange',
|
||||||
|
onSubscriptionPlanChanged: 'onSubscriptionPlanChanged',
|
||||||
|
|
||||||
|
onSubscriptionSubscribed: 'onSubscriptionSubscribed',
|
||||||
|
|
||||||
onSubscriptionPaymentSucceed: 'onSubscriptionPaymentSucceed',
|
onSubscriptionPaymentSucceed: 'onSubscriptionPaymentSucceed',
|
||||||
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed'
|
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed'
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user