mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 15:50:32 +00:00
feat: listen LemonSqueezy webhooks
This commit is contained in:
@@ -1,250 +0,0 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
|
||||||
import { check, oneOf, ValidationChain } from 'express-validator';
|
|
||||||
import basicAuth from 'express-basic-auth';
|
|
||||||
import config from '@/config';
|
|
||||||
import { License } from '@/system/models';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
import BaseController from '@/api/controllers/BaseController';
|
|
||||||
import LicenseService from '@/services/Payment/License';
|
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
|
||||||
import { ILicensesFilter, ISendLicenseDTO } from '@/interfaces';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class LicensesController extends BaseController {
|
|
||||||
@Inject()
|
|
||||||
licenseService: LicenseService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Router constructor.
|
|
||||||
*/
|
|
||||||
router() {
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.use(
|
|
||||||
basicAuth({
|
|
||||||
users: {
|
|
||||||
[config.licensesAuth.user]: config.licensesAuth.password,
|
|
||||||
},
|
|
||||||
challenge: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
router.post(
|
|
||||||
'/generate',
|
|
||||||
this.generateLicenseSchema,
|
|
||||||
this.validationResult,
|
|
||||||
asyncMiddleware(this.generateLicense.bind(this)),
|
|
||||||
this.catchServiceErrors,
|
|
||||||
);
|
|
||||||
router.post(
|
|
||||||
'/disable/:licenseId',
|
|
||||||
this.validationResult,
|
|
||||||
asyncMiddleware(this.disableLicense.bind(this)),
|
|
||||||
this.catchServiceErrors,
|
|
||||||
);
|
|
||||||
router.post(
|
|
||||||
'/send',
|
|
||||||
this.sendLicenseSchemaValidation,
|
|
||||||
this.validationResult,
|
|
||||||
asyncMiddleware(this.sendLicense.bind(this)),
|
|
||||||
this.catchServiceErrors,
|
|
||||||
);
|
|
||||||
router.delete(
|
|
||||||
'/:licenseId',
|
|
||||||
asyncMiddleware(this.deleteLicense.bind(this)),
|
|
||||||
this.catchServiceErrors,
|
|
||||||
);
|
|
||||||
router.get('/', asyncMiddleware(this.listLicenses.bind(this)));
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate license validation schema.
|
|
||||||
*/
|
|
||||||
get generateLicenseSchema(): ValidationChain[] {
|
|
||||||
return [
|
|
||||||
check('loop').exists().isNumeric().toInt(),
|
|
||||||
check('period').exists().isNumeric().toInt(),
|
|
||||||
check('period_interval')
|
|
||||||
.exists()
|
|
||||||
.isIn(['month', 'months', 'year', 'years', 'day', 'days']),
|
|
||||||
check('plan_slug').exists().trim().escape(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specific license validation schema.
|
|
||||||
*/
|
|
||||||
get specificLicenseSchema(): ValidationChain[] {
|
|
||||||
return [
|
|
||||||
oneOf(
|
|
||||||
[check('license_id').exists().isNumeric().toInt()],
|
|
||||||
[check('license_code').exists().isNumeric().toInt()]
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send license validation schema.
|
|
||||||
*/
|
|
||||||
get sendLicenseSchemaValidation(): ValidationChain[] {
|
|
||||||
return [
|
|
||||||
check('period').exists().isNumeric(),
|
|
||||||
check('period_interval').exists().trim().escape(),
|
|
||||||
check('plan_slug').exists().trim().escape(),
|
|
||||||
oneOf([
|
|
||||||
check('phone_number').exists().trim().escape(),
|
|
||||||
check('email').exists().trim().escape(),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate licenses codes with given period in bulk.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @return {Response}
|
|
||||||
*/
|
|
||||||
async generateLicense(req: Request, res: Response, next: Function) {
|
|
||||||
const { loop = 10, period, periodInterval, planSlug } = this.matchedBodyData(
|
|
||||||
req
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.licenseService.generateLicenses(
|
|
||||||
loop,
|
|
||||||
period,
|
|
||||||
periodInterval,
|
|
||||||
planSlug
|
|
||||||
);
|
|
||||||
return res.status(200).send({
|
|
||||||
code: 100,
|
|
||||||
type: 'LICENSEES.GENERATED.SUCCESSFULLY',
|
|
||||||
message: 'The licenses have been generated successfully.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable the given license on the storage.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @return {Response}
|
|
||||||
*/
|
|
||||||
async disableLicense(req: Request, res: Response, next: Function) {
|
|
||||||
const { licenseId } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.licenseService.disableLicense(licenseId);
|
|
||||||
|
|
||||||
return res.status(200).send({ license_id: licenseId });
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the given license code on the storage.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @return {Response}
|
|
||||||
*/
|
|
||||||
async deleteLicense(req: Request, res: Response, next: Function) {
|
|
||||||
const { licenseId } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.licenseService.deleteLicense(licenseId);
|
|
||||||
|
|
||||||
return res.status(200).send({ license_id: licenseId });
|
|
||||||
} catch (error) {
|
|
||||||
next(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send license code in the given period to the customer via email or phone number
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @return {Response}
|
|
||||||
*/
|
|
||||||
async sendLicense(req: Request, res: Response, next: Function) {
|
|
||||||
const sendLicenseDTO: ISendLicenseDTO = this.matchedBodyData(req);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.licenseService.sendLicenseToCustomer(sendLicenseDTO);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
status: 100,
|
|
||||||
code: 'LICENSE.CODE.SENT',
|
|
||||||
message: 'The license has been sent to the given customer.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listing licenses.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
*/
|
|
||||||
async listLicenses(req: Request, res: Response) {
|
|
||||||
const filter: ILicensesFilter = {
|
|
||||||
disabled: false,
|
|
||||||
used: false,
|
|
||||||
sent: false,
|
|
||||||
active: false,
|
|
||||||
...req.query,
|
|
||||||
};
|
|
||||||
const licenses = await License.query().onBuild((builder) => {
|
|
||||||
builder.modify('filter', filter);
|
|
||||||
builder.orderBy('createdAt', 'ASC');
|
|
||||||
});
|
|
||||||
return res.status(200).send({ licenses });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Catches all service errors.
|
|
||||||
*/
|
|
||||||
catchServiceErrors(error, req: Request, res: Response, next: NextFunction) {
|
|
||||||
if (error instanceof ServiceError) {
|
|
||||||
if (error.errorType === 'PLAN_NOT_FOUND') {
|
|
||||||
return res.status(400).send({
|
|
||||||
errors: [{
|
|
||||||
type: 'PLAN.NOT.FOUND',
|
|
||||||
code: 100,
|
|
||||||
message: 'The given plan not found.',
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (error.errorType === 'LICENSE_NOT_FOUND') {
|
|
||||||
return res.status(400).send({
|
|
||||||
errors: [{
|
|
||||||
type: 'LICENSE_NOT_FOUND',
|
|
||||||
code: 200,
|
|
||||||
message: 'The given license id not found.'
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (error.errorType === 'LICENSE_ALREADY_DISABLED') {
|
|
||||||
return res.status(400).send({
|
|
||||||
errors: [{
|
|
||||||
type: 'LICENSE.ALREADY.DISABLED',
|
|
||||||
code: 200,
|
|
||||||
message: 'License is already disabled.'
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (error.errorType === 'NO_AVALIABLE_LICENSE_CODE') {
|
|
||||||
return res.status(400).send({
|
|
||||||
status: 110,
|
|
||||||
message: 'There is no licenses availiable right now with the given period and plan.',
|
|
||||||
code: 'NO.AVALIABLE.LICENSE.CODE',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { NextFunction, Router, Request, Response } from 'express';
|
|
||||||
import { check } from 'express-validator';
|
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
|
||||||
import PaymentMethodController from '@/api/controllers/Subscription/PaymentMethod';
|
|
||||||
import {
|
|
||||||
NotAllowedChangeSubscriptionPlan,
|
|
||||||
NoPaymentModelWithPricedPlan,
|
|
||||||
PaymentAmountInvalidWithPlan,
|
|
||||||
PaymentInputInvalid,
|
|
||||||
VoucherCodeRequired,
|
|
||||||
} from '@/exceptions';
|
|
||||||
import { ILicensePaymentModel } from '@/interfaces';
|
|
||||||
import instance from 'tsyringe/dist/typings/dependency-container';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class PaymentViaLicenseController extends PaymentMethodController {
|
|
||||||
@Inject('logger')
|
|
||||||
logger: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Router constructor.
|
|
||||||
*/
|
|
||||||
router() {
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/payment',
|
|
||||||
this.paymentViaLicenseSchema,
|
|
||||||
this.validationResult,
|
|
||||||
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
|
|
||||||
asyncMiddleware(this.paymentViaLicense.bind(this)),
|
|
||||||
this.handleErrors,
|
|
||||||
);
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Payment via license validation schema.
|
|
||||||
*/
|
|
||||||
get paymentViaLicenseSchema() {
|
|
||||||
return [
|
|
||||||
check('plan_slug').exists().trim().escape(),
|
|
||||||
check('license_code').exists().trim().escape(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the subscription payment via license code.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @return {Response}
|
|
||||||
*/
|
|
||||||
async paymentViaLicense(req: Request, res: Response, next: Function) {
|
|
||||||
const { planSlug, licenseCode } = this.matchedBodyData(req);
|
|
||||||
const { tenant } = req;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const licenseModel: ILicensePaymentModel = { licenseCode };
|
|
||||||
|
|
||||||
await this.subscriptionService.subscriptionViaLicense(
|
|
||||||
tenant.id,
|
|
||||||
planSlug,
|
|
||||||
licenseModel
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
type: 'success',
|
|
||||||
code: 'PAYMENT.SUCCESSFULLY.MADE',
|
|
||||||
message: 'Payment via license has been made successfully.',
|
|
||||||
});
|
|
||||||
} catch (exception) {
|
|
||||||
next(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle service errors.
|
|
||||||
* @param {Error} error
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
*/
|
|
||||||
private handleErrors(
|
|
||||||
exception: Error,
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const errorReasons = [];
|
|
||||||
|
|
||||||
if (exception instanceof VoucherCodeRequired) {
|
|
||||||
errorReasons.push({
|
|
||||||
type: 'VOUCHER_CODE_REQUIRED',
|
|
||||||
code: 100,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (exception instanceof NoPaymentModelWithPricedPlan) {
|
|
||||||
errorReasons.push({
|
|
||||||
type: 'NO_PAYMENT_WITH_PRICED_PLAN',
|
|
||||||
code: 140,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (exception instanceof NotAllowedChangeSubscriptionPlan) {
|
|
||||||
errorReasons.push({
|
|
||||||
type: 'NOT.ALLOWED.RENEW.SUBSCRIPTION.WHILE.ACTIVE',
|
|
||||||
code: 120,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (errorReasons.length > 0) {
|
|
||||||
return res.status(400).send({ errors: errorReasons });
|
|
||||||
}
|
|
||||||
if (exception instanceof PaymentInputInvalid) {
|
|
||||||
return res.status(400).send({
|
|
||||||
errors: [{ type: 'LICENSE.CODE.IS.INVALID', code: 120 }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (exception instanceof PaymentAmountInvalidWithPlan) {
|
|
||||||
return res.status(400).send({
|
|
||||||
errors: [{ type: 'LICENSE.NOT.FOR.GIVEN.PLAN' }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
next(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
|
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
|
||||||
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
|
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -10,18 +11,39 @@ export class Webhooks extends BaseController {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private plaidApp: PlaidApplication;
|
private plaidApp: PlaidApplication;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private lemonWebhooksService: LemonSqueezyWebhooks;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
router() {
|
router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use(PlaidWebhookTenantBootMiddleware);
|
router.use('/plaid', PlaidWebhookTenantBootMiddleware);
|
||||||
router.post('/plaid', this.plaidWebhooks.bind(this));
|
router.post('/plaid', this.plaidWebhooks.bind(this));
|
||||||
|
|
||||||
|
router.post('/lemon', this.lemonWebhooks.bind(this));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens to LemonSqueezy webhooks events.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @returns {Response}
|
||||||
|
*/
|
||||||
|
public async lemonWebhooks(req: Request, res: Response) {
|
||||||
|
const data = req.body;
|
||||||
|
const signature = req.headers['x-signature'] ?? '';
|
||||||
|
const rawBody = req.rawBody;
|
||||||
|
|
||||||
|
await this.lemonWebhooksService.handlePostWebhook(rawBody, data, signature);
|
||||||
|
|
||||||
|
return res.status(200).send();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listens to Plaid webhooks.
|
* Listens to Plaid webhooks.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Container } from 'typedi';
|
|
||||||
import LicenseService from '@/services/Payment/License';
|
|
||||||
|
|
||||||
export default class SendLicenseViaEmailJob {
|
|
||||||
/**
|
|
||||||
* Constructor method.
|
|
||||||
* @param agenda
|
|
||||||
*/
|
|
||||||
constructor(agenda) {
|
|
||||||
agenda.define(
|
|
||||||
'send-license-via-email',
|
|
||||||
{ priority: 'high', concurrency: 1, },
|
|
||||||
this.handler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handler(job, done: Function): Promise<void> {
|
|
||||||
const Logger = Container.get('logger');
|
|
||||||
const licenseService = Container.get(LicenseService);
|
|
||||||
const { email, licenseCode } = job.attrs.data;
|
|
||||||
|
|
||||||
Logger.info(`[send_license_via_mail] started: ${job.attrs.data}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await licenseService.mailMessages.sendMailLicense(licenseCode, email);
|
|
||||||
Logger.info(`[send_license_via_mail] completed: ${job.attrs.data}`);
|
|
||||||
done();
|
|
||||||
} catch(e) {
|
|
||||||
Logger.error(`[send_license_via_mail] ${job.attrs.data}, error: ${e}`);
|
|
||||||
done(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Container } from 'typedi';
|
|
||||||
import LicenseService from '@/services/Payment/License';
|
|
||||||
|
|
||||||
export default class SendLicenseViaPhoneJob {
|
|
||||||
/**
|
|
||||||
* Constructor method.
|
|
||||||
*/
|
|
||||||
constructor(agenda) {
|
|
||||||
agenda.define(
|
|
||||||
'send-license-via-phone',
|
|
||||||
{ priority: 'high', concurrency: 1, },
|
|
||||||
this.handler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handler(job, done: Function): Promise<void> {
|
|
||||||
const { phoneNumber, licenseCode } = job.attrs.data;
|
|
||||||
|
|
||||||
const Logger = Container.get('logger');
|
|
||||||
const licenseService = Container.get(LicenseService);
|
|
||||||
|
|
||||||
Logger.debug(`Send license via phone number - started: ${job.attrs.data}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await licenseService.smsMessages.sendLicenseSMSMessage(phoneNumber, licenseCode);
|
|
||||||
Logger.debug(`Send license via phone number - completed: ${job.attrs.data}`);
|
|
||||||
done();
|
|
||||||
} catch(e) {
|
|
||||||
Logger.error(`Send license via phone number: ${job.attrs.data}, error: ${e}`);
|
|
||||||
done(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,6 @@ import Agenda from 'agenda';
|
|||||||
import ResetPasswordMailJob from 'jobs/ResetPasswordMail';
|
import ResetPasswordMailJob from 'jobs/ResetPasswordMail';
|
||||||
import ComputeItemCost from 'jobs/ComputeItemCost';
|
import ComputeItemCost from 'jobs/ComputeItemCost';
|
||||||
import RewriteInvoicesJournalEntries from 'jobs/WriteInvoicesJEntries';
|
import RewriteInvoicesJournalEntries from 'jobs/WriteInvoicesJEntries';
|
||||||
import SendLicenseViaPhoneJob from 'jobs/SendLicensePhone';
|
|
||||||
import SendLicenseViaEmailJob from 'jobs/SendLicenseEmail';
|
|
||||||
import SendSMSNotificationSubscribeEnd from 'jobs/SMSNotificationSubscribeEnd';
|
import SendSMSNotificationSubscribeEnd from 'jobs/SMSNotificationSubscribeEnd';
|
||||||
import SendSMSNotificationTrialEnd from 'jobs/SMSNotificationTrialEnd';
|
import SendSMSNotificationTrialEnd from 'jobs/SMSNotificationTrialEnd';
|
||||||
import SendMailNotificationSubscribeEnd from 'jobs/MailNotificationSubscribeEnd';
|
import SendMailNotificationSubscribeEnd from 'jobs/MailNotificationSubscribeEnd';
|
||||||
@@ -22,8 +20,6 @@ import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDelet
|
|||||||
export default ({ agenda }: { agenda: Agenda }) => {
|
export default ({ agenda }: { agenda: Agenda }) => {
|
||||||
new ResetPasswordMailJob(agenda);
|
new ResetPasswordMailJob(agenda);
|
||||||
new UserInviteMailJob(agenda);
|
new UserInviteMailJob(agenda);
|
||||||
new SendLicenseViaEmailJob(agenda);
|
|
||||||
new SendLicenseViaPhoneJob(agenda);
|
|
||||||
new ComputeItemCost(agenda);
|
new ComputeItemCost(agenda);
|
||||||
new RewriteInvoicesJournalEntries(agenda);
|
new RewriteInvoicesJournalEntries(agenda);
|
||||||
new OrganizationSetupJob(agenda);
|
new OrganizationSetupJob(agenda);
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
import { Service, Container, Inject } from 'typedi';
|
|
||||||
import cryptoRandomString from 'crypto-random-string';
|
|
||||||
import { times } from 'lodash';
|
|
||||||
import { License, Plan } from '@/system/models';
|
|
||||||
import { ILicense, ISendLicenseDTO } from '@/interfaces';
|
|
||||||
import LicenseMailMessages from '@/services/Payment/LicenseMailMessages';
|
|
||||||
import LicenseSMSMessages from '@/services/Payment/LicenseSMSMessages';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
|
|
||||||
const ERRORS = {
|
|
||||||
PLAN_NOT_FOUND: 'PLAN_NOT_FOUND',
|
|
||||||
LICENSE_NOT_FOUND: 'LICENSE_NOT_FOUND',
|
|
||||||
LICENSE_ALREADY_DISABLED: 'LICENSE_ALREADY_DISABLED',
|
|
||||||
NO_AVALIABLE_LICENSE_CODE: 'NO_AVALIABLE_LICENSE_CODE',
|
|
||||||
};
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class LicenseService {
|
|
||||||
@Inject()
|
|
||||||
smsMessages: LicenseSMSMessages;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
mailMessages: LicenseMailMessages;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the plan existance on the storage.
|
|
||||||
* @param {number} tenantId -
|
|
||||||
* @param {string} planSlug - Plan slug.
|
|
||||||
*/
|
|
||||||
private async getPlanOrThrowError(planSlug: string) {
|
|
||||||
const foundPlan = await Plan.query().where('slug', planSlug).first();
|
|
||||||
|
|
||||||
if (!foundPlan) {
|
|
||||||
throw new ServiceError(ERRORS.PLAN_NOT_FOUND);
|
|
||||||
}
|
|
||||||
return foundPlan;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Valdiate the license existance on the storage.
|
|
||||||
* @param {number} licenseId - License id.
|
|
||||||
*/
|
|
||||||
private async getLicenseOrThrowError(licenseId: number) {
|
|
||||||
const foundLicense = await License.query().findById(licenseId);
|
|
||||||
|
|
||||||
if (!foundLicense) {
|
|
||||||
throw new ServiceError(ERRORS.LICENSE_NOT_FOUND);
|
|
||||||
}
|
|
||||||
return foundLicense;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates whether the license id is disabled.
|
|
||||||
* @param {ILicense} license
|
|
||||||
*/
|
|
||||||
private validateNotDisabledLicense(license: ILicense) {
|
|
||||||
if (license.disabledAt) {
|
|
||||||
throw new ServiceError(ERRORS.LICENSE_ALREADY_DISABLED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates the license code in the given period.
|
|
||||||
* @param {number} licensePeriod
|
|
||||||
* @return {Promise<ILicense>}
|
|
||||||
*/
|
|
||||||
public async generateLicense(
|
|
||||||
licensePeriod: number,
|
|
||||||
periodInterval: string = 'days',
|
|
||||||
planSlug: string
|
|
||||||
): ILicense {
|
|
||||||
let licenseCode: string;
|
|
||||||
let repeat: boolean = true;
|
|
||||||
|
|
||||||
// Retrieve plan or throw not found error.
|
|
||||||
const plan = await this.getPlanOrThrowError(planSlug);
|
|
||||||
|
|
||||||
while (repeat) {
|
|
||||||
licenseCode = cryptoRandomString({ length: 10, type: 'numeric' });
|
|
||||||
const foundLicenses = await License.query().where(
|
|
||||||
'license_code',
|
|
||||||
licenseCode
|
|
||||||
);
|
|
||||||
|
|
||||||
if (foundLicenses.length === 0) {
|
|
||||||
repeat = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return License.query().insert({
|
|
||||||
licenseCode,
|
|
||||||
licensePeriod,
|
|
||||||
periodInterval,
|
|
||||||
planId: plan.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates licenses.
|
|
||||||
* @param {number} loop
|
|
||||||
* @param {number} licensePeriod
|
|
||||||
* @param {string} periodInterval
|
|
||||||
* @param {number} planId
|
|
||||||
*/
|
|
||||||
public async generateLicenses(
|
|
||||||
loop = 1,
|
|
||||||
licensePeriod: number,
|
|
||||||
periodInterval: string = 'days',
|
|
||||||
planSlug: string
|
|
||||||
) {
|
|
||||||
const asyncOpers: Promise<any>[] = [];
|
|
||||||
|
|
||||||
times(loop, () => {
|
|
||||||
const generateOper = this.generateLicense(
|
|
||||||
licensePeriod,
|
|
||||||
periodInterval,
|
|
||||||
planSlug
|
|
||||||
);
|
|
||||||
asyncOpers.push(generateOper);
|
|
||||||
});
|
|
||||||
return Promise.all(asyncOpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables the given license id on the storage.
|
|
||||||
* @param {string} licenseSlug - License slug.
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
public async disableLicense(licenseId: number) {
|
|
||||||
const license = await this.getLicenseOrThrowError(licenseId);
|
|
||||||
|
|
||||||
this.validateNotDisabledLicense(license);
|
|
||||||
|
|
||||||
return License.markLicenseAsDisabled(license.id, 'id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the given license id from the storage.
|
|
||||||
* @param licenseSlug {string} - License slug.
|
|
||||||
*/
|
|
||||||
public async deleteLicense(licenseSlug: string) {
|
|
||||||
const license = await this.getPlanOrThrowError(licenseSlug);
|
|
||||||
|
|
||||||
return License.query().where('id', license.id).delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends license code to the given customer via SMS or mail message.
|
|
||||||
* @param {string} licenseCode - License code.
|
|
||||||
* @param {string} phoneNumber - Phone number.
|
|
||||||
* @param {string} email - Email address.
|
|
||||||
*/
|
|
||||||
public async sendLicenseToCustomer(sendLicense: ISendLicenseDTO) {
|
|
||||||
const agenda = Container.get('agenda');
|
|
||||||
const { phoneNumber, email, period, periodInterval } = sendLicense;
|
|
||||||
|
|
||||||
// Retreive plan details byt the given plan slug.
|
|
||||||
const plan = await this.getPlanOrThrowError(sendLicense.planSlug);
|
|
||||||
|
|
||||||
const license = await License.query()
|
|
||||||
.modify('filterActiveLicense')
|
|
||||||
.where('license_period', period)
|
|
||||||
.where('period_interval', periodInterval)
|
|
||||||
.where('plan_id', plan.id)
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (!license) {
|
|
||||||
throw new ServiceError(ERRORS.NO_AVALIABLE_LICENSE_CODE)
|
|
||||||
}
|
|
||||||
// Mark the license as used.
|
|
||||||
await License.markLicenseAsSent(license.licenseCode);
|
|
||||||
|
|
||||||
if (sendLicense.email) {
|
|
||||||
await agenda.schedule('1 second', 'send-license-via-email', {
|
|
||||||
licenseCode: license.licenseCode,
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (phoneNumber) {
|
|
||||||
await agenda.schedule('1 second', 'send-license-via-phone', {
|
|
||||||
licenseCode: license.licenseCode,
|
|
||||||
phoneNumber,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Container } from 'typedi';
|
|
||||||
import Mail from '@/lib/Mail';
|
|
||||||
import config from '@/config';
|
|
||||||
export default class SubscriptionMailMessages {
|
|
||||||
/**
|
|
||||||
* Send license code to the given mail address.
|
|
||||||
* @param {string} licenseCode
|
|
||||||
* @param {email} email
|
|
||||||
*/
|
|
||||||
public async sendMailLicense(licenseCode: string, email: string) {
|
|
||||||
const Logger = Container.get('logger');
|
|
||||||
|
|
||||||
const mail = new Mail()
|
|
||||||
.setView('mail/LicenseReceive.html')
|
|
||||||
.setSubject('Bigcapital - License code')
|
|
||||||
.setTo(email)
|
|
||||||
.setData({
|
|
||||||
licenseCode,
|
|
||||||
successEmail: config.customerSuccess.email,
|
|
||||||
successPhoneNumber: config.customerSuccess.phoneNumber,
|
|
||||||
});
|
|
||||||
|
|
||||||
await mail.send();
|
|
||||||
Logger.info('[license_mail] sent successfully.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { License } from '@/system/models';
|
|
||||||
import PaymentMethod from '@/services/Payment/PaymentMethod';
|
|
||||||
import { Plan } from '@/system/models';
|
|
||||||
import { IPaymentMethod, ILicensePaymentModel } from '@/interfaces';
|
|
||||||
import {
|
|
||||||
PaymentInputInvalid,
|
|
||||||
PaymentAmountInvalidWithPlan,
|
|
||||||
VoucherCodeRequired,
|
|
||||||
} from '@/exceptions';
|
|
||||||
|
|
||||||
export default class LicensePaymentMethod
|
|
||||||
extends PaymentMethod
|
|
||||||
implements IPaymentMethod
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Payment subscription of organization via license code.
|
|
||||||
* @param {ILicensePaymentModel} licensePaymentModel -
|
|
||||||
*/
|
|
||||||
public async payment(licensePaymentModel: ILicensePaymentModel, plan: Plan) {
|
|
||||||
this.validateLicensePaymentModel(licensePaymentModel);
|
|
||||||
|
|
||||||
const license = await this.getLicenseOrThrowInvalid(licensePaymentModel);
|
|
||||||
this.validatePaymentAmountWithPlan(license, plan);
|
|
||||||
|
|
||||||
// Mark the license code as used.
|
|
||||||
return License.markLicenseAsUsed(licensePaymentModel.licenseCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the license code activation on the storage.
|
|
||||||
* @param {ILicensePaymentModel} licensePaymentModel -
|
|
||||||
*/
|
|
||||||
private async getLicenseOrThrowInvalid(
|
|
||||||
licensePaymentModel: ILicensePaymentModel
|
|
||||||
) {
|
|
||||||
const foundLicense = await License.query()
|
|
||||||
.modify('filterActiveLicense')
|
|
||||||
.where('license_code', licensePaymentModel.licenseCode)
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (!foundLicense) {
|
|
||||||
throw new PaymentInputInvalid();
|
|
||||||
}
|
|
||||||
return foundLicense;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the payment amount with given plan price.
|
|
||||||
* @param {License} license
|
|
||||||
* @param {Plan} plan
|
|
||||||
*/
|
|
||||||
private validatePaymentAmountWithPlan(license: License, plan: Plan) {
|
|
||||||
if (license.planId !== plan.id) {
|
|
||||||
throw new PaymentAmountInvalidWithPlan();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate voucher payload.
|
|
||||||
* @param {ILicensePaymentModel} licenseModel -
|
|
||||||
*/
|
|
||||||
private validateLicensePaymentModel(licenseModel: ILicensePaymentModel) {
|
|
||||||
if (!licenseModel || !licenseModel.licenseCode) {
|
|
||||||
throw new VoucherCodeRequired();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Container, Inject } from 'typedi';
|
|
||||||
import SMSClient from '@/services/SMSClient';
|
|
||||||
|
|
||||||
export default class SubscriptionSMSMessages {
|
|
||||||
@Inject('SMSClient')
|
|
||||||
smsClient: SMSClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends license code to the given phone number via SMS message.
|
|
||||||
* @param {string} phoneNumber
|
|
||||||
* @param {string} licenseCode
|
|
||||||
*/
|
|
||||||
public async sendLicenseSMSMessage(phoneNumber: string, licenseCode: string) {
|
|
||||||
const message: string = `Your license card number: ${licenseCode}. If you need any help please contact us. Bigcapital.`;
|
|
||||||
return this.smsClient.sendMessage(phoneNumber, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import { IPaymentModel } from '@/interfaces';
|
|
||||||
|
|
||||||
export default class PaymentMethod implements IPaymentModel {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { IPaymentMethod, IPaymentContext } from "interfaces";
|
|
||||||
import { Plan } from '@/system/models';
|
|
||||||
|
|
||||||
export default class PaymentContext<PaymentModel> implements IPaymentContext{
|
|
||||||
paymentMethod: IPaymentMethod;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor method.
|
|
||||||
* @param {IPaymentMethod} paymentMethod
|
|
||||||
*/
|
|
||||||
constructor(paymentMethod: IPaymentMethod) {
|
|
||||||
this.paymentMethod = paymentMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {<PaymentModel>} paymentModel
|
|
||||||
*/
|
|
||||||
makePayment(paymentModel: PaymentModel, plan: Plan) {
|
|
||||||
return this.paymentMethod.payment(paymentModel, plan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
|
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import {
|
import {
|
||||||
compareSignatures,
|
compareSignatures,
|
||||||
configureLemonSqueezy,
|
configureLemonSqueezy,
|
||||||
@@ -9,40 +9,41 @@ import {
|
|||||||
webhookHasMeta,
|
webhookHasMeta,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { Plan } from '@/system/models';
|
import { Plan } from '@/system/models';
|
||||||
|
import { Subscription } from './Subscription';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class LemonWebhooks {
|
export class LemonSqueezyWebhooks {
|
||||||
|
@Inject()
|
||||||
|
private subscriptionService: Subscription;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* handle the LemonSqueezy webhooks.
|
||||||
* @param {string} rawBody
|
* @param {string} rawBody
|
||||||
* @param {string} signature
|
* @param {string} signature
|
||||||
* @returns
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async handlePostWebhook(
|
public async handlePostWebhook(
|
||||||
rawData: any,
|
rawData: any,
|
||||||
data: Record<string, any>,
|
data: Record<string, any>,
|
||||||
signature: string
|
signature: string
|
||||||
) {
|
): Promise<void> {
|
||||||
configureLemonSqueezy();
|
configureLemonSqueezy();
|
||||||
|
|
||||||
if (!process.env.LEMONSQUEEZY_WEBHOOK_SECRET) {
|
if (!process.env.LEMONSQUEEZY_WEBHOOK_SECRET) {
|
||||||
return new ServiceError('Lemon Squeezy Webhook Secret not set in .env');
|
throw new ServiceError('Lemon Squeezy Webhook Secret not set in .env');
|
||||||
}
|
}
|
||||||
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
|
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
|
||||||
const hmacSignature = createHmacSignature(secret, rawData);
|
const hmacSignature = createHmacSignature(secret, rawData);
|
||||||
|
|
||||||
if (!compareSignatures(hmacSignature, signature)) {
|
if (!compareSignatures(hmacSignature, signature)) {
|
||||||
console.log('invalid');
|
throw new Error('Invalid signature');
|
||||||
return new Error('Invalid signature', { status: 400 });
|
|
||||||
}
|
}
|
||||||
// Type guard to check if the object has a 'meta' property.
|
// Type guard to check if the object has a 'meta' property.
|
||||||
if (webhookHasMeta(data)) {
|
if (webhookHasMeta(data)) {
|
||||||
// Non-blocking call to process the webhook event.
|
// Non-blocking call to process the webhook event.
|
||||||
void this.processWebhookEvent(data);
|
void this.processWebhookEvent(data);
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return new Error('Data invalid', { status: 400 });
|
throw new Error('Data invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +53,9 @@ export class LemonWebhooks {
|
|||||||
let processingError = '';
|
let processingError = '';
|
||||||
const webhookEvent = eventBody.meta.event_name;
|
const webhookEvent = eventBody.meta.event_name;
|
||||||
|
|
||||||
|
const userId = eventBody.meta.custom_data?.user_id;
|
||||||
|
const tenantId = eventBody.meta.custom_data?.tenant_id;
|
||||||
|
|
||||||
if (!webhookHasMeta(eventBody)) {
|
if (!webhookHasMeta(eventBody)) {
|
||||||
processingError = "Event body is missing the 'meta' property.";
|
processingError = "Event body is missing the 'meta' property.";
|
||||||
} else if (webhookHasData(eventBody)) {
|
} else if (webhookHasData(eventBody)) {
|
||||||
@@ -78,14 +82,20 @@ export class LemonWebhooks {
|
|||||||
if (priceData.error) {
|
if (priceData.error) {
|
||||||
processingError = `Failed to get the price data for the subscription ${eventBody.data.id}.`;
|
processingError = `Failed to get the price data for the subscription ${eventBody.data.id}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUsageBased =
|
const isUsageBased =
|
||||||
attributes.first_subscription_item.is_usage_based;
|
attributes.first_subscription_item.is_usage_based;
|
||||||
const price = isUsageBased
|
const price = isUsageBased
|
||||||
? priceData.data?.data.attributes.unit_price_decimal
|
? priceData.data?.data.attributes.unit_price_decimal
|
||||||
: priceData.data?.data.attributes.unit_price;
|
: priceData.data?.data.attributes.unit_price;
|
||||||
|
|
||||||
const newSubscription = {};
|
if (webhookEvent === 'subscription_created') {
|
||||||
|
await this.subscriptionService.newSubscribtion(
|
||||||
|
tenantId,
|
||||||
|
'pro-yearly',
|
||||||
|
'year',
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (webhookEvent.startsWith('order_')) {
|
} else if (webhookEvent.startsWith('order_')) {
|
||||||
// Save orders; eventBody is a "Order"
|
// Save orders; eventBody is a "Order"
|
||||||
@@ -1,37 +1,27 @@
|
|||||||
import { Inject } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { Tenant, Plan } from '@/system/models';
|
|
||||||
import { IPaymentContext } from '@/interfaces';
|
|
||||||
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
|
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
|
||||||
|
import { Plan, Tenant } from '@/system/models';
|
||||||
|
|
||||||
export default class Subscription<PaymentModel> {
|
@Service()
|
||||||
paymentContext: IPaymentContext | null;
|
export class Subscription {
|
||||||
|
|
||||||
@Inject('logger')
|
|
||||||
logger: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor method.
|
|
||||||
* @param {IPaymentContext}
|
|
||||||
*/
|
|
||||||
constructor(payment?: IPaymentContext) {
|
|
||||||
this.paymentContext = payment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Give the tenant a new subscription.
|
* Give the tenant a new subscription.
|
||||||
* @param {Tenant} tenant
|
* @param {number} tenantId - Tenant id.
|
||||||
* @param {Plan} plan
|
* @param {string} planSlug - Plan slug.
|
||||||
* @param {string} invoiceInterval
|
* @param {string} invoiceInterval
|
||||||
* @param {number} invoicePeriod
|
* @param {number} invoicePeriod
|
||||||
* @param {string} subscriptionSlug
|
* @param {string} subscriptionSlug
|
||||||
*/
|
*/
|
||||||
protected async newSubscribtion(
|
public async newSubscribtion(
|
||||||
tenant,
|
tenantId: number,
|
||||||
plan,
|
planSlug: string,
|
||||||
invoiceInterval: string,
|
invoiceInterval: string,
|
||||||
invoicePeriod: number,
|
invoicePeriod: number,
|
||||||
subscriptionSlug: string = 'main'
|
subscriptionSlug: string = 'main'
|
||||||
) {
|
) {
|
||||||
|
const tenant = await Tenant.query().findById(tenantId).throwIfNotFound();
|
||||||
|
const plan = await Plan.query().findOne('slug', planSlug).throwIfNotFound();
|
||||||
|
|
||||||
const subscription = await tenant
|
const subscription = await tenant
|
||||||
.$relatedQuery('subscriptions')
|
.$relatedQuery('subscriptions')
|
||||||
.modify('subscriptionBySlug', subscriptionSlug)
|
.modify('subscriptionBySlug', subscriptionSlug)
|
||||||
@@ -55,26 +45,4 @@ export default class Subscription<PaymentModel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscripe to the given plan.
|
|
||||||
* @param {Plan} plan
|
|
||||||
* @throws {NotAllowedChangeSubscriptionPlan}
|
|
||||||
*/
|
|
||||||
public async subscribe(
|
|
||||||
tenant: Tenant,
|
|
||||||
plan: Plan,
|
|
||||||
paymentModel?: PaymentModel,
|
|
||||||
subscriptionSlug: string = 'main'
|
|
||||||
) {
|
|
||||||
await this.paymentContext.makePayment(paymentModel, plan);
|
|
||||||
|
|
||||||
return this.newSubscribtion(
|
|
||||||
tenant,
|
|
||||||
plan,
|
|
||||||
plan.invoiceInterval,
|
|
||||||
plan.invoicePeriod,
|
|
||||||
subscriptionSlug
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,4 +38,4 @@ export default class SubscriptionPeriod {
|
|||||||
getIntervalCount() {
|
getIntervalCount() {
|
||||||
return this.interval;
|
return this.interval;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,8 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { Plan, PlanSubscription, Tenant } from '@/system/models';
|
import { PlanSubscription } from '@/system/models';
|
||||||
import Subscription from '@/services/Subscription/Subscription';
|
|
||||||
import LicensePaymentMethod from '@/services/Payment/LicensePaymentMethod';
|
|
||||||
import PaymentContext from '@/services/Payment';
|
|
||||||
import SubscriptionSMSMessages from '@/services/Subscription/SMSMessages';
|
|
||||||
import SubscriptionMailMessages from '@/services/Subscription/MailMessages';
|
|
||||||
import { ILicensePaymentModel } from '@/interfaces';
|
|
||||||
import SubscriptionViaLicense from './SubscriptionViaLicense';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SubscriptionService {
|
export default class SubscriptionService {
|
||||||
@Inject()
|
|
||||||
smsMessages: SubscriptionSMSMessages;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
mailMessages: SubscriptionMailMessages;
|
|
||||||
|
|
||||||
@Inject('logger')
|
|
||||||
logger: any;
|
|
||||||
|
|
||||||
@Inject('repositories')
|
|
||||||
sysRepositories: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the payment process via license code and than subscribe to
|
|
||||||
* the given tenant.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {String} planSlug
|
|
||||||
* @param {string} licenseCode
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
public async subscriptionViaLicense(
|
|
||||||
tenantId: number,
|
|
||||||
planSlug: string,
|
|
||||||
paymentModel: ILicensePaymentModel,
|
|
||||||
subscriptionSlug: string = 'main'
|
|
||||||
) {
|
|
||||||
// Retrieve plan details.
|
|
||||||
const plan = await Plan.query().findOne('slug', planSlug);
|
|
||||||
|
|
||||||
// Retrieve tenant details.
|
|
||||||
const tenant = await Tenant.query().findById(tenantId);
|
|
||||||
|
|
||||||
// License payment method.
|
|
||||||
const paymentViaLicense = new LicensePaymentMethod();
|
|
||||||
|
|
||||||
// Payment context.
|
|
||||||
const paymentContext = new PaymentContext(paymentViaLicense);
|
|
||||||
|
|
||||||
// Subscription.
|
|
||||||
const subscription = new SubscriptionViaLicense(paymentContext);
|
|
||||||
|
|
||||||
// Subscribe.
|
|
||||||
await subscription.subscribe(tenant, plan, paymentModel, subscriptionSlug);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all subscription of the given tenant.
|
* Retrieve all subscription of the given tenant.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import { License, Tenant, Plan } from '@/system/models';
|
|
||||||
import Subscription from './Subscription';
|
|
||||||
import { PaymentModel } from '@/interfaces';
|
|
||||||
|
|
||||||
export default class SubscriptionViaLicense extends Subscription<PaymentModel> {
|
|
||||||
/**
|
|
||||||
* Subscripe to the given plan.
|
|
||||||
* @param {Plan} plan
|
|
||||||
* @throws {NotAllowedChangeSubscriptionPlan}
|
|
||||||
*/
|
|
||||||
public async subscribe(
|
|
||||||
tenant: Tenant,
|
|
||||||
plan: Plan,
|
|
||||||
paymentModel?: PaymentModel,
|
|
||||||
subscriptionSlug: string = 'main'
|
|
||||||
): Promise<void> {
|
|
||||||
await this.paymentContext.makePayment(paymentModel, plan);
|
|
||||||
|
|
||||||
return this.newSubscriptionFromLicense(
|
|
||||||
tenant,
|
|
||||||
plan,
|
|
||||||
paymentModel.licenseCode,
|
|
||||||
subscriptionSlug
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* New subscription from the given license.
|
|
||||||
* @param {Tanant} tenant
|
|
||||||
* @param {Plab} plan
|
|
||||||
* @param {string} licenseCode
|
|
||||||
* @param {string} subscriptionSlug
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
private async newSubscriptionFromLicense(
|
|
||||||
tenant,
|
|
||||||
plan,
|
|
||||||
licenseCode: string,
|
|
||||||
subscriptionSlug: string = 'main'
|
|
||||||
): Promise<void> {
|
|
||||||
// License information.
|
|
||||||
const licenseInfo = await License.query().findOne(
|
|
||||||
'licenseCode',
|
|
||||||
licenseCode
|
|
||||||
);
|
|
||||||
return this.newSubscribtion(
|
|
||||||
tenant,
|
|
||||||
plan,
|
|
||||||
licenseInfo.periodInterval,
|
|
||||||
licenseInfo.licensePeriod,
|
|
||||||
subscriptionSlug
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
|
|
||||||
exports.up = function(knex) {
|
|
||||||
return knex.schema.createTable('subscription_plan_features', table => {
|
|
||||||
table.increments();
|
|
||||||
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
|
||||||
table.string('slug');
|
|
||||||
table.string('name');
|
|
||||||
table.string('description');
|
|
||||||
table.timestamps();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.down = function(knex) {
|
|
||||||
return knex.schema.dropTableIfExists('subscription_plan_features');
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
|
|
||||||
exports.up = function(knex) {
|
|
||||||
return knex.schema.createTable('subscription_licenses', (table) => {
|
|
||||||
table.increments();
|
|
||||||
|
|
||||||
table.string('license_code').unique().index();
|
|
||||||
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
|
||||||
|
|
||||||
table.integer('license_period').unsigned();
|
|
||||||
table.string('period_interval');
|
|
||||||
|
|
||||||
table.dateTime('sent_at').index();
|
|
||||||
table.dateTime('disabled_at').index();
|
|
||||||
table.dateTime('used_at').index();
|
|
||||||
|
|
||||||
table.timestamps();
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.down = function(knex) {
|
|
||||||
return knex.schema.dropTableIfExists('subscription_licenses');
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Model, mixin } from 'objection';
|
|
||||||
import SystemModel from '@/system/models/SystemModel';
|
|
||||||
|
|
||||||
export default class PlanFeature extends mixin(SystemModel) {
|
|
||||||
/**
|
|
||||||
* Table name.
|
|
||||||
*/
|
|
||||||
static get tableName() {
|
|
||||||
return 'subscriptions.plan_features';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Timestamps columns.
|
|
||||||
*/
|
|
||||||
static get timestamps() {
|
|
||||||
return ['createdAt', 'updatedAt'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relationship mapping.
|
|
||||||
*/
|
|
||||||
static get relationMappings() {
|
|
||||||
const Plan = require('system/models/Subscriptions/Plan');
|
|
||||||
|
|
||||||
return {
|
|
||||||
plan: {
|
|
||||||
relation: Model.BelongsToOneRelation,
|
|
||||||
modelClass: Plan.default,
|
|
||||||
join: {
|
|
||||||
from: 'subscriptions.plan_features.planId',
|
|
||||||
to: 'subscriptions.plans.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -147,9 +147,9 @@ export default class Tenant extends BaseModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given tenant as upgrading.
|
* Marks the given tenant as upgrading.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {string} upgradeJobId
|
* @param {string} upgradeJobId
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static markAsUpgrading(tenantId, upgradeJobId) {
|
static markAsUpgrading(tenantId, upgradeJobId) {
|
||||||
return this.query().update({ upgradeJobId }).where({ id: tenantId });
|
return this.query().update({ upgradeJobId }).where({ id: tenantId });
|
||||||
@@ -157,8 +157,8 @@ export default class Tenant extends BaseModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Markes the given tenant as upgraded.
|
* Markes the given tenant as upgraded.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static markAsUpgraded(tenantId) {
|
static markAsUpgraded(tenantId) {
|
||||||
return this.query().update({ upgradeJobId: null }).where({ id: tenantId });
|
return this.query().update({ upgradeJobId: null }).where({ id: tenantId });
|
||||||
@@ -185,4 +185,44 @@ export default class Tenant extends BaseModel {
|
|||||||
saveMetadata(metadata) {
|
saveMetadata(metadata) {
|
||||||
return Tenant.saveMetadata(this.id, metadata);
|
return Tenant.saveMetadata(this.id, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} planId
|
||||||
|
* @param {*} invoiceInterval
|
||||||
|
* @param {*} invoicePeriod
|
||||||
|
* @param {*} subscriptionSlug
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public newSubscription(planId, invoiceInterval, invoicePeriod, subscriptionSlug) {
|
||||||
|
return Tenant.newSubscription(
|
||||||
|
this.id,
|
||||||
|
planId,
|
||||||
|
invoiceInterval,
|
||||||
|
invoicePeriod,
|
||||||
|
subscriptionSlug
|
||||||
|
);
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a new subscription for the associated tenant.
|
||||||
|
*/
|
||||||
|
static newSubscription(
|
||||||
|
tenantId: number,
|
||||||
|
planId: number,
|
||||||
|
invoiceInterval: string,
|
||||||
|
invoicePeriod: number,
|
||||||
|
subscriptionSlug: string
|
||||||
|
) {
|
||||||
|
const period = new SubscriptionPeriod(invoiceInterval, invoicePeriod);
|
||||||
|
|
||||||
|
return PlanSubscription.query().insert({
|
||||||
|
tenantId,
|
||||||
|
slug: subscriptionSlug,
|
||||||
|
planId,
|
||||||
|
startsAt: period.getStartDate(),
|
||||||
|
endsAt: period.getEndDate(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Plan from './Subscriptions/Plan';
|
import Plan from './Subscriptions/Plan';
|
||||||
import PlanFeature from './Subscriptions/PlanFeature';
|
|
||||||
import PlanSubscription from './Subscriptions/PlanSubscription';
|
import PlanSubscription from './Subscriptions/PlanSubscription';
|
||||||
import License from './Subscriptions/License';
|
import License from './Subscriptions/License';
|
||||||
import Tenant from './Tenant';
|
import Tenant from './Tenant';
|
||||||
@@ -12,7 +11,6 @@ import { Import } from './Import';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Plan,
|
Plan,
|
||||||
PlanFeature,
|
|
||||||
PlanSubscription,
|
PlanSubscription,
|
||||||
License,
|
License,
|
||||||
Tenant,
|
Tenant,
|
||||||
|
|||||||
Reference in New Issue
Block a user