diff --git a/packages/server/src/api/controllers/Subscription/Licenses.ts b/packages/server/src/api/controllers/Subscription/Licenses.ts deleted file mode 100644 index cf483f1a1..000000000 --- a/packages/server/src/api/controllers/Subscription/Licenses.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/server/src/api/controllers/Subscription/PaymentViaLicense.ts b/packages/server/src/api/controllers/Subscription/PaymentViaLicense.ts deleted file mode 100644 index 7cf07c656..000000000 --- a/packages/server/src/api/controllers/Subscription/PaymentViaLicense.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/server/src/api/controllers/Webhooks/Webhooks.ts b/packages/server/src/api/controllers/Webhooks/Webhooks.ts index 955e382fb..662eff4ed 100644 --- a/packages/server/src/api/controllers/Webhooks/Webhooks.ts +++ b/packages/server/src/api/controllers/Webhooks/Webhooks.ts @@ -3,6 +3,7 @@ import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication'; import { Request, Response } from 'express'; import { Inject, Service } from 'typedi'; import BaseController from '../BaseController'; +import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks'; import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware'; @Service() @@ -10,18 +11,39 @@ export class Webhooks extends BaseController { @Inject() private plaidApp: PlaidApplication; + @Inject() + private lemonWebhooksService: LemonSqueezyWebhooks; + /** * Router constructor. */ router() { const router = Router(); - router.use(PlaidWebhookTenantBootMiddleware); + router.use('/plaid', PlaidWebhookTenantBootMiddleware); router.post('/plaid', this.plaidWebhooks.bind(this)); + router.post('/lemon', this.lemonWebhooks.bind(this)); + 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. * @param {Request} req diff --git a/packages/server/src/jobs/SendLicenseEmail.ts b/packages/server/src/jobs/SendLicenseEmail.ts deleted file mode 100644 index d28c07afe..000000000 --- a/packages/server/src/jobs/SendLicenseEmail.ts +++ /dev/null @@ -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 { - 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); - } - } -} diff --git a/packages/server/src/jobs/SendLicensePhone.ts b/packages/server/src/jobs/SendLicensePhone.ts deleted file mode 100644 index adc5429fb..000000000 --- a/packages/server/src/jobs/SendLicensePhone.ts +++ /dev/null @@ -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 { - 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); - } - } -} diff --git a/packages/server/src/loaders/jobs.ts b/packages/server/src/loaders/jobs.ts index 4ec446d0f..db232656b 100644 --- a/packages/server/src/loaders/jobs.ts +++ b/packages/server/src/loaders/jobs.ts @@ -2,8 +2,6 @@ import Agenda from 'agenda'; import ResetPasswordMailJob from 'jobs/ResetPasswordMail'; import ComputeItemCost from 'jobs/ComputeItemCost'; import RewriteInvoicesJournalEntries from 'jobs/WriteInvoicesJEntries'; -import SendLicenseViaPhoneJob from 'jobs/SendLicensePhone'; -import SendLicenseViaEmailJob from 'jobs/SendLicenseEmail'; import SendSMSNotificationSubscribeEnd from 'jobs/SMSNotificationSubscribeEnd'; import SendSMSNotificationTrialEnd from 'jobs/SMSNotificationTrialEnd'; import SendMailNotificationSubscribeEnd from 'jobs/MailNotificationSubscribeEnd'; @@ -22,8 +20,6 @@ import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDelet export default ({ agenda }: { agenda: Agenda }) => { new ResetPasswordMailJob(agenda); new UserInviteMailJob(agenda); - new SendLicenseViaEmailJob(agenda); - new SendLicenseViaPhoneJob(agenda); new ComputeItemCost(agenda); new RewriteInvoicesJournalEntries(agenda); new OrganizationSetupJob(agenda); diff --git a/packages/server/src/services/Payment/License.ts b/packages/server/src/services/Payment/License.ts deleted file mode 100644 index 8e0fbc675..000000000 --- a/packages/server/src/services/Payment/License.ts +++ /dev/null @@ -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} - */ - 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[] = []; - - 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, - }); - } - } -} diff --git a/packages/server/src/services/Payment/LicenseMailMessages.ts b/packages/server/src/services/Payment/LicenseMailMessages.ts deleted file mode 100644 index a9b144629..000000000 --- a/packages/server/src/services/Payment/LicenseMailMessages.ts +++ /dev/null @@ -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.'); - } -} \ No newline at end of file diff --git a/packages/server/src/services/Payment/LicensePaymentMethod.ts b/packages/server/src/services/Payment/LicensePaymentMethod.ts deleted file mode 100644 index a8dbdf4ec..000000000 --- a/packages/server/src/services/Payment/LicensePaymentMethod.ts +++ /dev/null @@ -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(); - } - } -} diff --git a/packages/server/src/services/Payment/LicenseSMSMessages.ts b/packages/server/src/services/Payment/LicenseSMSMessages.ts deleted file mode 100644 index aa884ccdf..000000000 --- a/packages/server/src/services/Payment/LicenseSMSMessages.ts +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/packages/server/src/services/Payment/PaymentMethod.ts b/packages/server/src/services/Payment/PaymentMethod.ts deleted file mode 100644 index a0c1072f0..000000000 --- a/packages/server/src/services/Payment/PaymentMethod.ts +++ /dev/null @@ -1,6 +0,0 @@ -import moment from 'moment'; -import { IPaymentModel } from '@/interfaces'; - -export default class PaymentMethod implements IPaymentModel { - -} \ No newline at end of file diff --git a/packages/server/src/services/Payment/index.ts b/packages/server/src/services/Payment/index.ts deleted file mode 100644 index bec52feaf..000000000 --- a/packages/server/src/services/Payment/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IPaymentMethod, IPaymentContext } from "interfaces"; -import { Plan } from '@/system/models'; - -export default class PaymentContext implements IPaymentContext{ - paymentMethod: IPaymentMethod; - - /** - * Constructor method. - * @param {IPaymentMethod} paymentMethod - */ - constructor(paymentMethod: IPaymentMethod) { - this.paymentMethod = paymentMethod; - } - - /** - * - * @param {} paymentModel - */ - makePayment(paymentModel: PaymentModel, plan: Plan) { - return this.paymentMethod.payment(paymentModel, plan); - } -} \ No newline at end of file diff --git a/packages/server/src/services/Subscription/LemonWebhooks.ts b/packages/server/src/services/Subscription/LemonSqueezyWebhooks.ts similarity index 78% rename from packages/server/src/services/Subscription/LemonWebhooks.ts rename to packages/server/src/services/Subscription/LemonSqueezyWebhooks.ts index 68565ad92..49899ec5b 100644 --- a/packages/server/src/services/Subscription/LemonWebhooks.ts +++ b/packages/server/src/services/Subscription/LemonSqueezyWebhooks.ts @@ -1,6 +1,6 @@ import { getPrice } from '@lemonsqueezy/lemonsqueezy.js'; import { ServiceError } from '@/exceptions'; -import { Service } from 'typedi'; +import { Inject, Service } from 'typedi'; import { compareSignatures, configureLemonSqueezy, @@ -9,40 +9,41 @@ import { webhookHasMeta, } from './utils'; import { Plan } from '@/system/models'; +import { Subscription } from './Subscription'; @Service() -export class LemonWebhooks { +export class LemonSqueezyWebhooks { + @Inject() + private subscriptionService: Subscription; + /** - * + * handle the LemonSqueezy webhooks. * @param {string} rawBody * @param {string} signature - * @returns + * @returns {Promise} */ public async handlePostWebhook( rawData: any, data: Record, signature: string - ) { + ): Promise { configureLemonSqueezy(); 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 hmacSignature = createHmacSignature(secret, rawData); if (!compareSignatures(hmacSignature, signature)) { - console.log('invalid'); - return new Error('Invalid signature', { status: 400 }); + throw new Error('Invalid signature'); } // Type guard to check if the object has a 'meta' property. if (webhookHasMeta(data)) { // Non-blocking call to process the webhook event. 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 = ''; 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)) { processingError = "Event body is missing the 'meta' property."; } else if (webhookHasData(eventBody)) { @@ -78,14 +82,20 @@ export class LemonWebhooks { if (priceData.error) { processingError = `Failed to get the price data for the subscription ${eventBody.data.id}.`; } - const isUsageBased = attributes.first_subscription_item.is_usage_based; const price = isUsageBased ? priceData.data?.data.attributes.unit_price_decimal : 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_')) { // Save orders; eventBody is a "Order" diff --git a/packages/server/src/services/Subscription/Subscription.ts b/packages/server/src/services/Subscription/Subscription.ts index 4592a781f..6832f7127 100644 --- a/packages/server/src/services/Subscription/Subscription.ts +++ b/packages/server/src/services/Subscription/Subscription.ts @@ -1,37 +1,27 @@ -import { Inject } from 'typedi'; -import { Tenant, Plan } from '@/system/models'; -import { IPaymentContext } from '@/interfaces'; +import { Service } from 'typedi'; import { NotAllowedChangeSubscriptionPlan } from '@/exceptions'; +import { Plan, Tenant } from '@/system/models'; -export default class Subscription { - paymentContext: IPaymentContext | null; - - @Inject('logger') - logger: any; - - /** - * Constructor method. - * @param {IPaymentContext} - */ - constructor(payment?: IPaymentContext) { - this.paymentContext = payment; - } - +@Service() +export class Subscription { /** * Give the tenant a new subscription. - * @param {Tenant} tenant - * @param {Plan} plan + * @param {number} tenantId - Tenant id. + * @param {string} planSlug - Plan slug. * @param {string} invoiceInterval * @param {number} invoicePeriod * @param {string} subscriptionSlug */ - protected async newSubscribtion( - tenant, - plan, + public async newSubscribtion( + tenantId: number, + planSlug: string, invoiceInterval: string, invoicePeriod: number, subscriptionSlug: string = 'main' ) { + const tenant = await Tenant.query().findById(tenantId).throwIfNotFound(); + const plan = await Plan.query().findOne('slug', planSlug).throwIfNotFound(); + const subscription = await tenant .$relatedQuery('subscriptions') .modify('subscriptionBySlug', subscriptionSlug) @@ -55,26 +45,4 @@ export default class Subscription { ); } } - - /** - * 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 - ); - } } diff --git a/packages/server/src/services/Subscription/SubscriptionPeriod.ts b/packages/server/src/services/Subscription/SubscriptionPeriod.ts index c1d2e4a8b..2deb6b073 100644 --- a/packages/server/src/services/Subscription/SubscriptionPeriod.ts +++ b/packages/server/src/services/Subscription/SubscriptionPeriod.ts @@ -38,4 +38,4 @@ export default class SubscriptionPeriod { getIntervalCount() { return this.interval; } -} \ No newline at end of file +} diff --git a/packages/server/src/services/Subscription/SubscriptionService.ts b/packages/server/src/services/Subscription/SubscriptionService.ts index 0e254066b..8e70c55d8 100644 --- a/packages/server/src/services/Subscription/SubscriptionService.ts +++ b/packages/server/src/services/Subscription/SubscriptionService.ts @@ -1,60 +1,8 @@ -import { Service, Inject } from 'typedi'; -import { Plan, PlanSubscription, Tenant } 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'; +import { Service } from 'typedi'; +import { PlanSubscription } from '@/system/models'; @Service() 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. * @param {number} tenantId diff --git a/packages/server/src/services/Subscription/SubscriptionViaLicense.ts b/packages/server/src/services/Subscription/SubscriptionViaLicense.ts deleted file mode 100644 index 629f30143..000000000 --- a/packages/server/src/services/Subscription/SubscriptionViaLicense.ts +++ /dev/null @@ -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 { - /** - * Subscripe to the given plan. - * @param {Plan} plan - * @throws {NotAllowedChangeSubscriptionPlan} - */ - public async subscribe( - tenant: Tenant, - plan: Plan, - paymentModel?: PaymentModel, - subscriptionSlug: string = 'main' - ): Promise { - 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} - */ - private async newSubscriptionFromLicense( - tenant, - plan, - licenseCode: string, - subscriptionSlug: string = 'main' - ): Promise { - // License information. - const licenseInfo = await License.query().findOne( - 'licenseCode', - licenseCode - ); - return this.newSubscribtion( - tenant, - plan, - licenseInfo.periodInterval, - licenseInfo.licensePeriod, - subscriptionSlug - ); - } -} diff --git a/packages/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js b/packages/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js deleted file mode 100644 index 43fea2798..000000000 --- a/packages/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js +++ /dev/null @@ -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'); -}; diff --git a/packages/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js b/packages/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js deleted file mode 100644 index 6babd6f03..000000000 --- a/packages/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js +++ /dev/null @@ -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'); -}; diff --git a/packages/server/src/system/models/Subscriptions/PlanFeature.ts b/packages/server/src/system/models/Subscriptions/PlanFeature.ts deleted file mode 100644 index 178fe818e..000000000 --- a/packages/server/src/system/models/Subscriptions/PlanFeature.ts +++ /dev/null @@ -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', - }, - }, - }; - } -} diff --git a/packages/server/src/system/models/Tenant.ts b/packages/server/src/system/models/Tenant.ts index a40966797..b715adc8c 100644 --- a/packages/server/src/system/models/Tenant.ts +++ b/packages/server/src/system/models/Tenant.ts @@ -147,9 +147,9 @@ export default class Tenant extends BaseModel { /** * Marks the given tenant as upgrading. - * @param {number} tenantId - * @param {string} upgradeJobId - * @returns + * @param {number} tenantId + * @param {string} upgradeJobId + * @returns */ static markAsUpgrading(tenantId, upgradeJobId) { return this.query().update({ upgradeJobId }).where({ id: tenantId }); @@ -157,8 +157,8 @@ export default class Tenant extends BaseModel { /** * Markes the given tenant as upgraded. - * @param {number} tenantId - * @returns + * @param {number} tenantId + * @returns */ static markAsUpgraded(tenantId) { return this.query().update({ upgradeJobId: null }).where({ id: tenantId }); @@ -185,4 +185,44 @@ export default class Tenant extends BaseModel { saveMetadata(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(), + }); + } } diff --git a/packages/server/src/system/models/index.ts b/packages/server/src/system/models/index.ts index 8d8f41146..4c21f755c 100644 --- a/packages/server/src/system/models/index.ts +++ b/packages/server/src/system/models/index.ts @@ -1,5 +1,4 @@ import Plan from './Subscriptions/Plan'; -import PlanFeature from './Subscriptions/PlanFeature'; import PlanSubscription from './Subscriptions/PlanSubscription'; import License from './Subscriptions/License'; import Tenant from './Tenant'; @@ -12,7 +11,6 @@ import { Import } from './Import'; export { Plan, - PlanFeature, PlanSubscription, License, Tenant,