From dbb4e4de47806f5d2676dccae53fd2b4621efd3b Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sat, 13 Mar 2021 15:36:03 +0200 Subject: [PATCH] fix(biling): Subscription billing system. fix(GeneralLedger): running balance. --- .../api/controllers/Subscription/Licenses.ts | 195 ++++++++---------- .../controllers/Subscription/PaymentMethod.ts | 1 + .../src/api/controllers/Subscription/index.ts | 68 +++--- server/src/exceptions/VoucherCodeRequired.ts | 6 + server/src/exceptions/index.ts | 2 + server/src/interfaces/License.ts | 8 + .../GeneralLedger/GeneralLedger.ts | 23 +-- server/src/services/Payment/License.ts | 150 +++++++++++--- .../services/Payment/LicensePaymentMethod.ts | 32 ++- .../src/services/Subscription/Subscription.ts | 6 +- ...091642_create_subscriptions_plans_table.js | 2 - .../system/seeds/seed_subscriptions_plans.js | 33 ++- 12 files changed, 310 insertions(+), 216 deletions(-) create mode 100644 server/src/exceptions/VoucherCodeRequired.ts diff --git a/server/src/api/controllers/Subscription/Licenses.ts b/server/src/api/controllers/Subscription/Licenses.ts index 6db07831e..d090c7aa9 100644 --- a/server/src/api/controllers/Subscription/Licenses.ts +++ b/server/src/api/controllers/Subscription/Licenses.ts @@ -1,13 +1,14 @@ import { Service, Inject } from 'typedi'; -import { Router, Request, Response } from 'express'; +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, Plan } from 'system/models'; +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 } from 'interfaces'; +import { ILicensesFilter, ISendLicenseDTO } from 'interfaces'; @Service() export default class LicensesController extends BaseController { @@ -32,26 +33,26 @@ export default class LicensesController extends BaseController { '/generate', this.generateLicenseSchema, this.validationResult, - asyncMiddleware(this.validatePlanExistance.bind(this)), - asyncMiddleware(this.generateLicense.bind(this)) + asyncMiddleware(this.generateLicense.bind(this)), + this.catchServiceErrors, ); router.post( '/disable/:licenseId', this.validationResult, - asyncMiddleware(this.validateLicenseExistance.bind(this)), - asyncMiddleware(this.validateNotDisabledLicense.bind(this)), - asyncMiddleware(this.disableLicense.bind(this)) + asyncMiddleware(this.disableLicense.bind(this)), + this.catchServiceErrors, ); router.post( '/send', this.sendLicenseSchemaValidation, this.validationResult, - asyncMiddleware(this.sendLicense.bind(this)) + asyncMiddleware(this.sendLicense.bind(this)), + this.catchServiceErrors, ); router.delete( '/:licenseId', - asyncMiddleware(this.validateLicenseExistance.bind(this)), - asyncMiddleware(this.deleteLicense.bind(this)) + asyncMiddleware(this.deleteLicense.bind(this)), + this.catchServiceErrors, ); router.get('/', asyncMiddleware(this.listLicenses.bind(this))); return router; @@ -67,7 +68,7 @@ export default class LicensesController extends BaseController { check('period_interval') .exists() .isIn(['month', 'months', 'year', 'years', 'day', 'days']), - check('plan_id').exists().isNumeric().toInt(), + check('plan_slug').exists().trim().escape(), ]; } @@ -90,7 +91,7 @@ export default class LicensesController extends BaseController { return [ check('period').exists().isNumeric(), check('period_interval').exists().trim().escape(), - check('plan_id').exists().isNumeric().toInt(), + check('plan_slug').exists().trim().escape(), oneOf([ check('phone_number').exists().trim().escape(), check('email').exists().trim().escape(), @@ -98,67 +99,6 @@ export default class LicensesController extends BaseController { ]; } - /** - * Validate the plan existance on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validatePlanExistance(req: Request, res: Response, next: Function) { - const body = this.matchedBodyData(req); - const planId: number = body.planId || req.params.planId; - const foundPlan = await Plan.query().findById(planId); - - if (!foundPlan) { - return res.status(400).send({ - erorrs: [{ type: 'PLAN.NOT.FOUND', code: 100 }], - }); - } - next(); - } - - /** - * Valdiate the license existance on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} - */ - async validateLicenseExistance(req: Request, res: Response, next: Function) { - const body = this.matchedBodyData(req); - - const licenseId = body.licenseId || req.params.licenseId; - const foundLicense = await License.query().findById(licenseId); - - if (!foundLicense) { - return res.status(400).send({ - errors: [{ type: 'LICENSE.NOT.FOUND', code: 200 }], - }); - } - next(); - } - - /** - * Validates whether the license id is disabled. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateNotDisabledLicense( - req: Request, - res: Response, - next: Function - ) { - const licenseId = req.params.licenseId || req.query.licenseId; - const foundLicense = await License.query().findById(licenseId); - - if (foundLicense.disabled) { - return res.status(400).send({ - errors: [{ type: 'LICENSE.ALREADY.DISABLED', code: 200 }], - }); - } - next(); - } - /** * Generate licenses codes with given period in bulk. * @param {Request} req @@ -166,7 +106,7 @@ export default class LicensesController extends BaseController { * @return {Response} */ async generateLicense(req: Request, res: Response, next: Function) { - const { loop = 10, period, periodInterval, planId } = this.matchedBodyData( + const { loop = 10, period, periodInterval, planSlug } = this.matchedBodyData( req ); @@ -175,7 +115,7 @@ export default class LicensesController extends BaseController { loop, period, periodInterval, - planId + planSlug ); return res.status(200).send({ code: 100, @@ -193,12 +133,16 @@ export default class LicensesController extends BaseController { * @param {Response} res * @return {Response} */ - async disableLicense(req: Request, res: Response) { + async disableLicense(req: Request, res: Response, next: Function) { const { licenseId } = req.params; - await this.licenseService.disableLicense(licenseId); + try { + await this.licenseService.disableLicense(licenseId); - return res.status(200).send({ license_id: licenseId }); + return res.status(200).send({ license_id: licenseId }); + } catch (error) { + next(error); + } } /** @@ -207,12 +151,16 @@ export default class LicensesController extends BaseController { * @param {Response} res * @return {Response} */ - async deleteLicense(req: Request, res: Response) { + async deleteLicense(req: Request, res: Response, next: Function) { const { licenseId } = req.params; - await this.licenseService.deleteLicense(licenseId); + try { + await this.licenseService.deleteLicense(licenseId); - return res.status(200).send({ license_id: licenseId }); + return res.status(200).send({ license_id: licenseId }); + } catch (error) { + next(error) + } } /** @@ -221,40 +169,20 @@ export default class LicensesController extends BaseController { * @param {Response} res * @return {Response} */ - async sendLicense(req: Request, res: Response) { - const { - phoneNumber, - email, - period, - periodInterval, - planId, - } = this.matchedBodyData(req); + async sendLicense(req: Request, res: Response, next: Function) { + const sendLicenseDTO: ISendLicenseDTO = this.matchedBodyData(req); - const license = await License.query() - .modify('filterActiveLicense') - .where('license_period', period) - .where('period_interval', periodInterval) - .where('plan_id', planId) - .first(); + try { + await this.licenseService.sendLicenseToCustomer(sendLicenseDTO); - if (!license) { - 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', + 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); } - await this.licenseService.sendLicenseToCustomer( - license.licenseCode, - phoneNumber, - email - ); - return res.status(200).send({ - status: 100, - code: 'LICENSE.CODE.SENT', - message: 'The license has been sent to the given customer.', - }); } /** @@ -276,4 +204,47 @@ export default class LicensesController extends BaseController { }); 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/server/src/api/controllers/Subscription/PaymentMethod.ts b/server/src/api/controllers/Subscription/PaymentMethod.ts index fdc1b0c53..73d3e5932 100644 --- a/server/src/api/controllers/Subscription/PaymentMethod.ts +++ b/server/src/api/controllers/Subscription/PaymentMethod.ts @@ -1,4 +1,5 @@ import { Inject } from 'typedi'; +import { Request, Response } from 'express'; import { Plan } from 'system/models'; import BaseController from 'api/controllers/BaseController'; import SubscriptionService from 'services/Subscription/SubscriptionService'; diff --git a/server/src/api/controllers/Subscription/index.ts b/server/src/api/controllers/Subscription/index.ts index 77e727193..3eecb80a1 100644 --- a/server/src/api/controllers/Subscription/index.ts +++ b/server/src/api/controllers/Subscription/index.ts @@ -1,4 +1,4 @@ -import { Router, Request, Response, NextFunction } from 'express' +import { Router, Request, Response, NextFunction } from 'express'; import { Container, Service, Inject } from 'typedi'; import JWTAuth from 'api/middleware/jwtAuth'; import TenancyMiddleware from 'api/middleware/TenancyMiddleware'; @@ -9,43 +9,41 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware'; @Service() export default class SubscriptionController { - @Inject() - subscriptionService: SubscriptionService; + @Inject() + subscriptionService: SubscriptionService; - /** - * Router constructor. - */ - router() { - const router = Router(); + /** + * Router constructor. + */ + router() { + const router = Router(); - router.use(JWTAuth); - router.use(AttachCurrentTenantUser); - router.use(TenancyMiddleware); + router.use(JWTAuth); + router.use(AttachCurrentTenantUser); + router.use(TenancyMiddleware); - router.use( - '/license', - Container.get(PaymentViaLicenseController).router() - ); - router.get('/', - asyncMiddleware(this.getSubscriptions.bind(this)) - ); - return router; - } + router.use('/license', Container.get(PaymentViaLicenseController).router()); + router.get('/', asyncMiddleware(this.getSubscriptions.bind(this))); - /** - * Retrieve all subscriptions of the authenticated user's tenant. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async getSubscriptions(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; + return router; + } - try { - const subscriptions = await this.subscriptionService.getSubscriptions(tenantId); - return res.status(200).send({ subscriptions }); - } catch (error) { - next(error); - } - } + /** + * Retrieve all subscriptions of the authenticated user's tenant. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async getSubscriptions(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + + try { + const subscriptions = await this.subscriptionService.getSubscriptions( + tenantId + ); + return res.status(200).send({ subscriptions }); + } catch (error) { + next(error); + } + } } diff --git a/server/src/exceptions/VoucherCodeRequired.ts b/server/src/exceptions/VoucherCodeRequired.ts new file mode 100644 index 000000000..b92eb155c --- /dev/null +++ b/server/src/exceptions/VoucherCodeRequired.ts @@ -0,0 +1,6 @@ + +export default class VoucherCodeRequired { + constructor() { + this.name = 'VoucherCodeRequired'; + } +} diff --git a/server/src/exceptions/index.ts b/server/src/exceptions/index.ts index 0ec59b132..a18746d02 100644 --- a/server/src/exceptions/index.ts +++ b/server/src/exceptions/index.ts @@ -8,6 +8,7 @@ import TenantAlreadyInitialized from './TenantAlreadyInitialized'; import TenantAlreadySeeded from './TenantAlreadySeeded'; import TenantDBAlreadyExists from './TenantDBAlreadyExists'; import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt'; +import VoucherCodeRequired from './VoucherCodeRequired'; export { NotAllowedChangeSubscriptionPlan, @@ -20,4 +21,5 @@ export { TenantAlreadySeeded, TenantDBAlreadyExists, TenantDatabaseNotBuilt, + VoucherCodeRequired, }; \ No newline at end of file diff --git a/server/src/interfaces/License.ts b/server/src/interfaces/License.ts index b2d1e47b6..e58e9a6ea 100644 --- a/server/src/interfaces/License.ts +++ b/server/src/interfaces/License.ts @@ -14,4 +14,12 @@ export interface ILicensesFilter { disabld: boolean, used: boolean, sent: boolean, +}; + +export interface ISendLicenseDTO { + phoneNumber: string, + email: string, + period: string, + periodInterval: string, + planSlug: string, }; \ No newline at end of file diff --git a/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index 0c4a88eaa..608030b8e 100644 --- a/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -1,4 +1,4 @@ -import { pick, get, last } from 'lodash'; +import { isEmpty, get, last } from 'lodash'; import { IGeneralLedgerSheetQuery, IGeneralLedgerSheetAccount, @@ -73,10 +73,9 @@ export default class GeneralLedgerSheet extends FinancialSheet { entryReducer( entries: IGeneralLedgerSheetAccountTransaction[], entry: IJournalEntry, - index: number + openingBalance: number ): IGeneralLedgerSheetAccountTransaction[] { const lastEntry = last(entries); - const openingBalance = 0; const contact = this.contactsMap.get(entry.contactId); const amount = this.getAmount( @@ -85,11 +84,7 @@ export default class GeneralLedgerSheet extends FinancialSheet { entry.accountNormal ); const runningBalance = - (entries.length === 0 - ? openingBalance - : lastEntry - ? lastEntry.runningBalance - : 0) + amount; + amount + (!isEmpty(entries) ? lastEntry.runningBalance : openingBalance); const newEntry = { date: entry.date, @@ -182,9 +177,7 @@ export default class GeneralLedgerSheet extends FinancialSheet { * @param {IAccount} account * @return {IGeneralLedgerSheetAccount} */ - private accountMapper( - account: IAccount - ): IGeneralLedgerSheetAccount { + private accountMapper(account: IAccount): IGeneralLedgerSheetAccount { const openingBalance = this.accountOpeningBalance(account); const closingBalance = this.accountClosingBalance(account); @@ -208,14 +201,10 @@ export default class GeneralLedgerSheet extends FinancialSheet { * @param {IAccount[]} accounts - * @return {IGeneralLedgerSheetAccount[]} */ - private accountsWalker( - accounts: IAccount[] - ): IGeneralLedgerSheetAccount[] { + private accountsWalker(accounts: IAccount[]): IGeneralLedgerSheetAccount[] { return ( accounts - .map((account: IAccount) => - this.accountMapper(account) - ) + .map((account: IAccount) => this.accountMapper(account)) // Filter general ledger accounts that have no transactions // when`noneTransactions` is on. .filter( diff --git a/server/src/services/Payment/License.ts b/server/src/services/Payment/License.ts index 1e7c8a337..c56325a44 100644 --- a/server/src/services/Payment/License.ts +++ b/server/src/services/Payment/License.ts @@ -1,10 +1,18 @@ import { Service, Container, Inject } from 'typedi'; import cryptoRandomString from 'crypto-random-string'; import { times } from 'lodash'; -import { License } from "system/models"; -import { ILicense } from 'interfaces'; +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 { @@ -14,49 +22,99 @@ export default class LicenseService { @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 + * @param {number} licensePeriod * @return {Promise} */ - async generateLicense( + public async generateLicense( licensePeriod: number, periodInterval: string = 'days', - planId: number, + planSlug: string ): ILicense { let licenseCode: string; let repeat: boolean = true; - while(repeat) { - licenseCode = cryptoRandomString({ length: 10, type: 'numeric' }); - const foundLicenses = await License.query().where('license_code', licenseCode); + // Retrieve plan or throw not found error. + const plan = await this.getPlanOrThrowError(planSlug); - if (foundLicenses.length === 0) { - repeat = false; + 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, + licenseCode, + licensePeriod, + periodInterval, + planId: plan.id, }); } /** - * - * @param {number} loop - * @param {number} licensePeriod - * @param {string} periodInterval - * @param {number} planId + * Generates licenses. + * @param {number} loop + * @param {number} licensePeriod + * @param {string} periodInterval + * @param {number} planId */ - async generateLicenses( + public async generateLicenses( loop = 1, licensePeriod: number, periodInterval: string = 'days', - planId: number, + planSlug: string ) { const asyncOpers: Promise[] = []; times(loop, () => { - const generateOper = this.generateLicense(licensePeriod, periodInterval, planId); + const generateOper = this.generateLicense( + licensePeriod, + periodInterval, + planSlug + ); asyncOpers.push(generateOper); }); return Promise.all(asyncOpers); @@ -64,38 +122,64 @@ export default class LicenseService { /** * Disables the given license id on the storage. - * @param {number} licenseId + * @param {string} licenseSlug - License slug. * @return {Promise} */ - async disableLicense(licenseId: number) { - return License.markLicenseAsDisabled(licenseId, 'id'); + 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 licenseId + * @param licenseSlug {string} - License slug. */ - async deleteLicense(licenseId: number) { - return License.query().where('id', licenseId).delete(); + 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} licenseCode - License code. + * @param {string} phoneNumber - Phone number. * @param {string} email - Email address. */ - async sendLicenseToCustomer(licenseCode: string, phoneNumber: string, email: string) { + 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(licenseCode); + await License.markLicenseAsSent(license.licenseCode); - if (email) { - await agenda.schedule('1 second', 'send-license-via-email', { licenseCode, email }); + 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, phoneNumber }); + await agenda.schedule('1 second', 'send-license-via-phone', { + licenseCode: license.licenseCode, + phoneNumber, + }); } } -} \ No newline at end of file +} diff --git a/server/src/services/Payment/LicensePaymentMethod.ts b/server/src/services/Payment/LicensePaymentMethod.ts index 88e428892..376726804 100644 --- a/server/src/services/Payment/LicensePaymentMethod.ts +++ b/server/src/services/Payment/LicensePaymentMethod.ts @@ -1,16 +1,24 @@ -import { License } from "system/models"; +import { License } from 'system/models'; import PaymentMethod from 'services/Payment/PaymentMethod'; import { Plan } from 'system/models'; import { IPaymentMethod, ILicensePaymentModel } from 'interfaces'; -import { ILicensePaymentModel } from "interfaces"; -import { PaymentInputInvalid, PaymentAmountInvalidWithPlan } from 'exceptions'; +import { ILicensePaymentModel } from 'interfaces'; +import { + PaymentInputInvalid, + PaymentAmountInvalidWithPlan, + VoucherCodeRequired, +} from 'exceptions'; -export default class LicensePaymentMethod extends PaymentMethod implements IPaymentMethod { +export default class LicensePaymentMethod + extends PaymentMethod + implements IPaymentMethod { /** * Payment subscription of organization via license code. * @param {ILicensePaymentModel} licensePaymentModel - */ async payment(licensePaymentModel: ILicensePaymentModel, plan: Plan) { + this.validateLicensePaymentModel(licensePaymentModel); + const license = await this.getLicenseOrThrowInvalid(licensePaymentModel); this.validatePaymentAmountWithPlan(license, plan); @@ -36,12 +44,22 @@ export default class LicensePaymentMethod extends PaymentMethod implements IPaym /** * Validates the payment amount with given plan price. - * @param {License} license - * @param {Plan} plan + * @param {License} license + * @param {Plan} plan */ validatePaymentAmountWithPlan(license: License, plan: Plan) { if (license.planId !== plan.id) { throw new PaymentAmountInvalidWithPlan(); } } -} \ No newline at end of file + + /** + * Validate voucher payload. + * @param {ILicensePaymentModel} licenseModel - + */ + validateLicensePaymentModel(licenseModel: ILicensePaymentModel) { + if (!licenseModel || !licenseModel.licenseCode) { + throw new VoucherCodeRequired(); + } + } +} diff --git a/server/src/services/Subscription/Subscription.ts b/server/src/services/Subscription/Subscription.ts index 3bfa31f6e..4b112f94b 100644 --- a/server/src/services/Subscription/Subscription.ts +++ b/server/src/services/Subscription/Subscription.ts @@ -31,10 +31,8 @@ export default class Subscription { ) { this.validateIfPlanHasPriceNoPayment(plan, paymentModel); - // @todo - if (plan.price > 0) { - await this.paymentContext.makePayment(paymentModel, plan); - } + await this.paymentContext.makePayment(paymentModel, plan); + const subscription = await tenant.$relatedQuery('subscriptions') .modify('subscriptionBySlug', subscriptionSlug) .first(); diff --git a/server/src/system/migrations/20200527091642_create_subscriptions_plans_table.js b/server/src/system/migrations/20200527091642_create_subscriptions_plans_table.js index 90ac9c4b6..09d890648 100644 --- a/server/src/system/migrations/20200527091642_create_subscriptions_plans_table.js +++ b/server/src/system/migrations/20200527091642_create_subscriptions_plans_table.js @@ -6,7 +6,6 @@ exports.up = function(knex) { table.string('name'); table.string('description'); table.decimal('price'); - table.decimal('signup_fee'); table.string('currency', 3); table.integer('trial_period'); @@ -14,7 +13,6 @@ exports.up = function(knex) { table.integer('invoice_period'); table.string('invoice_interval'); - table.timestamps(); }); }; diff --git a/server/src/system/seeds/seed_subscriptions_plans.js b/server/src/system/seeds/seed_subscriptions_plans.js index 1bedcab87..7c3810662 100644 --- a/server/src/system/seeds/seed_subscriptions_plans.js +++ b/server/src/system/seeds/seed_subscriptions_plans.js @@ -6,21 +6,42 @@ exports.seed = (knex) => { // Inserts seed entries return knex('subscription_plans').insert([ { - id: 1, - name: 'free', + name: 'Free', slug: 'free', price: 0, active: true, currency: 'LYD', - trial_period: 15, + trial_period: 7, trial_interval: 'days', - invoice_period: 3, + index: 1, + voucher_required: true, + }, + { + name: 'Starter', + slug: 'starter', + price: 500, + active: true, + currency: 'LYD', + + invoice_period: 12, invoice_interval: 'month', - index: 1, - } + index: 2, + }, + { + name: 'Growth', + slug: 'growth', + price: 1000, + active: true, + currency: 'LYD', + + invoice_period: 12, + invoice_interval: 'month', + + index: 3, + }, ]); }); };