mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
fix: Listen to payment webhooks
This commit is contained in:
@@ -64,6 +64,19 @@ export class LemonSqueezyWebhooks {
|
||||
throw new Error("Event body is missing the 'meta' property.");
|
||||
} else if (webhookHasData(eventBody)) {
|
||||
if (webhookEvent.startsWith('subscription_payment_')) {
|
||||
// Marks the main subscription payment as succeed.
|
||||
if (webhookEvent === 'subscription_payment_success') {
|
||||
await this.subscriptionService.markSubscriptionPaymentSucceed(
|
||||
tenantId,
|
||||
'main'
|
||||
);
|
||||
// Marks the main subscription payment as failed.
|
||||
} else if (webhookEvent === 'subscription_payment_failed') {
|
||||
await this.subscriptionService.markSubscriptionPaymentFailed(
|
||||
tenantId,
|
||||
'main'
|
||||
);
|
||||
}
|
||||
// Save subscription invoices; eventBody is a SubscriptionInvoice
|
||||
// Not implemented.
|
||||
} else if (webhookEvent.startsWith('subscription_')) {
|
||||
@@ -74,16 +87,34 @@ export class LemonSqueezyWebhooks {
|
||||
// We assume that the Plan table is up to date.
|
||||
const plan = await Plan.query().findOne('lemonVariantId', variantId);
|
||||
|
||||
// Update the subscription in the database.
|
||||
const priceId = attributes.first_subscription_item.price_id;
|
||||
const subscriptionId = eventBody.data.id;
|
||||
|
||||
// Throw error early if the given lemon variant id is not associated to any plan.
|
||||
if (!plan) {
|
||||
throw new Error(`Plan with variantId ${variantId} not found.`);
|
||||
} else {
|
||||
// Update the subscription in the database.
|
||||
const priceId = attributes.first_subscription_item.price_id;
|
||||
|
||||
// Create a new subscription of the tenant.
|
||||
if (webhookEvent === 'subscription_created') {
|
||||
await this.subscriptionService.newSubscribtion(tenantId, plan.slug);
|
||||
}
|
||||
}
|
||||
// Create a new subscription of the tenant.
|
||||
if (webhookEvent === 'subscription_created') {
|
||||
await this.subscriptionService.newSubscribtion(
|
||||
tenantId,
|
||||
plan.slug,
|
||||
'main',
|
||||
{ lemonSqueezyId: subscriptionId }
|
||||
);
|
||||
// Cancel the given subscription of the organization.
|
||||
} else if (webhookEvent === 'subscription_cancelled') {
|
||||
await this.subscriptionService.cancelSubscription(
|
||||
tenantId,
|
||||
plan.slug
|
||||
);
|
||||
} else if (webhookEvent === 'subscription_plan_changed') {
|
||||
await this.subscriptionService.subscriptionPlanChanged(
|
||||
tenantId,
|
||||
plan.slug,
|
||||
'main'
|
||||
);
|
||||
}
|
||||
} else if (webhookEvent.startsWith('order_')) {
|
||||
// Save orders; eventBody is a "Order"
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import { Service } from 'typedi';
|
||||
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
|
||||
import { Plan, Tenant } from '@/system/models';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { NotAllowedChangeSubscriptionPlan, ServiceError } from '@/exceptions';
|
||||
import { Plan, PlanSubscription, Tenant } from '@/system/models';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { SubscriptionPayload, SubscriptionPaymentStatus } from '@/interfaces';
|
||||
import { ERRORS } from './types';
|
||||
|
||||
@Service()
|
||||
export class Subscription {
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Give the tenant a new subscription.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} planSlug - Plan slug.
|
||||
* @param {string} invoiceInterval
|
||||
* @param {number} invoicePeriod
|
||||
* @param {string} subscriptionSlug
|
||||
* @param {string} planSlug - Plan slug of the new subscription.
|
||||
* @param {string} subscriptionSlug - Subscription slug by default takes main subscription
|
||||
* @param {SubscriptionPayload} payload - Subscription payload.
|
||||
*/
|
||||
public async newSubscribtion(
|
||||
tenantId: number,
|
||||
planSlug: string,
|
||||
subscriptionSlug: string = 'main'
|
||||
) {
|
||||
subscriptionSlug: string = 'main',
|
||||
payload?: SubscriptionPayload
|
||||
): Promise<void> {
|
||||
const tenant = await Tenant.query().findById(tenantId).throwIfNotFound();
|
||||
const plan = await Plan.query().findOne('slug', planSlug).throwIfNotFound();
|
||||
|
||||
@@ -45,8 +52,127 @@ export class Subscription {
|
||||
plan.id,
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
subscriptionSlug
|
||||
subscriptionSlug,
|
||||
payload
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the given tenant subscription.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} subscriptionSlug - Subscription slug.
|
||||
*/
|
||||
async cancelSubscription(
|
||||
tenantId: number,
|
||||
subscriptionSlug: string = 'main'
|
||||
): Promise<void> {
|
||||
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 already canceled.
|
||||
if (subscription.canceled()) {
|
||||
throw new ServiceError(ERRORS.SUBSCRIPTION_ALREADY_CANCELED);
|
||||
}
|
||||
await subscription.$query().patch({ canceledAt: new Date() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given subscription payment of the tenant as succeed.
|
||||
* @param {number} tenantId
|
||||
* @param {string} newPlanSlug
|
||||
* @param {string} subscriptionSlug
|
||||
*/
|
||||
async subscriptionPlanChanged(
|
||||
tenantId: number,
|
||||
newPlanSlug: string,
|
||||
subscriptionSlug: string = 'main'
|
||||
): Promise<void> {
|
||||
const tenant = await Tenant.query().findById(tenantId).throwIfNotFound();
|
||||
const newPlan = await Plan.query()
|
||||
.findOne('slug', newPlanSlug)
|
||||
.throwIfNotFound();
|
||||
|
||||
const subscription = await PlanSubscription.query().findOne({
|
||||
tenantId,
|
||||
slug: subscriptionSlug,
|
||||
});
|
||||
if (subscription.planId === newPlan.id) {
|
||||
throw new ServiceError('');
|
||||
}
|
||||
await subscription.$query().patch({ planId: newPlan.id });
|
||||
|
||||
// Triggers `onSubscriptionPlanChanged` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.subscription.onSubscriptionPlanChanged,
|
||||
{
|
||||
tenantId,
|
||||
newPlanSlug,
|
||||
subscriptionSlug,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the subscription payment as succeed.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} subscriptionSlug - Given subscription slug by default main subscription.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async markSubscriptionPaymentSucceed(
|
||||
tenantId: number,
|
||||
subscriptionSlug: string = 'main'
|
||||
): Promise<void> {
|
||||
const subscription = await PlanSubscription.query()
|
||||
.findOne({ tenantId, slug: subscriptionSlug })
|
||||
.throwIfNotFound();
|
||||
|
||||
await subscription
|
||||
.$query()
|
||||
.patch({ paymentStatus: SubscriptionPaymentStatus.Succeed });
|
||||
|
||||
// Triggers `onSubscriptionSucceed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.subscription.onSubscriptionPaymentSucceed,
|
||||
{
|
||||
tenantId,
|
||||
subscriptionSlug,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given subscription payment of the tenant as failed.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} subscriptionSlug - Given subscription slug.
|
||||
* @returns {Prmise<void>}
|
||||
*/
|
||||
async markSubscriptionPaymentFailed(
|
||||
tenantId: number,
|
||||
subscriptionSlug: string = 'main'
|
||||
): Promise<void> {
|
||||
const subscription = await PlanSubscription.query()
|
||||
.findOne({ tenantId, slug: subscriptionSlug })
|
||||
.throwIfNotFound();
|
||||
|
||||
await subscription
|
||||
.$query()
|
||||
.patch({ paymentStatus: SubscriptionPaymentStatus.Failed });
|
||||
|
||||
// Triggers `onSubscriptionPaymentFailed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.subscription.onSubscriptionPaymentFailed,
|
||||
{
|
||||
tenantId,
|
||||
subscriptionSlug,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export const ERRORS = {
|
||||
SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT:
|
||||
'SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT',
|
||||
SUBSCRIPTION_NOT_EXIST: 'SUBSCRIPTION_NOT_EXIST',
|
||||
SUBSCRIPTION_ALREADY_CANCELED: 'SUBSCRIPTION_ALREADY_CANCELED',
|
||||
};
|
||||
|
||||
export interface IOrganizationSubscriptionChanged {
|
||||
|
||||
Reference in New Issue
Block a user