This commit is contained in:
elforjani3
2021-03-14 15:46:23 +02:00
39 changed files with 721 additions and 322 deletions

View File

@@ -45,28 +45,19 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
const { tenantId, settings } = req; const { tenantId, settings } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
try { try {
const { const {
data, data,
columns, columns,
query, query,
meta
} = await this.APAgingSummaryService.APAgingSummary(tenantId, filter); } = await this.APAgingSummaryService.APAgingSummary(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
organization_name: organizationName,
base_currency: baseCurrency,
data: this.transfromToResponse(data), data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns), columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query), query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta)
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -25,7 +25,7 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
} }
/** /**
* Receivable aging summary validation roles. * AR aging summary validation roles.
*/ */
get validationSchema() { get validationSchema() {
return [ 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) { async receivableAgingSummary(req: Request, res: Response) {
const { tenantId, settings } = req; const { tenantId, settings } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
try { try {
const { const {
data, data,
columns, columns,
query, query,
meta,
} = await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter); } = await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
organization_name: organizationName,
base_currency: baseCurrency,
data: this.transfromToResponse(data), data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns), columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query), query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
}); });
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -59,28 +59,20 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
...filter, ...filter,
accountsIds: castArray(filter.accountsIds), accountsIds: castArray(filter.accountsIds),
}; };
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
try { try {
const { const {
data, data,
columns, columns,
query, query,
meta,
} = await this.balanceSheetService.balanceSheet(tenantId, filter); } = await this.balanceSheetService.balanceSheet(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
organization_name: organizationName,
base_currency: baseCurrency,
data: this.transfromToResponse(data), data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns), columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query), query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -52,23 +52,13 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
const { tenantId, settings } = req; const { tenantId, settings } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
try { try {
const { data, query } = await this.generalLedgetService.generalLedger( const { data, query, meta } = await this.generalLedgetService.generalLedger(
tenantId, tenantId,
filter filter
); );
return res.status(200).send({ return res.status(200).send({
organization_name: organizationName, meta: this.transfromToResponse(meta),
base_currency: baseCurrency,
data: this.transfromToResponse(data), data: this.transfromToResponse(data),
query: this.transfromToResponse(query), query: this.transfromToResponse(query),
}); });

View File

@@ -66,26 +66,17 @@ export default class JournalSheetController extends BaseFinancialReportControlle
...filter, ...filter,
accountsIds: castArray(filter.accountsIds), accountsIds: castArray(filter.accountsIds),
}; };
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
try { try {
const { data, query } = await this.journalService.journalSheet( const { data, query, meta } = await this.journalService.journalSheet(
tenantId, tenantId,
filter filter
); );
return res.status(200).send({ return res.status(200).send({
organization_name: organizationName,
base_currency: baseCurrency,
data: this.transfromToResponse(data), data: this.transfromToResponse(data),
query: this.transfromToResponse(query), query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -54,22 +54,19 @@ export default class ProfitLossSheetController extends BaseFinancialReportContro
const { tenantId, settings } = req; const { tenantId, settings } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
const organizationName = settings.get({ group: 'organization', key: 'name' });
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
try { try {
const { const {
data, data,
columns, columns,
query, query,
meta
} = await this.profitLossSheetService.profitLossSheet(tenantId, filter); } = await this.profitLossSheetService.profitLossSheet(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
organization_name: organizationName,
base_currency: baseCurrency,
data: this.transfromToResponse(data), data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns), columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query), query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta)
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -58,29 +58,21 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
...filter, ...filter,
accountsIds: castArray(filter.accountsIds), accountsIds: castArray(filter.accountsIds),
}; };
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
try { try {
const { const {
data, data,
query, query,
meta
} = await this.trialBalanceSheetService.trialBalanceSheet( } = await this.trialBalanceSheetService.trialBalanceSheet(
tenantId, tenantId,
filter filter
); );
return res.status(200).send({ return res.status(200).send({
organization_name: organizationName,
base_currency: baseCurrency,
data: this.transfromToResponse(data), data: this.transfromToResponse(data),
query: this.transfromToResponse(query), query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,13 +1,14 @@
import { Service, Inject } from 'typedi'; 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 { check, oneOf, ValidationChain } from 'express-validator';
import basicAuth from 'express-basic-auth'; import basicAuth from 'express-basic-auth';
import config from 'config'; 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 BaseController from 'api/controllers/BaseController';
import LicenseService from 'services/Payment/License'; import LicenseService from 'services/Payment/License';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import { ILicensesFilter } from 'interfaces'; import { ILicensesFilter, ISendLicenseDTO } from 'interfaces';
@Service() @Service()
export default class LicensesController extends BaseController { export default class LicensesController extends BaseController {
@@ -32,26 +33,26 @@ export default class LicensesController extends BaseController {
'/generate', '/generate',
this.generateLicenseSchema, this.generateLicenseSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.validatePlanExistance.bind(this)), asyncMiddleware(this.generateLicense.bind(this)),
asyncMiddleware(this.generateLicense.bind(this)) this.catchServiceErrors,
); );
router.post( router.post(
'/disable/:licenseId', '/disable/:licenseId',
this.validationResult, this.validationResult,
asyncMiddleware(this.validateLicenseExistance.bind(this)), asyncMiddleware(this.disableLicense.bind(this)),
asyncMiddleware(this.validateNotDisabledLicense.bind(this)), this.catchServiceErrors,
asyncMiddleware(this.disableLicense.bind(this))
); );
router.post( router.post(
'/send', '/send',
this.sendLicenseSchemaValidation, this.sendLicenseSchemaValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.sendLicense.bind(this)) asyncMiddleware(this.sendLicense.bind(this)),
this.catchServiceErrors,
); );
router.delete( router.delete(
'/:licenseId', '/: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))); router.get('/', asyncMiddleware(this.listLicenses.bind(this)));
return router; return router;
@@ -67,7 +68,7 @@ export default class LicensesController extends BaseController {
check('period_interval') check('period_interval')
.exists() .exists()
.isIn(['month', 'months', 'year', 'years', 'day', 'days']), .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 [ return [
check('period').exists().isNumeric(), check('period').exists().isNumeric(),
check('period_interval').exists().trim().escape(), check('period_interval').exists().trim().escape(),
check('plan_id').exists().isNumeric().toInt(), check('plan_slug').exists().trim().escape(),
oneOf([ oneOf([
check('phone_number').exists().trim().escape(), check('phone_number').exists().trim().escape(),
check('email').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. * Generate licenses codes with given period in bulk.
* @param {Request} req * @param {Request} req
@@ -166,7 +106,7 @@ export default class LicensesController extends BaseController {
* @return {Response} * @return {Response}
*/ */
async generateLicense(req: Request, res: Response, next: Function) { 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 req
); );
@@ -175,7 +115,7 @@ export default class LicensesController extends BaseController {
loop, loop,
period, period,
periodInterval, periodInterval,
planId planSlug
); );
return res.status(200).send({ return res.status(200).send({
code: 100, code: 100,
@@ -193,12 +133,16 @@ export default class LicensesController extends BaseController {
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
async disableLicense(req: Request, res: Response) { async disableLicense(req: Request, res: Response, next: Function) {
const { licenseId } = req.params; const { licenseId } = req.params;
try {
await this.licenseService.disableLicense(licenseId); 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 * @param {Response} res
* @return {Response} * @return {Response}
*/ */
async deleteLicense(req: Request, res: Response) { async deleteLicense(req: Request, res: Response, next: Function) {
const { licenseId } = req.params; const { licenseId } = req.params;
try {
await this.licenseService.deleteLicense(licenseId); 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 * @param {Response} res
* @return {Response} * @return {Response}
*/ */
async sendLicense(req: Request, res: Response) { async sendLicense(req: Request, res: Response, next: Function) {
const { const sendLicenseDTO: ISendLicenseDTO = this.matchedBodyData(req);
phoneNumber,
email,
period,
periodInterval,
planId,
} = this.matchedBodyData(req);
const license = await License.query() try {
.modify('filterActiveLicense') await this.licenseService.sendLicenseToCustomer(sendLicenseDTO);
.where('license_period', period)
.where('period_interval', periodInterval)
.where('plan_id', planId)
.first();
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',
});
}
await this.licenseService.sendLicenseToCustomer(
license.licenseCode,
phoneNumber,
email
);
return res.status(200).send({ return res.status(200).send({
status: 100, status: 100,
code: 'LICENSE.CODE.SENT', code: 'LICENSE.CODE.SENT',
message: 'The license has been sent to the given customer.', message: 'The license has been sent to the given customer.',
}); });
} catch (error) {
next(error);
}
} }
/** /**
@@ -276,4 +204,47 @@ export default class LicensesController extends BaseController {
}); });
return res.status(200).send({ licenses }); 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);
}
} }

View File

@@ -1,4 +1,5 @@
import { Inject } from 'typedi'; import { Inject } from 'typedi';
import { Request, Response } from 'express';
import { Plan } from 'system/models'; import { Plan } from 'system/models';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
import SubscriptionService from 'services/Subscription/SubscriptionService'; import SubscriptionService from 'services/Subscription/SubscriptionService';

View File

@@ -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 { Container, Service, Inject } from 'typedi';
import JWTAuth from 'api/middleware/jwtAuth'; import JWTAuth from 'api/middleware/jwtAuth';
import TenancyMiddleware from 'api/middleware/TenancyMiddleware'; import TenancyMiddleware from 'api/middleware/TenancyMiddleware';
@@ -22,13 +22,9 @@ export default class SubscriptionController {
router.use(AttachCurrentTenantUser); router.use(AttachCurrentTenantUser);
router.use(TenancyMiddleware); router.use(TenancyMiddleware);
router.use( router.use('/license', Container.get(PaymentViaLicenseController).router());
'/license', router.get('/', asyncMiddleware(this.getSubscriptions.bind(this)));
Container.get(PaymentViaLicenseController).router()
);
router.get('/',
asyncMiddleware(this.getSubscriptions.bind(this))
);
return router; return router;
} }
@@ -42,7 +38,9 @@ export default class SubscriptionController {
const { tenantId } = req; const { tenantId } = req;
try { try {
const subscriptions = await this.subscriptionService.getSubscriptions(tenantId); const subscriptions = await this.subscriptionService.getSubscriptions(
tenantId
);
return res.status(200).send({ subscriptions }); return res.status(200).send({ subscriptions });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -0,0 +1,6 @@
export default class VoucherCodeRequired {
constructor() {
this.name = 'VoucherCodeRequired';
}
}

View File

@@ -8,6 +8,7 @@ import TenantAlreadyInitialized from './TenantAlreadyInitialized';
import TenantAlreadySeeded from './TenantAlreadySeeded'; import TenantAlreadySeeded from './TenantAlreadySeeded';
import TenantDBAlreadyExists from './TenantDBAlreadyExists'; import TenantDBAlreadyExists from './TenantDBAlreadyExists';
import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt'; import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt';
import VoucherCodeRequired from './VoucherCodeRequired';
export { export {
NotAllowedChangeSubscriptionPlan, NotAllowedChangeSubscriptionPlan,
@@ -20,4 +21,5 @@ export {
TenantAlreadySeeded, TenantAlreadySeeded,
TenantDBAlreadyExists, TenantDBAlreadyExists,
TenantDatabaseNotBuilt, TenantDatabaseNotBuilt,
VoucherCodeRequired,
}; };

View File

@@ -35,3 +35,15 @@ export interface IAPAgingSummaryData {
}; };
export type IAPAgingSummaryColumns = IAgingPeriod[]; export type IAPAgingSummaryColumns = IAgingPeriod[];
export interface IARAgingSummaryMeta {
baseCurrency: string,
organizationName: string,
}
export interface IAPAgingSummaryMeta {
baseCurrency: string,
organizationName: string,
}

View File

@@ -29,3 +29,8 @@ export interface IARAgingSummaryData {
} }
export type IARAgingSummaryColumns = IAgingPeriod[]; export type IARAgingSummaryColumns = IAgingPeriod[];
export interface IARAgingSummaryMeta {
organizationName: string,
baseCurrency: string,
}

View File

@@ -15,10 +15,16 @@ export interface IBalanceSheetQuery {
accountIds: number[]; accountIds: number[];
} }
export interface IBalanceSheetMeta {
isCostComputeRunning: boolean,
organizationName: string,
baseCurrency: string,
};
export interface IBalanceSheetFormatNumberSettings export interface IBalanceSheetFormatNumberSettings
extends IFormatNumberSettings { extends IFormatNumberSettings {
type: string; type: string;
} };
export interface IBalanceSheetStatementService { export interface IBalanceSheetStatementService {
balanceSheet( balanceSheet(
@@ -35,6 +41,7 @@ export interface IBalanceSheetStatement {
query: IBalanceSheetQuery; query: IBalanceSheetQuery;
columns: IBalanceSheetStatementColumns; columns: IBalanceSheetStatementColumns;
data: IBalanceSheetStatementData; data: IBalanceSheetStatementData;
meta: IBalanceSheetMeta;
} }
export interface IBalanceSheetStructureSection { export interface IBalanceSheetStructureSection {

View File

@@ -72,3 +72,9 @@ export interface IAccountTransaction {
createdAt: string|Date, createdAt: string|Date,
updatedAt: string|Date, updatedAt: string|Date,
} }
export interface IGeneralLedgerMeta {
isCostComputeRunning: boolean,
organizationName: string,
baseCurrency: string,
};

View File

@@ -26,3 +26,9 @@ export interface IJournalReportEntriesGroup {
export interface IJournalReport { export interface IJournalReport {
entries: IJournalReportEntriesGroup[], entries: IJournalReportEntriesGroup[],
} }
export interface IJournalSheetMeta {
isCostComputeRunning: boolean,
organizationName: string,
baseCurrency: string,
}

View File

@@ -15,3 +15,11 @@ export interface ILicensesFilter {
used: boolean, used: boolean,
sent: boolean, sent: boolean,
}; };
export interface ISendLicenseDTO {
phoneNumber: string,
email: string,
period: string,
periodInterval: string,
planSlug: string,
};

View File

@@ -55,3 +55,9 @@ export interface IProfitLossSheetStatement {
operatingProfit: IProfitLossSheetTotalSection; operatingProfit: IProfitLossSheetTotalSection;
grossProfit: IProfitLossSheetTotalSection; grossProfit: IProfitLossSheetTotalSection;
}; };
export interface IProfitLossSheetMeta {
isCostComputeRunning: boolean,
organizationName: string,
baseCurrency: string,
}

View File

@@ -21,6 +21,12 @@ export interface ITrialBalanceTotal {
formattedBalance: string; formattedBalance: string;
} }
export interface ITrialBalanceSheetMeta {
isCostComputeRunning: boolean,
organizationName: string,
baseCurrency: string,
};
export interface ITrialBalanceAccount extends ITrialBalanceTotal { export interface ITrialBalanceAccount extends ITrialBalanceTotal {
id: number; id: number;
parentAccountId: number; parentAccountId: number;
@@ -38,4 +44,5 @@ export type ITrialBalanceSheetData = {
export interface ITrialBalanceStatement { export interface ITrialBalanceStatement {
data: ITrialBalanceSheetData; data: ITrialBalanceSheetData;
query: ITrialBalanceSheetQuery; query: ITrialBalanceSheetQuery;
meta: ITrialBalanceSheetMeta,
} }

View File

@@ -1,8 +1,5 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import {EventDispatcher} from "event-dispatch"; import {EventDispatcher} from "event-dispatch";
// import {
// EventDispatcher,
// } from 'decorators/eventDispatcher';
import events from 'subscribers/events'; import events from 'subscribers/events';
import InventoryService from 'services/Inventory/Inventory'; import InventoryService from 'services/Inventory/Inventory';
@@ -11,7 +8,7 @@ export default class ComputeItemCostJob {
eventDispatcher: EventDispatcher; eventDispatcher: EventDispatcher;
/** /**
* * Constructor method.
* @param agenda * @param agenda
*/ */
constructor(agenda) { constructor(agenda) {

View File

@@ -1,13 +1,24 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import {EventDispatcher} from "event-dispatch";
import events from 'subscribers/events';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
export default class WriteInvoicesJournalEntries { export default class WriteInvoicesJournalEntries {
eventDispatcher: EventDispatcher;
/**
* Constructor method.
*/
constructor(agenda) { constructor(agenda) {
const eventName = 'rewrite-invoices-journal-entries';
this.eventDispatcher = new EventDispatcher();
agenda.define( agenda.define(
'rewrite-invoices-journal-entries', eventName,
{ priority: 'normal', concurrency: 1 }, { priority: 'normal', concurrency: 1 },
this.handler.bind(this) this.handler.bind(this)
); );
agenda.on(`complete:${eventName}`, this.onJobCompleted.bind(this));
} }
public async handler(job, done: Function): Promise<void> { public async handler(job, done: Function): Promise<void> {
@@ -36,4 +47,16 @@ export default class WriteInvoicesJournalEntries {
done(e); 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 }
);
}
} }

View File

@@ -17,6 +17,9 @@ export default class AccountTransaction extends TenantModel {
return ['createdAt']; return ['createdAt'];
} }
/**
* Virtual attributes.
*/
static get virtualAttributes() { static get virtualAttributes() {
return ['referenceTypeFormatted']; return ['referenceTypeFormatted'];
} }
@@ -37,6 +40,7 @@ export default class AccountTransaction extends TenantModel {
'SaleInvoice': 'Sale invoice', 'SaleInvoice': 'Sale invoice',
'SaleReceipt': 'Sale receipt', 'SaleReceipt': 'Sale receipt',
'PaymentReceive': 'Payment receive', 'PaymentReceive': 'Payment receive',
'Bill': 'Bill',
'BillPayment': 'Payment made', 'BillPayment': 'Payment made',
'VendorOpeningBalance': 'Vendor opening balance', 'VendorOpeningBalance': 'Vendor opening balance',
'CustomerOpeningBalance': 'Customer opening balance', 'CustomerOpeningBalance': 'Customer opening balance',

View File

@@ -1,6 +1,6 @@
import moment from 'moment'; import moment from 'moment';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { IAPAgingSummaryQuery } from 'interfaces'; import { IAPAgingSummaryQuery, IARAgingSummaryMeta } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import APAgingSummarySheet from './APAgingSummarySheet'; import APAgingSummarySheet from './APAgingSummarySheet';
@@ -25,13 +25,36 @@ export default class PayableAgingSummaryService {
divideOn1000: false, divideOn1000: false,
showZero: false, showZero: false,
formatMoney: 'total', formatMoney: 'total',
negativeFormat: 'mines' negativeFormat: 'mines',
}, },
vendorsIds: [], vendorsIds: [],
noneZero: false, 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. * Retrieve A/P aging summary report.
* @param {number} tenantId - * @param {number} tenantId -
@@ -81,6 +104,11 @@ export default class PayableAgingSummaryService {
const data = APAgingSummaryReport.reportData(); const data = APAgingSummaryReport.reportData();
const columns = APAgingSummaryReport.reportColumns(); const columns = APAgingSummaryReport.reportColumns();
return { data, columns, query: filter }; return {
data,
columns,
query: filter,
meta: this.reportMetadata(tenantId),
};
} }
} }

View File

@@ -1,6 +1,6 @@
import moment from 'moment'; import moment from 'moment';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { IARAgingSummaryQuery } from 'interfaces'; import { IARAgingSummaryQuery, IARAgingSummaryMeta } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import ARAgingSummarySheet from './ARAgingSummarySheet'; 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. * Retrieve A/R aging summary report.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
@@ -85,6 +108,11 @@ export default class ARAgingSummaryService {
const data = ARAgingSummaryReport.reportData(); const data = ARAgingSummaryReport.reportData();
const columns = ARAgingSummaryReport.reportColumns(); const columns = ARAgingSummaryReport.reportColumns();
return { data, columns, query: filter }; return {
data,
columns,
query: filter,
meta: this.reportMetadata(tenantId),
};
} }
} }

View File

@@ -4,10 +4,13 @@ import {
IBalanceSheetStatementService, IBalanceSheetStatementService,
IBalanceSheetQuery, IBalanceSheetQuery,
IBalanceSheetStatement, IBalanceSheetStatement,
IBalanceSheetMeta,
} from 'interfaces'; } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster'; import Journal from 'services/Accounting/JournalPoster';
import BalanceSheetStatement from './BalanceSheet'; import BalanceSheetStatement from './BalanceSheet';
import InventoryService from 'services/Inventory/Inventory';
import { parseBoolean } from 'utils';
@Service() @Service()
export default class BalanceSheetStatementService export default class BalanceSheetStatementService
@@ -18,6 +21,9 @@ export default class BalanceSheetStatementService
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject()
inventoryService: InventoryService;
/** /**
* Defaults balance sheet filter query. * Defaults balance sheet filter query.
* @return {IBalanceSheetQuery} * @return {IBalanceSheetQuery}
@@ -33,7 +39,7 @@ export default class BalanceSheetStatementService
divideOn1000: false, divideOn1000: false,
showZero: false, showZero: false,
formatMoney: 'total', formatMoney: 'total',
negativeFormat: 'mines' negativeFormat: 'mines',
}, },
noneZero: false, noneZero: false,
noneTransactions: 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. * Retrieve balance sheet statement.
* ------------- * -------------
@@ -61,14 +94,19 @@ export default class BalanceSheetStatementService
// Settings tenant service. // Settings tenant service.
const settings = this.tenancy.settings(tenantId); 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 = { const filter = {
...this.defaultQuery, ...this.defaultQuery,
...query, ...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. // Retrieve all accounts on the storage.
const accounts = await accountRepository.all(); const accounts = await accountRepository.all();
const accountsGraph = await accountRepository.getDependencyGraph(); const accountsGraph = await accountRepository.getDependencyGraph();
@@ -82,7 +120,7 @@ export default class BalanceSheetStatementService
const transactionsJournal = Journal.fromTransactions( const transactionsJournal = Journal.fromTransactions(
transactions, transactions,
tenantId, tenantId,
accountsGraph, accountsGraph
); );
// Balance sheet report instance. // Balance sheet report instance.
const balanceSheetInstanace = new BalanceSheetStatement( const balanceSheetInstanace = new BalanceSheetStatement(
@@ -102,6 +140,7 @@ export default class BalanceSheetStatementService
data: balanceSheetData, data: balanceSheetData,
columns: balanceSheetColumns, columns: balanceSheetColumns,
query: filter, query: filter,
meta: this.reportMetadata(tenantId),
}; };
} }
} }

View File

@@ -1,4 +1,4 @@
import { pick, get, last } from 'lodash'; import { isEmpty, get, last } from 'lodash';
import { import {
IGeneralLedgerSheetQuery, IGeneralLedgerSheetQuery,
IGeneralLedgerSheetAccount, IGeneralLedgerSheetAccount,
@@ -73,10 +73,9 @@ export default class GeneralLedgerSheet extends FinancialSheet {
entryReducer( entryReducer(
entries: IGeneralLedgerSheetAccountTransaction[], entries: IGeneralLedgerSheetAccountTransaction[],
entry: IJournalEntry, entry: IJournalEntry,
index: number openingBalance: number
): IGeneralLedgerSheetAccountTransaction[] { ): IGeneralLedgerSheetAccountTransaction[] {
const lastEntry = last(entries); const lastEntry = last(entries);
const openingBalance = 0;
const contact = this.contactsMap.get(entry.contactId); const contact = this.contactsMap.get(entry.contactId);
const amount = this.getAmount( const amount = this.getAmount(
@@ -85,11 +84,7 @@ export default class GeneralLedgerSheet extends FinancialSheet {
entry.accountNormal entry.accountNormal
); );
const runningBalance = const runningBalance =
(entries.length === 0 amount + (!isEmpty(entries) ? lastEntry.runningBalance : openingBalance);
? openingBalance
: lastEntry
? lastEntry.runningBalance
: 0) + amount;
const newEntry = { const newEntry = {
date: entry.date, date: entry.date,
@@ -182,9 +177,7 @@ export default class GeneralLedgerSheet extends FinancialSheet {
* @param {IAccount} account * @param {IAccount} account
* @return {IGeneralLedgerSheetAccount} * @return {IGeneralLedgerSheetAccount}
*/ */
private accountMapper( private accountMapper(account: IAccount): IGeneralLedgerSheetAccount {
account: IAccount
): IGeneralLedgerSheetAccount {
const openingBalance = this.accountOpeningBalance(account); const openingBalance = this.accountOpeningBalance(account);
const closingBalance = this.accountClosingBalance(account); const closingBalance = this.accountClosingBalance(account);
@@ -208,14 +201,10 @@ export default class GeneralLedgerSheet extends FinancialSheet {
* @param {IAccount[]} accounts - * @param {IAccount[]} accounts -
* @return {IGeneralLedgerSheetAccount[]} * @return {IGeneralLedgerSheetAccount[]}
*/ */
private accountsWalker( private accountsWalker(accounts: IAccount[]): IGeneralLedgerSheetAccount[] {
accounts: IAccount[]
): IGeneralLedgerSheetAccount[] {
return ( return (
accounts accounts
.map((account: IAccount) => .map((account: IAccount) => this.accountMapper(account))
this.accountMapper(account)
)
// Filter general ledger accounts that have no transactions // Filter general ledger accounts that have no transactions
// when`noneTransactions` is on. // when`noneTransactions` is on.
.filter( .filter(

View File

@@ -2,12 +2,12 @@ import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import { difference } from 'lodash'; import { difference } from 'lodash';
import { IGeneralLedgerSheetQuery } from 'interfaces'; import { IGeneralLedgerSheetQuery, IGeneralLedgerMeta } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster'; import Journal from 'services/Accounting/JournalPoster';
import GeneralLedgerSheet from 'services/FinancialStatements/GeneralLedger/GeneralLedger'; import GeneralLedgerSheet from 'services/FinancialStatements/GeneralLedger/GeneralLedger';
import InventoryService from 'services/Inventory/Inventory';
import { transformToMap } from 'utils'; import { transformToMap, parseBoolean } from 'utils';
const ERRORS = { const ERRORS = {
ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND', ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND',
@@ -18,6 +18,9 @@ export default class GeneralLedgerService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject()
inventoryService: InventoryService;
@Inject('logger') @Inject('logger')
logger: any; 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. * Retrieve general ledger report statement.
* ---------- * ----------
@@ -68,6 +98,7 @@ export default class GeneralLedgerService {
): Promise<{ ): Promise<{
data: any; data: any;
query: IGeneralLedgerSheetQuery; query: IGeneralLedgerSheetQuery;
meta: IGeneralLedgerMeta
}> { }> {
const { const {
accountRepository, accountRepository,
@@ -146,6 +177,7 @@ export default class GeneralLedgerService {
return { return {
data: reportData, data: reportData,
query: filter, query: filter,
meta: this.reportMetadata(tenantId),
}; };
} }
} }

View File

@@ -1,18 +1,21 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { IJournalReportQuery } from 'interfaces';
import moment from 'moment'; import moment from 'moment';
import { IJournalReportQuery, IJournalSheetMeta } from 'interfaces';
import JournalSheet from './JournalSheet'; import JournalSheet from './JournalSheet';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster'; import Journal from 'services/Accounting/JournalPoster';
import InventoryService from 'services/Inventory/Inventory';
import { transformToMap } from 'utils'; import { parseBoolean, transformToMap } from 'utils';
@Service() @Service()
export default class JournalSheetService { export default class JournalSheetService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject()
inventoryService: InventoryService;
@Inject('logger') @Inject('logger')
logger: any; 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. * Journal sheet.
* @param {number} tenantId * @param {number} tenantId
@@ -96,6 +126,7 @@ export default class JournalSheetService {
return { return {
data: journalSheetData, data: journalSheetData,
query: filter, query: filter,
meta: this.reportMetadata(tenantId),
}; };
} }
} }

View File

@@ -1,10 +1,12 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import Journal from 'services/Accounting/JournalPoster'; import Journal from 'services/Accounting/JournalPoster';
import { IProfitLossSheetQuery } from 'interfaces'; import { IProfitLossSheetQuery, IProfitLossSheetMeta } from 'interfaces';
import ProfitLossSheet from './ProfitLossSheet'; import ProfitLossSheet from './ProfitLossSheet';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import InventoryService from 'services/Inventory/Inventory';
import { parseBoolean } from 'utils';
// Profit/Loss sheet service. // Profit/Loss sheet service.
@Service() @Service()
@@ -15,6 +17,9 @@ export default class ProfitLossSheetService {
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject()
inventoryService: InventoryService;
@Inject() @Inject()
accountsService: AccountsService; 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. * Retrieve profit/loss sheet statement.
* @param {number} tenantId * @param {number} tenantId
@@ -107,6 +140,7 @@ export default class ProfitLossSheetService {
data: profitLossData, data: profitLossData,
columns: profitLossColumns, columns: profitLossColumns,
query: filter, query: filter,
meta: this.reportMetadata(tenantId),
}; };
} }
} }

View File

@@ -2,15 +2,20 @@ import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster'; import Journal from 'services/Accounting/JournalPoster';
import { INumberFormatQuery, ITrialBalanceSheetQuery, ITrialBalanceStatement } from 'interfaces'; import { ITrialBalanceSheetMeta, ITrialBalanceSheetQuery, ITrialBalanceStatement } from 'interfaces';
import TrialBalanceSheet from './TrialBalanceSheet'; import TrialBalanceSheet from './TrialBalanceSheet';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
import InventoryService from 'services/Inventory/Inventory';
import { parseBoolean } from 'utils';
@Service() @Service()
export default class TrialBalanceSheetService extends FinancialSheet { export default class TrialBalanceSheetService extends FinancialSheet {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject()
inventoryService: InventoryService;
@Inject('logger') @Inject('logger')
logger: any; 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. * Retrieve trial balance sheet statement.
* ------------- * -------------
@@ -46,7 +78,7 @@ export default class TrialBalanceSheetService extends FinancialSheet {
*/ */
public async trialBalanceSheet( public async trialBalanceSheet(
tenantId: number, tenantId: number,
query: ITrialBalanceSheetQuery, query: ITrialBalanceSheetQuery
): Promise<ITrialBalanceStatement> { ): Promise<ITrialBalanceStatement> {
const filter = { const filter = {
...this.defaultQuery, ...this.defaultQuery,
@@ -98,6 +130,7 @@ export default class TrialBalanceSheetService extends FinancialSheet {
return { return {
data: trialBalanceSheetData, data: trialBalanceSheetData,
query: filter, query: filter,
meta: this.reportMetadata(tenantId),
}; };
} }
} }

View File

@@ -17,6 +17,7 @@ import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker'
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import events from 'subscribers/events'; import events from 'subscribers/events';
import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import SettingsMiddleware from 'api/middleware/SettingsMiddleware';
type TCostMethod = 'FIFO' | 'LIFO' | 'AVG'; type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';
@@ -346,4 +347,37 @@ export default class InventoryService {
return lotNumber; 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'
});
}
} }

View File

@@ -1,10 +1,18 @@
import { Service, Container, Inject } from 'typedi'; import { Service, Container, Inject } from 'typedi';
import cryptoRandomString from 'crypto-random-string'; import cryptoRandomString from 'crypto-random-string';
import { times } from 'lodash'; import { times } from 'lodash';
import { License } from "system/models"; import { License, Plan } from 'system/models';
import { ILicense } from 'interfaces'; import { ILicense, ISendLicenseDTO } from 'interfaces';
import LicenseMailMessages from 'services/Payment/LicenseMailMessages'; import LicenseMailMessages from 'services/Payment/LicenseMailMessages';
import LicenseSMSMessages from 'services/Payment/LicenseSMSMessages'; 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() @Service()
export default class LicenseService { export default class LicenseService {
@@ -14,49 +22,99 @@ export default class LicenseService {
@Inject() @Inject()
mailMessages: LicenseMailMessages; 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. * Generates the license code in the given period.
* @param {number} licensePeriod * @param {number} licensePeriod
* @return {Promise<ILicense>} * @return {Promise<ILicense>}
*/ */
async generateLicense( public async generateLicense(
licensePeriod: number, licensePeriod: number,
periodInterval: string = 'days', periodInterval: string = 'days',
planId: number, planSlug: string
): ILicense { ): ILicense {
let licenseCode: string; let licenseCode: string;
let repeat: boolean = true; let repeat: boolean = true;
while(repeat) { // Retrieve plan or throw not found error.
const plan = await this.getPlanOrThrowError(planSlug);
while (repeat) {
licenseCode = cryptoRandomString({ length: 10, type: 'numeric' }); licenseCode = cryptoRandomString({ length: 10, type: 'numeric' });
const foundLicenses = await License.query().where('license_code', licenseCode); const foundLicenses = await License.query().where(
'license_code',
licenseCode
);
if (foundLicenses.length === 0) { if (foundLicenses.length === 0) {
repeat = false; repeat = false;
} }
} }
return License.query().insert({ return License.query().insert({
licenseCode, licensePeriod, periodInterval, planId, licenseCode,
licensePeriod,
periodInterval,
planId: plan.id,
}); });
} }
/** /**
* * Generates licenses.
* @param {number} loop * @param {number} loop
* @param {number} licensePeriod * @param {number} licensePeriod
* @param {string} periodInterval * @param {string} periodInterval
* @param {number} planId * @param {number} planId
*/ */
async generateLicenses( public async generateLicenses(
loop = 1, loop = 1,
licensePeriod: number, licensePeriod: number,
periodInterval: string = 'days', periodInterval: string = 'days',
planId: number, planSlug: string
) { ) {
const asyncOpers: Promise<any>[] = []; const asyncOpers: Promise<any>[] = [];
times(loop, () => { times(loop, () => {
const generateOper = this.generateLicense(licensePeriod, periodInterval, planId); const generateOper = this.generateLicense(
licensePeriod,
periodInterval,
planSlug
);
asyncOpers.push(generateOper); asyncOpers.push(generateOper);
}); });
return Promise.all(asyncOpers); return Promise.all(asyncOpers);
@@ -64,38 +122,64 @@ export default class LicenseService {
/** /**
* Disables the given license id on the storage. * Disables the given license id on the storage.
* @param {number} licenseId * @param {string} licenseSlug - License slug.
* @return {Promise} * @return {Promise}
*/ */
async disableLicense(licenseId: number) { public async disableLicense(licenseId: number) {
return License.markLicenseAsDisabled(licenseId, 'id'); const license = await this.getLicenseOrThrowError(licenseId);
this.validateNotDisabledLicense(license);
return License.markLicenseAsDisabled(license.id, 'id');
} }
/** /**
* Deletes the given license id from the storage. * Deletes the given license id from the storage.
* @param licenseId * @param licenseSlug {string} - License slug.
*/ */
async deleteLicense(licenseId: number) { public async deleteLicense(licenseSlug: string) {
return License.query().where('id', licenseId).delete(); 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. * Sends license code to the given customer via SMS or mail message.
* @param {string} licenseCode - License code * @param {string} licenseCode - License code.
* @param {string} phoneNumber - Phone number * @param {string} phoneNumber - Phone number.
* @param {string} email - Email address. * @param {string} email - Email address.
*/ */
async sendLicenseToCustomer(licenseCode: string, phoneNumber: string, email: string) { public async sendLicenseToCustomer(sendLicense: ISendLicenseDTO) {
const agenda = Container.get('agenda'); 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. // Mark the license as used.
await License.markLicenseAsSent(licenseCode); await License.markLicenseAsSent(license.licenseCode);
if (email) { if (sendLicense.email) {
await agenda.schedule('1 second', 'send-license-via-email', { licenseCode, email }); await agenda.schedule('1 second', 'send-license-via-email', {
licenseCode: license.licenseCode,
email,
});
} }
if (phoneNumber) { 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,
});
} }
} }
} }

View File

@@ -1,16 +1,24 @@
import { License } from "system/models"; import { License } from 'system/models';
import PaymentMethod from 'services/Payment/PaymentMethod'; import PaymentMethod from 'services/Payment/PaymentMethod';
import { Plan } from 'system/models'; import { Plan } from 'system/models';
import { IPaymentMethod, ILicensePaymentModel } from 'interfaces'; import { IPaymentMethod, ILicensePaymentModel } from 'interfaces';
import { ILicensePaymentModel } from "interfaces"; import { ILicensePaymentModel } from 'interfaces';
import { PaymentInputInvalid, PaymentAmountInvalidWithPlan } from 'exceptions'; 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. * Payment subscription of organization via license code.
* @param {ILicensePaymentModel} licensePaymentModel - * @param {ILicensePaymentModel} licensePaymentModel -
*/ */
async payment(licensePaymentModel: ILicensePaymentModel, plan: Plan) { async payment(licensePaymentModel: ILicensePaymentModel, plan: Plan) {
this.validateLicensePaymentModel(licensePaymentModel);
const license = await this.getLicenseOrThrowInvalid(licensePaymentModel); const license = await this.getLicenseOrThrowInvalid(licensePaymentModel);
this.validatePaymentAmountWithPlan(license, plan); this.validatePaymentAmountWithPlan(license, plan);
@@ -44,4 +52,14 @@ export default class LicensePaymentMethod extends PaymentMethod implements IPaym
throw new PaymentAmountInvalidWithPlan(); throw new PaymentAmountInvalidWithPlan();
} }
} }
/**
* Validate voucher payload.
* @param {ILicensePaymentModel} licenseModel -
*/
validateLicensePaymentModel(licenseModel: ILicensePaymentModel) {
if (!licenseModel || !licenseModel.licenseCode) {
throw new VoucherCodeRequired();
}
}
} }

View File

@@ -31,10 +31,8 @@ export default class Subscription<PaymentModel> {
) { ) {
this.validateIfPlanHasPriceNoPayment(plan, paymentModel); 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') const subscription = await tenant.$relatedQuery('subscriptions')
.modify('subscriptionBySlug', subscriptionSlug) .modify('subscriptionBySlug', subscriptionSlug)
.first(); .first();

View File

@@ -2,25 +2,52 @@ import { Container } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch'; import { EventSubscriber, On } from 'event-dispatch';
import { map, head } from 'lodash'; import { map, head } from 'lodash';
import events from 'subscribers/events'; import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
import SaleInvoicesCost from 'services/Sales/SalesInvoicesCost'; import SaleInvoicesCost from 'services/Sales/SalesInvoicesCost';
import InventoryItemsQuantitySync from 'services/Inventory/InventoryItemsQuantitySync'; import InventoryItemsQuantitySync from 'services/Inventory/InventoryItemsQuantitySync';
import { InventoryTransaction } from 'models'; import InventoryService from 'services/Inventory/Inventory';
@EventSubscriber() @EventSubscriber()
export class InventorySubscriber { export class InventorySubscriber {
depends: number = 0; depends: number = 0;
startingDate: Date; startingDate: Date;
saleInvoicesCost: SaleInvoicesCost; saleInvoicesCost: SaleInvoicesCost;
tenancy: TenancyService;
itemsQuantitySync: InventoryItemsQuantitySync; itemsQuantitySync: InventoryItemsQuantitySync;
inventoryService: InventoryService;
agenda: any; agenda: any;
/**
* Constructor method.
*/
constructor() { constructor() {
this.saleInvoicesCost = Container.get(SaleInvoicesCost); this.saleInvoicesCost = Container.get(SaleInvoicesCost);
this.itemsQuantitySync = Container.get(InventoryItemsQuantitySync); this.itemsQuantitySync = Container.get(InventoryItemsQuantitySync);
this.inventoryService = Container.get(InventoryService);
this.tenancy = Container.get(TenancyService);
this.agenda = Container.get('agenda'); 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. * Handle run writing the journal entries once the compute items jobs completed.
*/ */

View File

@@ -190,7 +190,9 @@ export default {
onComputeItemCostJobScheduled: 'onComputeItemCostJobScheduled', onComputeItemCostJobScheduled: 'onComputeItemCostJobScheduled',
onComputeItemCostJobStarted: 'onComputeItemCostJobStarted', onComputeItemCostJobStarted: 'onComputeItemCostJobStarted',
onComputeItemCostJobCompleted: 'onComputeItemCostJobCompleted' onComputeItemCostJobCompleted: 'onComputeItemCostJobCompleted',
onInventoryCostEntriesWritten: 'onInventoryCostEntriesWritten'
}, },
/** /**

View File

@@ -6,7 +6,6 @@ exports.up = function(knex) {
table.string('name'); table.string('name');
table.string('description'); table.string('description');
table.decimal('price'); table.decimal('price');
table.decimal('signup_fee');
table.string('currency', 3); table.string('currency', 3);
table.integer('trial_period'); table.integer('trial_period');
@@ -14,7 +13,6 @@ exports.up = function(knex) {
table.integer('invoice_period'); table.integer('invoice_period');
table.string('invoice_interval'); table.string('invoice_interval');
table.timestamps(); table.timestamps();
}); });
}; };

View File

@@ -6,21 +6,42 @@ exports.seed = (knex) => {
// Inserts seed entries // Inserts seed entries
return knex('subscription_plans').insert([ return knex('subscription_plans').insert([
{ {
id: 1, name: 'Free',
name: 'free',
slug: 'free', slug: 'free',
price: 0, price: 0,
active: true, active: true,
currency: 'LYD', currency: 'LYD',
trial_period: 15, trial_period: 7,
trial_interval: 'days', 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', 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,
},
]); ]);
}); });
}; };