diff --git a/server/src/api/controllers/FinancialStatements/APAgingSummary.ts b/server/src/api/controllers/FinancialStatements/APAgingSummary.ts index 5f18f2555..f827019fd 100644 --- a/server/src/api/controllers/FinancialStatements/APAgingSummary.ts +++ b/server/src/api/controllers/FinancialStatements/APAgingSummary.ts @@ -45,28 +45,19 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC const { tenantId, settings } = req; const filter = this.matchedQueryData(req); - const organizationName = settings.get({ - group: 'organization', - key: 'name', - }); - const baseCurrency = settings.get({ - group: 'organization', - key: 'base_currency', - }); - try { const { data, columns, query, + meta } = await this.APAgingSummaryService.APAgingSummary(tenantId, filter); return res.status(200).send({ - organization_name: organizationName, - base_currency: baseCurrency, data: this.transfromToResponse(data), columns: this.transfromToResponse(columns), query: this.transfromToResponse(query), + meta: this.transfromToResponse(meta) }); } catch (error) { next(error); diff --git a/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts b/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts index 979eea0c5..fbf79195c 100644 --- a/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts +++ b/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts @@ -25,7 +25,7 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC } /** - * Receivable aging summary validation roles. + * AR aging summary validation roles. */ get validationSchema() { return [ @@ -41,34 +41,25 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC } /** - * Retrieve receivable aging summary report. + * Retrieve AR aging summary report. */ async receivableAgingSummary(req: Request, res: Response) { const { tenantId, settings } = req; const filter = this.matchedQueryData(req); - const organizationName = settings.get({ - group: 'organization', - key: 'name', - }); - const baseCurrency = settings.get({ - group: 'organization', - key: 'base_currency', - }); - try { const { data, columns, query, + meta, } = await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter); return res.status(200).send({ - organization_name: organizationName, - base_currency: baseCurrency, data: this.transfromToResponse(data), columns: this.transfromToResponse(columns), query: this.transfromToResponse(query), + meta: this.transfromToResponse(meta), }); } catch (error) { console.log(error); diff --git a/server/src/api/controllers/FinancialStatements/BalanceSheet.ts b/server/src/api/controllers/FinancialStatements/BalanceSheet.ts index 46163c04b..c8bca9cc8 100644 --- a/server/src/api/controllers/FinancialStatements/BalanceSheet.ts +++ b/server/src/api/controllers/FinancialStatements/BalanceSheet.ts @@ -59,28 +59,20 @@ export default class BalanceSheetStatementController extends BaseFinancialReport ...filter, accountsIds: castArray(filter.accountsIds), }; - const organizationName = settings.get({ - group: 'organization', - key: 'name', - }); - const baseCurrency = settings.get({ - group: 'organization', - key: 'base_currency', - }); - + try { const { data, columns, query, + meta, } = await this.balanceSheetService.balanceSheet(tenantId, filter); return res.status(200).send({ - organization_name: organizationName, - base_currency: baseCurrency, data: this.transfromToResponse(data), columns: this.transfromToResponse(columns), query: this.transfromToResponse(query), + meta: this.transfromToResponse(meta), }); } catch (error) { next(error); diff --git a/server/src/api/controllers/FinancialStatements/GeneralLedger.ts b/server/src/api/controllers/FinancialStatements/GeneralLedger.ts index 071921a43..ce4ce3e44 100644 --- a/server/src/api/controllers/FinancialStatements/GeneralLedger.ts +++ b/server/src/api/controllers/FinancialStatements/GeneralLedger.ts @@ -51,24 +51,14 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo async generalLedger(req: Request, res: Response, next: NextFunction) { const { tenantId, settings } = req; const filter = this.matchedQueryData(req); - - const organizationName = settings.get({ - group: 'organization', - key: 'name', - }); - const baseCurrency = settings.get({ - group: 'organization', - key: 'base_currency', - }); - + try { - const { data, query } = await this.generalLedgetService.generalLedger( + const { data, query, meta } = await this.generalLedgetService.generalLedger( tenantId, filter ); return res.status(200).send({ - organization_name: organizationName, - base_currency: baseCurrency, + meta: this.transfromToResponse(meta), data: this.transfromToResponse(data), query: this.transfromToResponse(query), }); diff --git a/server/src/api/controllers/FinancialStatements/JournalSheet.ts b/server/src/api/controllers/FinancialStatements/JournalSheet.ts index 350a80dbb..86748f6a4 100644 --- a/server/src/api/controllers/FinancialStatements/JournalSheet.ts +++ b/server/src/api/controllers/FinancialStatements/JournalSheet.ts @@ -66,26 +66,17 @@ export default class JournalSheetController extends BaseFinancialReportControlle ...filter, accountsIds: castArray(filter.accountsIds), }; - const organizationName = settings.get({ - group: 'organization', - key: 'name', - }); - const baseCurrency = settings.get({ - group: 'organization', - key: 'base_currency', - }); try { - const { data, query } = await this.journalService.journalSheet( + const { data, query, meta } = await this.journalService.journalSheet( tenantId, filter ); return res.status(200).send({ - organization_name: organizationName, - base_currency: baseCurrency, data: this.transfromToResponse(data), query: this.transfromToResponse(query), + meta: this.transfromToResponse(meta), }); } catch (error) { next(error); diff --git a/server/src/api/controllers/FinancialStatements/ProfitLossSheet.ts b/server/src/api/controllers/FinancialStatements/ProfitLossSheet.ts index e5b5125fb..bf2b9da95 100644 --- a/server/src/api/controllers/FinancialStatements/ProfitLossSheet.ts +++ b/server/src/api/controllers/FinancialStatements/ProfitLossSheet.ts @@ -54,22 +54,19 @@ export default class ProfitLossSheetController extends BaseFinancialReportContro const { tenantId, settings } = req; const filter = this.matchedQueryData(req); - const organizationName = settings.get({ group: 'organization', key: 'name' }); - const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' }); - try { const { data, columns, query, + meta } = await this.profitLossSheetService.profitLossSheet(tenantId, filter); return res.status(200).send({ - organization_name: organizationName, - base_currency: baseCurrency, data: this.transfromToResponse(data), columns: this.transfromToResponse(columns), query: this.transfromToResponse(query), + meta: this.transfromToResponse(meta) }); } catch (error) { next(error); diff --git a/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts b/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts index 9ea0f4113..6255ae8a9 100644 --- a/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts +++ b/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts @@ -58,29 +58,21 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont ...filter, accountsIds: castArray(filter.accountsIds), }; - const organizationName = settings.get({ - group: 'organization', - key: 'name', - }); - const baseCurrency = settings.get({ - group: 'organization', - key: 'base_currency', - }); - + try { const { data, query, + meta } = await this.trialBalanceSheetService.trialBalanceSheet( tenantId, filter ); return res.status(200).send({ - organization_name: organizationName, - base_currency: baseCurrency, data: this.transfromToResponse(data), query: this.transfromToResponse(query), + meta: this.transfromToResponse(meta), }); } catch (error) { next(error); 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/APAgingSummaryReport.ts b/server/src/interfaces/APAgingSummaryReport.ts index dd62b4b1f..451b6bc85 100644 --- a/server/src/interfaces/APAgingSummaryReport.ts +++ b/server/src/interfaces/APAgingSummaryReport.ts @@ -34,4 +34,16 @@ export interface IAPAgingSummaryData { total: IAPAgingSummaryTotal, }; -export type IAPAgingSummaryColumns = IAgingPeriod[]; \ No newline at end of file +export type IAPAgingSummaryColumns = IAgingPeriod[]; + + +export interface IARAgingSummaryMeta { + baseCurrency: string, + organizationName: string, +} + + +export interface IAPAgingSummaryMeta { + baseCurrency: string, + organizationName: string, +} \ No newline at end of file diff --git a/server/src/interfaces/ARAgingSummaryReport.ts b/server/src/interfaces/ARAgingSummaryReport.ts index 20e3df42d..2313fe5c7 100644 --- a/server/src/interfaces/ARAgingSummaryReport.ts +++ b/server/src/interfaces/ARAgingSummaryReport.ts @@ -29,3 +29,8 @@ export interface IARAgingSummaryData { } export type IARAgingSummaryColumns = IAgingPeriod[]; + +export interface IARAgingSummaryMeta { + organizationName: string, + baseCurrency: string, +} \ No newline at end of file diff --git a/server/src/interfaces/BalanceSheet.ts b/server/src/interfaces/BalanceSheet.ts index cf330b1dc..29f6bf337 100644 --- a/server/src/interfaces/BalanceSheet.ts +++ b/server/src/interfaces/BalanceSheet.ts @@ -15,10 +15,16 @@ export interface IBalanceSheetQuery { accountIds: number[]; } +export interface IBalanceSheetMeta { + isCostComputeRunning: boolean, + organizationName: string, + baseCurrency: string, +}; + export interface IBalanceSheetFormatNumberSettings extends IFormatNumberSettings { type: string; -} +}; export interface IBalanceSheetStatementService { balanceSheet( @@ -35,6 +41,7 @@ export interface IBalanceSheetStatement { query: IBalanceSheetQuery; columns: IBalanceSheetStatementColumns; data: IBalanceSheetStatementData; + meta: IBalanceSheetMeta; } export interface IBalanceSheetStructureSection { diff --git a/server/src/interfaces/GeneralLedgerSheet.ts b/server/src/interfaces/GeneralLedgerSheet.ts index 84d0cc607..b071fbd57 100644 --- a/server/src/interfaces/GeneralLedgerSheet.ts +++ b/server/src/interfaces/GeneralLedgerSheet.ts @@ -71,4 +71,10 @@ export interface IAccountTransaction { date: string|Date, createdAt: string|Date, updatedAt: string|Date, -} \ No newline at end of file +} + +export interface IGeneralLedgerMeta { + isCostComputeRunning: boolean, + organizationName: string, + baseCurrency: string, +}; \ No newline at end of file diff --git a/server/src/interfaces/JournalReport.ts b/server/src/interfaces/JournalReport.ts index b3b21d92a..d7ff7a738 100644 --- a/server/src/interfaces/JournalReport.ts +++ b/server/src/interfaces/JournalReport.ts @@ -25,4 +25,10 @@ export interface IJournalReportEntriesGroup { export interface IJournalReport { entries: IJournalReportEntriesGroup[], +} + +export interface IJournalSheetMeta { + isCostComputeRunning: boolean, + organizationName: string, + baseCurrency: string, } \ 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/interfaces/ProfitLossSheet.ts b/server/src/interfaces/ProfitLossSheet.ts index f070c1f1b..ce3e849a0 100644 --- a/server/src/interfaces/ProfitLossSheet.ts +++ b/server/src/interfaces/ProfitLossSheet.ts @@ -54,4 +54,10 @@ export interface IProfitLossSheetStatement { netIncome: IProfitLossSheetTotalSection; operatingProfit: IProfitLossSheetTotalSection; grossProfit: IProfitLossSheetTotalSection; -}; \ No newline at end of file +}; + +export interface IProfitLossSheetMeta { + isCostComputeRunning: boolean, + organizationName: string, + baseCurrency: string, +} \ No newline at end of file diff --git a/server/src/interfaces/TrialBalanceSheet.ts b/server/src/interfaces/TrialBalanceSheet.ts index abd42e5a6..2c761eee5 100644 --- a/server/src/interfaces/TrialBalanceSheet.ts +++ b/server/src/interfaces/TrialBalanceSheet.ts @@ -21,6 +21,12 @@ export interface ITrialBalanceTotal { formattedBalance: string; } +export interface ITrialBalanceSheetMeta { + isCostComputeRunning: boolean, + organizationName: string, + baseCurrency: string, +}; + export interface ITrialBalanceAccount extends ITrialBalanceTotal { id: number; parentAccountId: number; @@ -38,4 +44,5 @@ export type ITrialBalanceSheetData = { export interface ITrialBalanceStatement { data: ITrialBalanceSheetData; query: ITrialBalanceSheetQuery; + meta: ITrialBalanceSheetMeta, } diff --git a/server/src/jobs/ComputeItemCost.ts b/server/src/jobs/ComputeItemCost.ts index ecc24ea08..f68e99c11 100644 --- a/server/src/jobs/ComputeItemCost.ts +++ b/server/src/jobs/ComputeItemCost.ts @@ -1,8 +1,5 @@ import { Container } from 'typedi'; import {EventDispatcher} from "event-dispatch"; -// import { -// EventDispatcher, -// } from 'decorators/eventDispatcher'; import events from 'subscribers/events'; import InventoryService from 'services/Inventory/Inventory'; @@ -11,7 +8,7 @@ export default class ComputeItemCostJob { eventDispatcher: EventDispatcher; /** - * + * Constructor method. * @param agenda */ constructor(agenda) { diff --git a/server/src/jobs/writeInvoicesJEntries.ts b/server/src/jobs/writeInvoicesJEntries.ts index 4523dcb62..c0eed27d7 100644 --- a/server/src/jobs/writeInvoicesJEntries.ts +++ b/server/src/jobs/writeInvoicesJEntries.ts @@ -1,13 +1,24 @@ import { Container } from 'typedi'; +import {EventDispatcher} from "event-dispatch"; +import events from 'subscribers/events'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; export default class WriteInvoicesJournalEntries { + eventDispatcher: EventDispatcher; + + /** + * Constructor method. + */ constructor(agenda) { + const eventName = 'rewrite-invoices-journal-entries'; + this.eventDispatcher = new EventDispatcher(); + agenda.define( - 'rewrite-invoices-journal-entries', + eventName, { priority: 'normal', concurrency: 1 }, this.handler.bind(this) ); + agenda.on(`complete:${eventName}`, this.onJobCompleted.bind(this)); } public async handler(job, done: Function): Promise { @@ -36,4 +47,16 @@ export default class WriteInvoicesJournalEntries { done(e); } } + + /** + * Handle the job complete. + */ + async onJobCompleted(job) { + const { startingDate, itemId, tenantId } = job.attrs.data; + + await this.eventDispatcher.dispatch( + events.inventory.onInventoryCostEntriesWritten, + { startingDate, itemId, tenantId } + ); + } } diff --git a/server/src/models/AccountTransaction.js b/server/src/models/AccountTransaction.js index 291c984f4..afb8b26ec 100644 --- a/server/src/models/AccountTransaction.js +++ b/server/src/models/AccountTransaction.js @@ -17,6 +17,9 @@ export default class AccountTransaction extends TenantModel { return ['createdAt']; } + /** + * Virtual attributes. + */ static get virtualAttributes() { return ['referenceTypeFormatted']; } @@ -37,6 +40,7 @@ export default class AccountTransaction extends TenantModel { 'SaleInvoice': 'Sale invoice', 'SaleReceipt': 'Sale receipt', 'PaymentReceive': 'Payment receive', + 'Bill': 'Bill', 'BillPayment': 'Payment made', 'VendorOpeningBalance': 'Vendor opening balance', 'CustomerOpeningBalance': 'Customer opening balance', diff --git a/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts b/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts index ba1916b29..930ee66f8 100644 --- a/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts +++ b/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts @@ -1,6 +1,6 @@ import moment from 'moment'; import { Inject, Service } from 'typedi'; -import { IAPAgingSummaryQuery } from 'interfaces'; +import { IAPAgingSummaryQuery, IARAgingSummaryMeta } from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; import APAgingSummarySheet from './APAgingSummarySheet'; @@ -25,13 +25,36 @@ export default class PayableAgingSummaryService { divideOn1000: false, showZero: false, formatMoney: 'total', - negativeFormat: 'mines' + negativeFormat: 'mines', }, vendorsIds: [], noneZero: false, }; } + /** + * Retrieve the balance sheet meta. + * @param {number} tenantId - + * @returns {IBalanceSheetMeta} + */ + reportMetadata(tenantId: number): IARAgingSummaryMeta { + const settings = this.tenancy.settings(tenantId); + + const organizationName = settings.get({ + group: 'organization', + key: 'name', + }); + const baseCurrency = settings.get({ + group: 'organization', + key: 'base_currency', + }); + + return { + organizationName, + baseCurrency, + }; + } + /** * Retrieve A/P aging summary report. * @param {number} tenantId - @@ -81,6 +104,11 @@ export default class PayableAgingSummaryService { const data = APAgingSummaryReport.reportData(); const columns = APAgingSummaryReport.reportColumns(); - return { data, columns, query: filter }; + return { + data, + columns, + query: filter, + meta: this.reportMetadata(tenantId), + }; } } diff --git a/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts b/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts index c1d715216..de623f854 100644 --- a/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts +++ b/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts @@ -1,6 +1,6 @@ import moment from 'moment'; import { Inject, Service } from 'typedi'; -import { IARAgingSummaryQuery } from 'interfaces'; +import { IARAgingSummaryQuery, IARAgingSummaryMeta } from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; import ARAgingSummarySheet from './ARAgingSummarySheet'; @@ -32,6 +32,29 @@ export default class ARAgingSummaryService { }; } + /** + * Retrieve the balance sheet meta. + * @param {number} tenantId - + * @returns {IBalanceSheetMeta} + */ + reportMetadata(tenantId: number): IARAgingSummaryMeta { + const settings = this.tenancy.settings(tenantId); + + const organizationName = settings.get({ + group: 'organization', + key: 'name', + }); + const baseCurrency = settings.get({ + group: 'organization', + key: 'base_currency', + }); + + return { + organizationName, + baseCurrency, + }; + } + /** * Retrieve A/R aging summary report. * @param {number} tenantId - Tenant id. @@ -85,6 +108,11 @@ export default class ARAgingSummaryService { const data = ARAgingSummaryReport.reportData(); const columns = ARAgingSummaryReport.reportColumns(); - return { data, columns, query: filter }; + return { + data, + columns, + query: filter, + meta: this.reportMetadata(tenantId), + }; } } diff --git a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts index 3c6daa5aa..7b4ef5526 100644 --- a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts +++ b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts @@ -4,10 +4,13 @@ import { IBalanceSheetStatementService, IBalanceSheetQuery, IBalanceSheetStatement, + IBalanceSheetMeta, } from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; import Journal from 'services/Accounting/JournalPoster'; import BalanceSheetStatement from './BalanceSheet'; +import InventoryService from 'services/Inventory/Inventory'; +import { parseBoolean } from 'utils'; @Service() export default class BalanceSheetStatementService @@ -18,6 +21,9 @@ export default class BalanceSheetStatementService @Inject('logger') logger: any; + @Inject() + inventoryService: InventoryService; + /** * Defaults balance sheet filter query. * @return {IBalanceSheetQuery} @@ -33,7 +39,7 @@ export default class BalanceSheetStatementService divideOn1000: false, showZero: false, formatMoney: 'total', - negativeFormat: 'mines' + negativeFormat: 'mines', }, noneZero: false, noneTransactions: false, @@ -42,6 +48,33 @@ export default class BalanceSheetStatementService }; } + /** + * Retrieve the balance sheet meta. + * @param {number} tenantId - + * @returns {IBalanceSheetMeta} + */ + reportMetadata(tenantId: number): IBalanceSheetMeta { + const settings = this.tenancy.settings(tenantId); + + const isCostComputeRunning = this.inventoryService + .isItemsCostComputeRunning(tenantId); + + const organizationName = settings.get({ + group: 'organization', + key: 'name', + }); + const baseCurrency = settings.get({ + group: 'organization', + key: 'base_currency', + }); + + return { + isCostComputeRunning: parseBoolean(isCostComputeRunning, false), + organizationName, + baseCurrency + }; + } + /** * Retrieve balance sheet statement. * ------------- @@ -61,14 +94,19 @@ export default class BalanceSheetStatementService // Settings tenant service. const settings = this.tenancy.settings(tenantId); - const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' }); + const baseCurrency = settings.get({ + group: 'organization', + key: 'base_currency', + }); const filter = { ...this.defaultQuery, ...query, }; - this.logger.info('[balance_sheet] trying to calculate the report.', { filter, tenantId }); - + this.logger.info('[balance_sheet] trying to calculate the report.', { + filter, + tenantId, + }); // Retrieve all accounts on the storage. const accounts = await accountRepository.all(); const accountsGraph = await accountRepository.getDependencyGraph(); @@ -82,7 +120,7 @@ export default class BalanceSheetStatementService const transactionsJournal = Journal.fromTransactions( transactions, tenantId, - accountsGraph, + accountsGraph ); // Balance sheet report instance. const balanceSheetInstanace = new BalanceSheetStatement( @@ -102,6 +140,7 @@ export default class BalanceSheetStatementService data: balanceSheetData, columns: balanceSheetColumns, query: filter, + meta: this.reportMetadata(tenantId), }; } } 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/FinancialStatements/GeneralLedger/GeneralLedgerService.ts b/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts index 6c2ec00e7..0878305e1 100644 --- a/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts +++ b/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts @@ -2,12 +2,12 @@ import { Service, Inject } from 'typedi'; import moment from 'moment'; import { ServiceError } from 'exceptions'; import { difference } from 'lodash'; -import { IGeneralLedgerSheetQuery } from 'interfaces'; +import { IGeneralLedgerSheetQuery, IGeneralLedgerMeta } from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; import Journal from 'services/Accounting/JournalPoster'; import GeneralLedgerSheet from 'services/FinancialStatements/GeneralLedger/GeneralLedger'; - -import { transformToMap } from 'utils'; +import InventoryService from 'services/Inventory/Inventory'; +import { transformToMap, parseBoolean } from 'utils'; const ERRORS = { ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND', @@ -18,6 +18,9 @@ export default class GeneralLedgerService { @Inject() tenancy: TenancyService; + @Inject() + inventoryService: InventoryService; + @Inject('logger') logger: any; @@ -55,6 +58,33 @@ export default class GeneralLedgerService { } } + /** + * Retrieve the balance sheet meta. + * @param {number} tenantId - + * @returns {IGeneralLedgerMeta} + */ + reportMetadata(tenantId: number): IGeneralLedgerMeta { + const settings = this.tenancy.settings(tenantId); + + const isCostComputeRunning = this.inventoryService + .isItemsCostComputeRunning(tenantId); + + const organizationName = settings.get({ + group: 'organization', + key: 'name', + }); + const baseCurrency = settings.get({ + group: 'organization', + key: 'base_currency', + }); + + return { + isCostComputeRunning: parseBoolean(isCostComputeRunning, false), + organizationName, + baseCurrency + }; + } + /** * Retrieve general ledger report statement. * ---------- @@ -68,6 +98,7 @@ export default class GeneralLedgerService { ): Promise<{ data: any; query: IGeneralLedgerSheetQuery; + meta: IGeneralLedgerMeta }> { const { accountRepository, @@ -146,6 +177,7 @@ export default class GeneralLedgerService { return { data: reportData, query: filter, + meta: this.reportMetadata(tenantId), }; } } diff --git a/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts b/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts index 686399c53..99455a886 100644 --- a/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts +++ b/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts @@ -1,18 +1,21 @@ import { Service, Inject } from 'typedi'; -import { IJournalReportQuery } from 'interfaces'; import moment from 'moment'; +import { IJournalReportQuery, IJournalSheetMeta } from 'interfaces'; import JournalSheet from './JournalSheet'; import TenancyService from 'services/Tenancy/TenancyService'; import Journal from 'services/Accounting/JournalPoster'; - -import { transformToMap } from 'utils'; +import InventoryService from 'services/Inventory/Inventory'; +import { parseBoolean, transformToMap } from 'utils'; @Service() export default class JournalSheetService { @Inject() tenancy: TenancyService; + @Inject() + inventoryService: InventoryService; + @Inject('logger') logger: any; @@ -34,6 +37,33 @@ export default class JournalSheetService { }; } + /** + * Retrieve the balance sheet meta. + * @param {number} tenantId - + * @returns {IBalanceSheetMeta} + */ + reportMetadata(tenantId: number): IJournalSheetMeta { + const settings = this.tenancy.settings(tenantId); + + const isCostComputeRunning = this.inventoryService + .isItemsCostComputeRunning(tenantId); + + const organizationName = settings.get({ + group: 'organization', + key: 'name', + }); + const baseCurrency = settings.get({ + group: 'organization', + key: 'base_currency', + }); + + return { + isCostComputeRunning: parseBoolean(isCostComputeRunning, false), + organizationName, + baseCurrency + }; + } + /** * Journal sheet. * @param {number} tenantId @@ -96,6 +126,7 @@ export default class JournalSheetService { return { data: journalSheetData, query: filter, + meta: this.reportMetadata(tenantId), }; } } diff --git a/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts b/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts index 7f90eee73..3ca848d38 100644 --- a/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts +++ b/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts @@ -1,10 +1,12 @@ import { Service, Inject } from 'typedi'; import moment from 'moment'; import Journal from 'services/Accounting/JournalPoster'; -import { IProfitLossSheetQuery } from 'interfaces'; +import { IProfitLossSheetQuery, IProfitLossSheetMeta } from 'interfaces'; import ProfitLossSheet from './ProfitLossSheet'; import TenancyService from 'services/Tenancy/TenancyService'; import AccountsService from 'services/Accounts/AccountsService'; +import InventoryService from 'services/Inventory/Inventory'; +import { parseBoolean } from 'utils'; // Profit/Loss sheet service. @Service() @@ -15,6 +17,9 @@ export default class ProfitLossSheetService { @Inject('logger') logger: any; + @Inject() + inventoryService: InventoryService; + @Inject() accountsService: AccountsService; @@ -42,6 +47,34 @@ export default class ProfitLossSheetService { }; } + + /** + * Retrieve the trial balance sheet meta. + * @param {number} tenantId - Tenant id. + * @returns {ITrialBalanceSheetMeta} + */ + reportMetadata(tenantId: number): IProfitLossSheetMeta { + const settings = this.tenancy.settings(tenantId); + + const isCostComputeRunning = this.inventoryService.isItemsCostComputeRunning( + tenantId + ); + const organizationName = settings.get({ + group: 'organization', + key: 'name', + }); + const baseCurrency = settings.get({ + group: 'organization', + key: 'base_currency', + }); + + return { + isCostComputeRunning: parseBoolean(isCostComputeRunning, false), + organizationName, + baseCurrency, + }; + } + /** * Retrieve profit/loss sheet statement. * @param {number} tenantId @@ -107,6 +140,7 @@ export default class ProfitLossSheetService { data: profitLossData, columns: profitLossColumns, query: filter, + meta: this.reportMetadata(tenantId), }; } } diff --git a/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts b/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts index 9f96dbc57..48abaa44a 100644 --- a/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts +++ b/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts @@ -2,15 +2,20 @@ import { Service, Inject } from 'typedi'; import moment from 'moment'; import TenancyService from 'services/Tenancy/TenancyService'; import Journal from 'services/Accounting/JournalPoster'; -import { INumberFormatQuery, ITrialBalanceSheetQuery, ITrialBalanceStatement } from 'interfaces'; +import { ITrialBalanceSheetMeta, ITrialBalanceSheetQuery, ITrialBalanceStatement } from 'interfaces'; import TrialBalanceSheet from './TrialBalanceSheet'; import FinancialSheet from '../FinancialSheet'; +import InventoryService from 'services/Inventory/Inventory'; +import { parseBoolean } from 'utils'; @Service() export default class TrialBalanceSheetService extends FinancialSheet { @Inject() tenancy: TenancyService; + @Inject() + inventoryService: InventoryService; + @Inject('logger') logger: any; @@ -36,6 +41,33 @@ export default class TrialBalanceSheetService extends FinancialSheet { }; } + /** + * Retrieve the trial balance sheet meta. + * @param {number} tenantId - Tenant id. + * @returns {ITrialBalanceSheetMeta} + */ + reportMetadata(tenantId: number): ITrialBalanceSheetMeta { + const settings = this.tenancy.settings(tenantId); + + const isCostComputeRunning = this.inventoryService.isItemsCostComputeRunning( + tenantId + ); + const organizationName = settings.get({ + group: 'organization', + key: 'name', + }); + const baseCurrency = settings.get({ + group: 'organization', + key: 'base_currency', + }); + + return { + isCostComputeRunning: parseBoolean(isCostComputeRunning, false), + organizationName, + baseCurrency, + }; + } + /** * Retrieve trial balance sheet statement. * ------------- @@ -46,7 +78,7 @@ export default class TrialBalanceSheetService extends FinancialSheet { */ public async trialBalanceSheet( tenantId: number, - query: ITrialBalanceSheetQuery, + query: ITrialBalanceSheetQuery ): Promise { const filter = { ...this.defaultQuery, @@ -98,6 +130,7 @@ export default class TrialBalanceSheetService extends FinancialSheet { return { data: trialBalanceSheetData, query: filter, + meta: this.reportMetadata(tenantId), }; } } diff --git a/server/src/services/Inventory/Inventory.ts b/server/src/services/Inventory/Inventory.ts index 5159abd8a..ef9bda031 100644 --- a/server/src/services/Inventory/Inventory.ts +++ b/server/src/services/Inventory/Inventory.ts @@ -17,6 +17,7 @@ import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker' import TenancyService from 'services/Tenancy/TenancyService'; import events from 'subscribers/events'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; +import SettingsMiddleware from 'api/middleware/SettingsMiddleware'; type TCostMethod = 'FIFO' | 'LIFO' | 'AVG'; @@ -346,4 +347,37 @@ export default class InventoryService { return lotNumber; } + + /** + * Mark item cost computing is running. + * @param {number} tenantId - + * @param {boolean} isRunning - + */ + async markItemsCostComputeRunning( + tenantId: number, + isRunning: boolean = true + ) { + const settings = this.tenancy.settings(tenantId); + + settings.set({ + key: 'cost_compute_running', + group: 'inventory', + value: isRunning, + }); + await settings.save(); + } + + /** + * + * @param {number} tenantId + * @returns + */ + isItemsCostComputeRunning(tenantId) { + const settings = this.tenancy.settings(tenantId); + + return settings.get({ + key: 'cost_compute_running', + group: 'inventory' + }); + } } 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/subscribers/Inventory/Inventory.ts b/server/src/subscribers/Inventory/Inventory.ts index 7caf72859..05c8c3fd2 100644 --- a/server/src/subscribers/Inventory/Inventory.ts +++ b/server/src/subscribers/Inventory/Inventory.ts @@ -2,25 +2,52 @@ import { Container } from 'typedi'; import { EventSubscriber, On } from 'event-dispatch'; import { map, head } from 'lodash'; import events from 'subscribers/events'; +import TenancyService from 'services/Tenancy/TenancyService'; import SaleInvoicesCost from 'services/Sales/SalesInvoicesCost'; import InventoryItemsQuantitySync from 'services/Inventory/InventoryItemsQuantitySync'; -import { InventoryTransaction } from 'models'; +import InventoryService from 'services/Inventory/Inventory'; @EventSubscriber() export class InventorySubscriber { depends: number = 0; startingDate: Date; saleInvoicesCost: SaleInvoicesCost; - + tenancy: TenancyService; itemsQuantitySync: InventoryItemsQuantitySync; + inventoryService: InventoryService; agenda: any; + /** + * Constructor method. + */ constructor() { this.saleInvoicesCost = Container.get(SaleInvoicesCost); this.itemsQuantitySync = Container.get(InventoryItemsQuantitySync); + this.inventoryService = Container.get(InventoryService); + this.tenancy = Container.get(TenancyService); this.agenda = Container.get('agenda'); } + /** + * Marks items cost compute running state. + */ + @On(events.inventory.onComputeItemCostJobScheduled) + async markGlobalSettingsComputeItems({ + tenantId + }) { + await this.inventoryService.markItemsCostComputeRunning(tenantId, true); + } + + /** + * Marks items cost compute as completed. + */ + @On(events.inventory.onInventoryCostEntriesWritten) + async markGlobalSettingsComputeItemsCompeted({ + tenantId + }) { + await this.inventoryService.markItemsCostComputeRunning(tenantId, false); + } + /** * Handle run writing the journal entries once the compute items jobs completed. */ diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 62c41d7c2..48f8836b5 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -190,7 +190,9 @@ export default { onComputeItemCostJobScheduled: 'onComputeItemCostJobScheduled', onComputeItemCostJobStarted: 'onComputeItemCostJobStarted', - onComputeItemCostJobCompleted: 'onComputeItemCostJobCompleted' + onComputeItemCostJobCompleted: 'onComputeItemCostJobCompleted', + + onInventoryCostEntriesWritten: 'onInventoryCostEntriesWritten' }, /** 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, + }, ]); }); };