- feat: remove unnecessary migrations, controllers and models files.

- feat: metable store
- feat: metable store with settings store.
- feat: settings middleware to auto-save and load.
- feat: DI db manager to master container.
- feat: write some logs to sale invoices.
This commit is contained in:
Ahmed Bouhuolia
2020-09-03 16:51:48 +02:00
parent abefba22ee
commit 9ee7ed89ec
98 changed files with 1697 additions and 2052 deletions

View File

@@ -4,13 +4,11 @@ import config from '@/../config/config';
const knexConfig = knexfile[process.env.NODE_ENV];
const dbManager = knexManager.databaseManagerFactory({
export default () => knexManager.databaseManagerFactory({
knex: knexConfig,
dbManager: {
collate: [],
superUser: config.manager.superUser,
superPassword: config.manager.superPassword,
},
});
export default dbManager;
});

View File

@@ -1,11 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable('permissions', (table) => {
table.increments();
table.string('name');
});
};
exports.down = function (knex) {
return knex.schema.dropTable('permissions');
};

View File

@@ -1,12 +0,0 @@
exports.up = (knex) => knex.schema.createTable('roles', (table) => {
table.increments();
table.string('name');
table.string('description');
table.boolean('predefined').default(false);
table.timestamps();
}).raw('ALTER TABLE `ROLES` AUTO_INCREMENT = 1000');
exports.down = (knex) => knex.schema.dropTable('roles');

View File

@@ -1,12 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable('user_has_roles', (table) => {
table.increments();
table.integer('user_id').unsigned();
table.integer('role_id').unsigned();
});
};
exports.down = function (knex) {
return knex.schema.dropTableIfExists('user_has_roles');
};

View File

@@ -1,10 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable('role_has_accounts', (table) => {
table.increments();
table.integer('role_id').unsigned();
table.integer('account_id').unsigned();
});
};
exports.down = (knex) => knex.schema.dropTableIfExists('role_has_accounts');

View File

@@ -1,11 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable('role_has_permissions', (table) => {
table.increments();
table.integer('role_id').unsigned();
table.integer('permission_id').unsigned();
table.integer('resource_id').unsigned();
});
};
exports.down = (knex) => knex.schema.dropTableIfExists('role_has_permissions');

View File

@@ -1,17 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable('view_roles', (table) => {
table.increments();
table.integer('index');
table.integer('field_id').unsigned();
table.string('comparator');
table.string('value');
table.integer('view_id').unsigned();
}).raw('ALTER TABLE `VIEW_ROLES` AUTO_INCREMENT = 1000').then(() => {
return knex.seed.run({
specific: 'seed_views_roles.js',
});
});
};
exports.down = (knex) => knex.schema.dropTableIfExists('view_roles');

View File

@@ -1,14 +0,0 @@
exports.up = function(knex) {
return knex.schema.createTable('currency_adjustments', (table) => {
table.increments();
table.date('date');
table.string('currency_code');
table.decimal('exchange_rate', 8, 5);
table.string('note');
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('currency_adjustments');
};

View File

@@ -1,12 +0,0 @@
exports.up = function(knex) {
return knex.schema.createTable('recurring_journals', (table) => {
table.increments();
table.string('template_name');
table.timestamps();
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('recurring_journals');
};

View File

@@ -1,14 +0,0 @@
exports.up = function(knex) {
return knex.schema.createTable('budgets', (table) => {
table.increments();
table.string('name');
table.string('fiscal_year');
table.string('period');
table.string('account_types');
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('budgets');
};

View File

@@ -1,14 +0,0 @@
exports.up = function(knex) {
return knex.schema.createTable('budget_entries', (table) => {
table.increments();
table.integer('budget_id').unsigned();
table.integer('account_id').unsigned();
table.decimal('amount', 15, 5);
table.integer('order');
})
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('budget_entries');
};

View File

@@ -1,8 +1,3 @@
/**
* Originally taken from 'w3tecch/express-typescript-boilerplate'
* Credits to the author
*/
import { EventDispatcher as EventDispatcherClass } from 'event-dispatch';
import { Container } from 'typedi';

View File

@@ -0,0 +1,9 @@
class HttpException extends Error {
public status: number;
public message: string;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.message = message;
}
}

View File

@@ -1,147 +0,0 @@
import express from 'express';
import { check, validationResult, oneOf } from 'express-validator';
import { difference } from 'lodash';
import moment from 'moment';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import jwtAuth from '@/http/middleware/jwtAuth';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.use(jwtAuth);
router.post('/',
this.openingBalnace.validation,
asyncMiddleware(this.openingBalnace.handler));
return router;
},
/**
* Opening balance to the given account.
* @param {Request} req -
* @param {Response} res -
*/
openingBalnace: {
validation: [
check('date').optional(),
check('note').optional().trim().escape(),
check('balance_adjustment_account').exists().isNumeric().toInt(),
check('accounts').isArray({ min: 1 }),
check('accounts.*.id').exists().isInt(),
oneOf([
check('accounts.*.debit').exists().isNumeric().toFloat(),
check('accounts.*.credit').exists().isNumeric().toFloat(),
]),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { accounts } = req.body;
const { user } = req;
const form = { ...req.body };
const date = moment(form.date).format('YYYY-MM-DD');
const accountsIds = accounts.map((account) => account.id);
const { Account, ManualJournal } = req.models;
const storedAccounts = await Account.query()
.select(['id']).whereIn('id', accountsIds)
.withGraphFetched('type');
const accountsCollection = new Map(storedAccounts.map((i) => [i.id, i]));
// Get the stored accounts Ids and difference with submit accounts.
const accountsStoredIds = storedAccounts.map((account) => account.id);
const notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
const errorReasons = [];
if (notFoundAccountsIds.length > 0) {
const ids = notFoundAccountsIds.map((a) => parseInt(a, 10));
errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids });
}
if (form.balance_adjustment_account) {
const account = await Account.query().findById(form.balance_adjustment_account);
if (!account) {
errorReasons.push({ type: 'BALANCE.ADJUSTMENT.ACCOUNT.NOT.EXIST', code: 300 });
}
}
if (errorReasons.length > 0) {
return res.boom.badData(null, { errors: errorReasons });
}
const journalEntries = new JournalPoster();
accounts.forEach((account) => {
const storedAccount = accountsCollection.get(account.id);
// Can't continue in case the stored account was not found.
if (!storedAccount) { return; }
const entryModel = new JournalEntry({
referenceType: 'OpeningBalance',
account: account.id,
accountNormal: storedAccount.type.normal,
userId: user.id,
});
if (account.credit) {
entryModel.entry.credit = account.credit;
journalEntries.credit(entryModel);
} else if (account.debit) {
entryModel.entry.debit = account.debit;
journalEntries.debit(entryModel);
}
});
// Calculates the credit and debit balance of stacked entries.
const trial = journalEntries.getTrialBalance();
if (trial.credit !== trial.debit) {
const entryModel = new JournalEntry({
referenceType: 'OpeningBalance',
account: form.balance_adjustment_account,
accountNormal: 'credit',
userId: user.id,
});
if (trial.credit > trial.debit) {
entryModel.entry.credit = Math.abs(trial.credit);
journalEntries.credit(entryModel);
} else if (trial.credit < trial.debit) {
entryModel.entry.debit = Math.abs(trial.debit);
journalEntries.debit(entryModel);
}
}
const manualJournal = await ManualJournal.query().insert({
amount: Math.max(trial.credit, trial.debit),
transaction_type: 'OpeningBalance',
date,
note: form.note,
user_id: user.id,
});
journalEntries.entries = journalEntries.entries.map((entry) => ({
...entry,
referenceId: manualJournal.id,
}));
await Promise.all([
journalEntries.saveEntries(),
journalEntries.saveBalance(),
]);
return res.status(200).send({ id: manualJournal.id });
},
},
};

View File

@@ -1,17 +1,15 @@
import { Request, Response, Router } from 'express';
import { check, validationResult, matchedData, ValidationChain } from 'express-validator';
import { check, ValidationChain } from 'express-validator';
import { Service, Inject } from 'typedi';
import { camelCase, mapKeys } from 'lodash';
import BaseController from '@/http/controllers/BaseController';
import validateMiddleware from '@/http/middleware/validateMiddleware';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import prettierMiddleware from '@/http/middleware/prettierMiddleware';
import AuthenticationService from '@/services/Authentication';
import { IUserOTD, ISystemUser, IRegisterOTD } from '@/interfaces';
import { ServiceError, ServiceErrors } from "@/exceptions";
import { IRegisterDTO } from 'src/interfaces';
@Service()
export default class AuthenticationController {
export default class AuthenticationController extends BaseController{
@Inject()
authService: AuthenticationService;
@@ -88,6 +86,9 @@ export default class AuthenticationController {
]
}
/**
* Send reset password validation schema.
*/
get sendResetPasswordSchema(): ValidationChain[] {
return [
check('email').exists().isEmail().trim().escape(),
@@ -100,10 +101,7 @@ export default class AuthenticationController {
* @param {Response} res
*/
async login(req: Request, res: Response, next: Function): Response {
const userDTO: IUserOTD = mapKeys(matchedData(req, {
locations: ['body'],
includeOptionals: true,
}), (v, k) => camelCase(k));
const userDTO: IUserOTD = this.matchedBodyData(req);
try {
const { token, user } = await this.authService.signIn(
@@ -134,13 +132,10 @@ export default class AuthenticationController {
* @param {Response} res
*/
async register(req: Request, res: Response, next: Function) {
const registerDTO: IRegisterDTO = mapKeys(matchedData(req, {
locations: ['body'],
includeOptionals: true,
}), (v, k) => camelCase(k));
const registerDTO: IRegisterOTD = this.matchedBodyData(req);
try {
const registeredUser = await this.authService.register(registerDTO);
const registeredUser: ISystemUser = await this.authService.register(registerDTO);
return res.status(200).send({
code: 'REGISTER.SUCCESS',
@@ -170,7 +165,7 @@ export default class AuthenticationController {
* @param {Response} res
*/
async sendResetPassword(req: Request, res: Response, next: Function) {
const { email } = req.body;
const { email } = this.matchedBodyData(req);
try {
await this.authService.sendResetPassword(email);

View File

@@ -1,33 +0,0 @@
import express from 'express';
export default {
router() {
const router = express.Router();
return router;
},
reconciliations: {
validation: [
],
async handler(req, res) {
},
},
reconciliation: {
validation: [
body('from_date'),
body('to_date'),
body('closing_balance'),
],
async handler(req, res) {
},
},
}

View File

@@ -1,3 +1,14 @@
import { matchedData } from "express-validator";
import { mapKeys, camelCase, omit } from "lodash";
export default class BaseController {
matchedBodyData(req: Request, options: any) {
const data = matchedData(req, {
locations: ['body'],
includeOptionals: true,
...omit(options, ['locations']), // override any propery except locations.
});
return mapKeys(data, (v, k) => camelCase(k));
}
}

View File

@@ -20,7 +20,6 @@ export default {
*/
router() {
const router = express.Router();
router.use(JWTAuth);
router.post(
'/',

View File

@@ -1,251 +0,0 @@
import express from 'express';
import uniqid from 'uniqid';
import {
check,
body,
param,
validationResult,
} from 'express-validator';
import path from 'path';
import fs from 'fs';
import Mustache from 'mustache';
import moment from 'moment';
import { hashPassword } from '@/utils';
import SystemUser from '@/system/models/SystemUser';
import Invite from '@/system/models/Invite';
import TenantUser from '@/models/TenantUser';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Tenant from '@/system/models/Tenant';
import TenantsManager from '@/system/TenantsManager';
import jwtAuth from '@/http/middleware/jwtAuth';
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
import TenantModel from '@/models/TenantModel';
import Logger from '@/services/Logger';
import Option from '@/models/Option';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.use('/send', jwtAuth);
router.use('/send', TenancyMiddleware);
router.post('/send',
this.invite.validation,
asyncMiddleware(this.invite.handler));
router.post('/accept/:token',
this.accept.validation,
asyncMiddleware(this.accept.handler));
router.get('/invited/:token',
this.invited.validation,
asyncMiddleware(this.invited.handler));
return router;
},
/**
* Invite a user to the authorized user organization.
*/
invite: {
validation: [
body('email').exists().trim().escape(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const form = { ...req.body };
const { user } = req;
const { TenantUser } = req.models;
const foundUser = await SystemUser.query()
.where('email', form.email).first();
if (foundUser) {
return res.status(400).send({
errors: [{ type: 'USER.EMAIL.ALREADY.REGISTERED', code: 100 }],
});
}
const token = uniqid();
const invite = await Invite.query().insert({
email: form.email,
tenant_id: user.tenantId,
token,
});
const tenantUser = await TenantUser.query().insert({
first_name: form.email,
email: form.email,
});
const { Option } = req.models;
const organizationOptions = await Option.query()
.where('key', 'organization_name');
const filePath = path.join(global.rootPath, 'views/mail/UserInvite.html');
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, {
acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`,
fullName: `${user.firstName} ${user.lastName}`,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
organizationName: organizationOptions.getMeta('organization_name'),
});
const mailOptions = {
to: user.email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
subject: `${user.fullName} has invited you to join a Bigcapital`,
html: rendered,
};
mail.sendMail(mailOptions, (error) => {
if (error) {
Logger.log('error', 'Failed send user invite mail', { error, form });
}
Logger.log('info', 'User has been sent invite user email successfuly.', { form });
});
return res.status(200).send();
}
},
/**
* Acceprt the inviation.
*/
accept: {
validation: [
check('first_name').exists().trim().escape(),
check('last_name').exists().trim().escape(),
check('phone_number').exists().trim().escape(),
check('password').exists().trim().escape(),
param('token').exists().trim().escape(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { token } = req.params;
const inviteToken = await Invite.query()
.where('token', token).first();
if (!inviteToken) {
return res.status(404).send({
errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }],
});
}
const form = {
language: 'en',
...req.body,
};
const systemUser = await SystemUser.query()
.where('phone_number', form.phone_number)
.first();
const errorReasons = [];
// Validate there is already registered phone number.
if (systemUser && systemUser.phoneNumber === form.phone_number) {
errorReasons.push({
type: 'PHONE_MUMNER.ALREADY.EXISTS', code: 400,
});
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
// Find the tenant that associated to the given token.
const tenant = await Tenant.query()
.where('id', inviteToken.tenantId).first();
const tenantDb = TenantsManager.knexInstance(tenant.organizationId);
const hashedPassword = await hashPassword(form.password);
const userForm = {
first_name: form.first_name,
last_name: form.last_name,
email: inviteToken.email,
phone_number: form.phone_number,
language: form.language,
active: 1,
};
TenantModel.knexBinded = tenantDb;
const foundTenantUser = await TenantUser.query()
.where('phone_number', form.phone_number).first();
if (foundTenantUser) {
return res.status(400).send({
errors: [{ type: 'PHONE_NUMBER.ALREADY.EXISTS', code: 400 }],
});
}
const insertUserOper = TenantUser.bindKnex(tenantDb)
.query()
.where('email', userForm.email)
.patch({
...userForm,
invite_accepted_at: moment().format('YYYY/MM/DD'),
});
const insertSysUserOper = SystemUser.query().insert({
...userForm,
password: hashedPassword,
tenant_id: inviteToken.tenantId,
});
const deleteInviteTokenOper = Invite.query()
.where('token', inviteToken.token).delete();
await Promise.all([
insertUserOper,
insertSysUserOper,
deleteInviteTokenOper,
]);
return res.status(200).send();
},
},
/**
* Get
*/
invited: {
validation: [
param('token').exists().trim().escape(),
],
async handler(req, res) {
const { token } = req.params;
const inviteToken = await Invite.query()
.where('token', token).first();
if (!inviteToken) {
return res.status(404).send({
errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }],
});
}
// Find the tenant that associated to the given token.
const tenant = await Tenant.query()
.where('id', inviteToken.tenantId).first();
const tenantDb = TenantsManager.knexInstance(tenant.organizationId);
const organizationOptions = await Option.bindKnex(tenantDb).query()
.where('key', 'organization_name');
return res.status(200).send({
data: {
organization_name: organizationOptions.getMeta('organization_name', '') ,
invited_email: inviteToken.email,
},
});
},
},
}

View File

@@ -0,0 +1,140 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response } from 'express';
import {
check,
body,
param,
matchedData,
} from 'express-validator';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import jwtAuth from '@/http/middleware/jwtAuth';
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
import InviteUserService from '@/services/InviteUsers';
import { ServiceErrors, ServiceError } from '@/exceptions';
@Service()
export default class InviteUsersController {
@Inject()
inviteUsersService: InviteUserService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.use('/send', jwtAuth);
router.use('/send', TenancyMiddleware);
router.post('/send', [
body('email').exists().trim().escape(),
],
asyncMiddleware(this.sendInvite),
);
router.post('/accept/:token', [
check('first_name').exists().trim().escape(),
check('last_name').exists().trim().escape(),
check('phone_number').exists().trim().escape(),
check('password').exists().trim().escape(),
param('token').exists().trim().escape(),
],
asyncMiddleware(this.accept)
);
router.get('/invited/:token', [
param('token').exists().trim().escape(),
],
asyncMiddleware(this.invited)
);
return router;
}
/**
* Invite a user to the authorized user organization.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async sendInvite(req: Request, res: Response, next: Function) {
const { email } = req.body;
const { tenantId } = req;
const { user } = req;
try {
await this.inviteUsersService.sendInvite(tenantId, email, user);
} catch (error) {
console.log(error);
if (error instanceof ServiceError) {
if (error.errorType === 'email_already_invited') {
return res.status(400).send({
errors: [{ type: 'EMAIL.ALREADY.INVITED' }],
});
}
}
next(error);
}
return res.status(200).send();
}
/**
* Accept the inviation.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async accept(req: Request, res: Response, next: Function) {
const inviteUserInput: IInviteUserInput = matchedData(req, {
locations: ['body'],
includeOptionals: true,
});
const { token } = req.params;
try {
await this.inviteUsersService.acceptInvite(token, inviteUserInput);
return res.status(200).send();
} catch (error) {
if (error instanceof ServiceErrors) {
const errorReasons = [];
if (error.hasType('email_exists')) {
errorReasons.push({ type: 'EMAIL.EXISTS' });
}
if (error.hasType('phone_number_exists')) {
errorReasons.push({ type: 'PHONE_NUMBER.EXISTS' });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
}
next(error);
}
}
/**
* Check if the invite token is valid.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async invited(req: Request, res: Response, next: Function) {
const { token } = req.params;
try {
await this.inviteUsersService.checkInvite(token);
return res.status(200).send();
} catch (error) {
if (error instanceof ServiceError) {
if (error.errorType === 'invite_token_invalid') {
return res.status(400).send({
errors: [{ type: 'INVITE.TOKEN.INVALID' }],
});
}
}
next(error);
}
}
}

View File

@@ -1,4 +1,4 @@
import express from 'express';
import { Router, Request, Response } from 'express';
import {
check,
param,
@@ -17,22 +17,21 @@ import {
mapFilterRolesToDynamicFilter,
} from '@/lib/ViewRolesBuilder';
import { IItemCategory, IItemCategoryOTD } from '@/interfaces';
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
import BaseController from '@/http/controllers/BaseController';
@Service()
export default class ItemsCategoriesController {
export default class ItemsCategoriesController extends BaseController {
/**
* Router constructor method.
*/
constructor() {
const router = express.Router();
router() {
const router = Router();
router.post('/:id', [
...this.categoryValidationSchema,
...this.specificCategoryValidationSchema,
],
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateParentCategoryExistance),
asyncMiddleware(this.validateSellAccountExistance),
asyncMiddleware(this.validateCostAccountExistance),
@@ -42,7 +41,6 @@ export default class ItemsCategoriesController {
router.post('/',
this.categoryValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateParentCategoryExistance),
asyncMiddleware(this.validateSellAccountExistance),
asyncMiddleware(this.validateCostAccountExistance),
@@ -52,28 +50,24 @@ export default class ItemsCategoriesController {
router.delete('/bulk',
this.categoriesBulkValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateCategoriesIdsExistance),
asyncMiddleware(this.bulkDeleteCategories),
);
router.delete('/:id',
this.specificCategoryValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateItemCategoryExistance),
asyncMiddleware(this.deleteItem),
);
router.get('/:id',
this.specificCategoryValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateItemCategoryExistance),
asyncMiddleware(this.getCategory)
);
router.get('/',
this.categoriesListValidationSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.getList)
);
return router;
@@ -164,7 +158,7 @@ export default class ItemsCategoriesController {
*/
async validateCostAccountExistance(req: Request, res: Response, next: Function) {
const { Account, AccountType } = req.models;
const category: IItemCategoryOTD = { ...req.body };
const category: IItemCategoryOTD = this.matchedBodyData(req);
if (category.costAccountId) {
const COGSType = await AccountType.query().findOne('key', 'cost_of_goods_sold');
@@ -191,7 +185,7 @@ export default class ItemsCategoriesController {
*/
async validateSellAccountExistance(req: Request, res: Response, next: Function) {
const { Account, AccountType } = req.models;
const category: IItemCategoryOTD = { ...req.body };
const category: IItemCategoryOTD = this.matchedBodyData(req);
if (category.sellAccountId) {
const incomeType = await AccountType.query().findOne('key', 'income');
@@ -218,7 +212,7 @@ export default class ItemsCategoriesController {
*/
async validateInventoryAccountExistance(req: Request, res: Response, next: Function) {
const { Account, AccountType } = req.models;
const category: IItemCategoryOTD = { ...req.body };
const category: IItemCategoryOTD = this.matchedBodyData(req);
if (category.inventoryAccountId) {
const otherAsset = await AccountType.query().findOne('key', 'other_asset');
@@ -244,7 +238,7 @@ export default class ItemsCategoriesController {
* @param {Function} next
*/
async validateParentCategoryExistance(req: Request, res: Response, next: Function) {
const category: IItemCategory = { ...req.body };
const category: IItemCategory = this.matchedBodyData(req);
const { ItemCategory } = req.models;
if (category.parentCategoryId) {
@@ -290,7 +284,7 @@ export default class ItemsCategoriesController {
*/
async newCategory(req: Request, res: Response) {
const { user } = req;
const category: IItemCategory = { ...req.body };
const category: IItemCategory = this.matchedBodyData(req);
const { ItemCategory } = req.models;
const storedCategory = await ItemCategory.query().insert({
@@ -308,7 +302,7 @@ export default class ItemsCategoriesController {
*/
async editCategory(req: Request, res: Response) {
const { id } = req.params;
const category: IItemCategory = { ...req.body };
const category: IItemCategory = this.matchedBodyData(req);
const { ItemCategory } = req.models;
const updateItemCategory = await ItemCategory.query()

View File

@@ -5,10 +5,10 @@ import {
query,
validationResult,
} from 'express-validator';
import Container from 'typedi';
import fs from 'fs';
import { difference } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Logger from '@/services/Logger';
const fsPromises = fs.promises;
@@ -70,6 +70,8 @@ export default {
// check('attachment').exists(),
],
async handler(req, res) {
const Logger = Container.get('logger');
if (!req.files.attachment) {
return res.status(400).send({
errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }],
@@ -93,9 +95,9 @@ export default {
try {
await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`);
Logger.log('info', 'Attachment uploaded successfully');
Logger.info('[attachment] uploaded successfully');
} catch (error) {
Logger.log('info', 'Attachment uploading failed.', { error });
Logger.info('[attachment] uploading failed.', { error });
}
const media = await Media.query().insert({
@@ -114,6 +116,7 @@ export default {
query('ids.*').exists().isNumeric().toInt(),
],
async handler(req, res) {
const Logger = Container.get('logger');
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
@@ -142,12 +145,12 @@ export default {
});
await Promise.all(unlinkOpers).then((resolved) => {
resolved.forEach(() => {
Logger.log('error', 'Attachment file has been deleted.');
Logger.info('[attachment] file has been deleted.');
});
})
.catch((errors) => {
errors.forEach((error) => {
Logger.log('error', 'Delete item attachment file delete failed.', { error });
Logger.info('[attachment] Delete item attachment file delete failed.', { error });
})
});

View File

@@ -1,97 +0,0 @@
import express from 'express';
import { body, query, validationResult } from 'express-validator';
import { pick } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.post('/',
this.saveOptions.validation,
asyncMiddleware(this.saveOptions.handler));
router.get('/',
this.getOptions.validation,
asyncMiddleware(this.getOptions.handler));
return router;
},
/**
* Saves the given options to the storage.
*/
saveOptions: {
validation: [
body('options').isArray({ min: 1 }),
body('options.*.key').exists(),
body('options.*.value').exists(),
body('options.*.group').exists(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'VALIDATION_ERROR', ...validationErrors,
});
}
const { Option } = req.models;
const form = { ...req.body };
const optionsCollections = await Option.query();
const errorReasons = [];
const notDefinedOptions = Option.validateDefined(form.options);
if (notDefinedOptions.length) {
errorReasons.push({
type: 'OPTIONS.KEY.NOT.DEFINED',
code: 200,
keys: notDefinedOptions.map((o) => ({ ...pick(o, ['key', 'group']) })),
});
}
if (errorReasons.length) {
return res.status(400).send({ errors: errorReasons });
}
form.options.forEach((option) => {
optionsCollections.setMeta({ ...option });
});
await optionsCollections.saveMeta();
return res.status(200).send({ options: form });
},
},
/**
* Retrieve the application options from the storage.
*/
getOptions: {
validation: [
query('key').optional(),
query('group').optional(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'VALIDATION_ERROR', ...validationErrors,
});
}
const { Option } = req.models;
const filter = { ...req.query };
const options = await Option.query().onBuild((builder) => {
if (filter.key) {
builder.where('key', filter.key);
}
if (filter.group) {
builder.where('group', filter.group);
}
});
return res.status(200).send({ options: options.metadata });
},
},
};

View File

@@ -0,0 +1,65 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response } from 'express';
import { check, matchedData } from 'express-validator';
import { mapKeys, camelCase } from 'lodash';
import asyncMiddleware from "@/http/middleware/asyncMiddleware";
import validateMiddleware from '@/http/middleware/validateMiddleware';
import OrganizationService from '@/services/Organization';
import { ServiceError } from '@/exceptions';
@Service()
export default class OrganizationController {
@Inject()
organizationService: OrganizationService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.post(
'/build', [
check('organization_id').exists().trim().escape(),
],
validateMiddleware,
asyncMiddleware(this.build.bind(this))
);
return router;
}
/**
* Builds tenant database and seed initial data.
* @param {Request} req - Express request.
* @param {Response} res - Express response.
* @param {NextFunction} next
*/
async build(req: Request, res: Response, next: Function) {
const buildOTD = mapKeys(matchedData(req, {
locations: ['body'],
includeOptionals: true,
}), (v, k) => camelCase(k));
try {
await this.organizationService.build(buildOTD.organizationId);
return res.status(200).send({
type: 'ORGANIZATION.DATABASE.INITIALIZED',
});
} catch (error) {
if (error instanceof ServiceError) {
if (error.errorType === 'tenant_not_found') {
return res.status(400).send({
errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'tenant_initialized') {
return res.status(400).send({
errors: [{ type: 'TENANT.DATABASE.ALREADY.BUILT', code: 200 }],
});
}
}
next(error);
}
}
}

View File

@@ -1,346 +0,0 @@
/* eslint-disable no-unused-vars */
import express from 'express';
import { check, validationResult } from 'express-validator';
import { difference } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Role from '@/models/Role';
import Permission from '@/models/Permission';
import Resource from '@/models/Resource';
import knex from '@/database/knex';
const AccessControllSchema = [
{
resource: 'items',
label: 'products_services',
permissions: ['create', 'edit', 'delete', 'view'],
fullAccess: true,
ownControl: true,
},
];
const getResourceSchema = (resource) => AccessControllSchema
.find((schema) => schema.resource === resource);
const getResourcePermissions = (resource) => {
const foundResource = getResourceSchema(resource);
return foundResource ? foundResource.permissions : [];
};
const findNotFoundResources = (resourcesSlugs) => {
const schemaResourcesSlugs = AccessControllSchema.map((s) => s.resource);
return difference(resourcesSlugs, schemaResourcesSlugs);
};
const findNotFoundPermissions = (permissions, resourceSlug) => {
const schemaPermissions = getResourcePermissions(resourceSlug);
return difference(permissions, schemaPermissions);
};
export default {
/**
* Router constructor method.
*/
router() {
const router = express.Router();
router.post('/',
this.newRole.validation,
asyncMiddleware(this.newRole.handler));
router.post('/:id',
this.editRole.validation,
asyncMiddleware(this.editRole.handler.bind(this)));
router.delete('/:id',
this.deleteRole.validation,
asyncMiddleware(this.deleteRole.handler));
return router;
},
/**
* Creates a new role.
*/
newRole: {
validation: [
check('name').exists().trim().escape(),
check('description').optional().trim().escape(),
check('permissions').isArray({ min: 0 }),
check('permissions.*.resource_slug').exists().whitelist('^[a-z0-9]+(?:-[a-z0-9]+)*$'),
check('permissions.*.permissions').isArray({ min: 1 }),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { name, description, permissions } = req.body;
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
const permissionsSlugs = [];
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
const errorReasons = [];
const notFoundPermissions = [];
if (resourcesNotFound.length > 0) {
errorReasons.push({
type: 'RESOURCE_SLUG_NOT_FOUND', code: 100, resources: resourcesNotFound,
});
}
permissions.forEach((perm) => {
const abilities = perm.permissions.map((ability) => ability);
// Gets the not found permissions in the schema.
const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug);
if (notFoundAbilities.length > 0) {
notFoundPermissions.push({
resource_slug: perm.resource_slug,
permissions: notFoundAbilities,
});
} else {
const perms = perm.permissions || [];
perms.forEach((permission) => {
if (perms.indexOf(permission) !== -1) {
permissionsSlugs.push(permission);
}
});
}
});
if (notFoundPermissions.length > 0) {
errorReasons.push({
type: 'PERMISSIONS_SLUG_NOT_FOUND',
code: 200,
permissions: notFoundPermissions,
});
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
// Permissions.
const [resourcesCollection, permsCollection] = await Promise.all([
Resource.query((query) => { query.whereIn('name', resourcesSlugs); }).fetchAll(),
Permission.query((query) => { query.whereIn('name', permissionsSlugs); }).fetchAll(),
]);
const notStoredResources = difference(
resourcesSlugs, resourcesCollection.map((s) => s.name),
);
const notStoredPermissions = difference(
permissionsSlugs, permsCollection.map((perm) => perm.slug),
);
const insertThread = [];
if (notStoredResources.length > 0) {
insertThread.push(knex('resources').insert([
...notStoredResources.map((resource) => ({ name: resource })),
]));
}
if (notStoredPermissions.length > 0) {
insertThread.push(knex('permissions').insert([
...notStoredPermissions.map((permission) => ({ name: permission })),
]));
}
await Promise.all(insertThread);
const [storedPermissions, storedResources] = await Promise.all([
Permission.query((q) => { q.whereIn('name', permissionsSlugs); }).fetchAll(),
Resource.query((q) => { q.whereIn('name', resourcesSlugs); }).fetchAll(),
]);
const storedResourcesSet = new Map(storedResources.map((resource) => [
resource.attributes.name, resource.attributes.id,
]));
const storedPermissionsSet = new Map(storedPermissions.map((perm) => [
perm.attributes.name, perm.attributes.id,
]));
const role = Role.forge({ name, description });
await role.save();
const roleHasPerms = permissions.map((resource) => resource.permissions.map((perm) => ({
role_id: role.id,
resource_id: storedResourcesSet.get(resource.resource_slug),
permission_id: storedPermissionsSet.get(perm),
})));
if (roleHasPerms.length > 0) {
await knex('role_has_permissions').insert(roleHasPerms[0]);
}
return res.status(200).send({ id: role.get('id') });
},
},
/**
* Edit the give role.
*/
editRole: {
validation: [
check('name').exists().trim().escape(),
check('description').optional().trim().escape(),
check('permissions').isArray({ min: 0 }),
check('permissions.*.resource_slug').exists().whitelist('^[a-z0-9]+(?:-[a-z0-9]+)*$'),
check('permissions.*.permissions').isArray({ min: 1 }),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { id } = req.params;
const role = await Role.where('id', id).fetch();
if (!role) {
return res.boom.notFound(null, {
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
});
}
const { permissions } = req.body;
const errorReasons = [];
const permissionsSlugs = [];
const notFoundPermissions = [];
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
if (resourcesNotFound.length > 0) {
errorReasons.push({
type: 'RESOURCE_SLUG_NOT_FOUND',
code: 100,
resources: resourcesNotFound,
});
}
permissions.forEach((perm) => {
const abilities = perm.permissions.map((ability) => ability);
// Gets the not found permissions in the schema.
const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug);
if (notFoundAbilities.length > 0) {
notFoundPermissions.push({
resource_slug: perm.resource_slug, permissions: notFoundAbilities,
});
} else {
const perms = perm.permissions || [];
perms.forEach((permission) => {
if (perms.indexOf(permission) !== -1) {
permissionsSlugs.push(permission);
}
});
}
});
if (notFoundPermissions.length > 0) {
errorReasons.push({
type: 'PERMISSIONS_SLUG_NOT_FOUND',
code: 200,
permissions: notFoundPermissions,
});
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
// Permissions.
const [resourcesCollection, permsCollection] = await Promise.all([
Resource.query((query) => { query.whereIn('name', resourcesSlugs); }).fetchAll(),
Permission.query((query) => { query.whereIn('name', permissionsSlugs); }).fetchAll(),
]);
const notStoredResources = difference(
resourcesSlugs, resourcesCollection.map((s) => s.name),
);
const notStoredPermissions = difference(
permissionsSlugs, permsCollection.map((perm) => perm.slug),
);
const insertThread = [];
if (notStoredResources.length > 0) {
insertThread.push(knex('resources').insert([
...notStoredResources.map((resource) => ({ name: resource })),
]));
}
if (notStoredPermissions.length > 0) {
insertThread.push(knex('permissions').insert([
...notStoredPermissions.map((permission) => ({ name: permission })),
]));
}
await Promise.all(insertThread);
const [storedPermissions, storedResources] = await Promise.all([
Permission.query((q) => { q.whereIn('name', permissionsSlugs); }).fetchAll(),
Resource.query((q) => { q.whereIn('name', resourcesSlugs); }).fetchAll(),
]);
const storedResourcesSet = new Map(storedResources.map((resource) => [
resource.attributes.name, resource.attributes.id,
]));
const storedPermissionsSet = new Map(storedPermissions.map((perm) => [
perm.attributes.name, perm.attributes.id,
]));
await role.save();
const savedRoleHasPerms = await knex('role_has_permissions').where({
role_id: role.id,
});
console.log(savedRoleHasPerms);
// const roleHasPerms = permissions.map((resource) => resource.permissions.map((perm) => ({
// role_id: role.id,
// resource_id: storedResourcesSet.get(resource.resource_slug),
// permission_id: storedPermissionsSet.get(perm),
// })));
// if (roleHasPerms.length > 0) {
// await knex('role_has_permissions').insert(roleHasPerms[0]);
// }
return res.status(200).send({ id: role.get('id') });
},
},
deleteRole: {
validation: [],
async handler(req, res) {
const { id } = req.params;
const role = await Role.where('id', id).fetch();
if (!role) {
return res.boom.notFound();
}
if (role.attributes.predefined) {
return res.boom.badRequest(null, {
errors: [{ type: 'ROLE_PREDEFINED', code: 100 }],
});
}
await knex('role_has_permissions')
.where('role_id', role.id).delete({ require: false });
await role.destroy();
return res.status(200).send();
},
},
getRole: {
validation: [],
handler(req, res) {
return res.status(200).send();
},
},
};

View File

@@ -0,0 +1,89 @@
import { Router, Request, Response } from 'express';
import { body, query, validationResult } from 'express-validator';
import { pick } from 'lodash';
import { IOptionDTO, IOptionsDTO } from '@/interfaces';
import BaseController from '@/http/controllers/BaseController';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
export default class SettingsController extends BaseController{
/**
* Router constructor.
*/
router() {
const router = Router();
router.post('/',
this.saveSettingsValidationSchema,
asyncMiddleware(this.saveSettings.bind(this)));
router.get('/',
this.getSettingsSchema,
asyncMiddleware(this.getSettings.bind(this)));
return router;
}
/**
* Save settings validation schema.
*/
get saveSettingsValidationSchema() {
return [
body('options').isArray({ min: 1 }),
body('options.*.key').exists(),
body('options.*.value').exists(),
body('options.*.group').exists(),
];
}
/**
* Retrieve the application options from the storage.
*/
get getSettingsSchema() {
return [
query('key').optional(),
query('group').optional(),
];
}
/**
* Saves the given options to the storage.
*/
saveSettings(req: Request, res: Response) {
const { Option } = req.models;
const optionsDTO: IOptionsDTO = this.matchedBodyData(req);
const { settings } = req;
const errorReasons: { type: string, code: number, keys: [] }[] = [];
const notDefinedOptions = Option.validateDefined(optionsDTO.options);
if (notDefinedOptions.length) {
errorReasons.push({
type: 'OPTIONS.KEY.NOT.DEFINED',
code: 200,
keys: notDefinedOptions.map((o) => ({
...pick(o, ['key', 'group'])
})),
});
}
if (errorReasons.length) {
return res.status(400).send({ errors: errorReasons });
}
optionsDTO.options.forEach((option: IOptionDTO) => {
settings.set({ ...option });
});
return res.status(200).send({ });
}
/**
* Retrieve settings.
* @param {Request} req
* @param {Response} res
*/
getSettings(req: Request, res: Response) {
const { settings } = req;
const allSettings = settings.all();
return res.status(200).send({ settings: allSettings });
}
};

View File

@@ -1,8 +1,9 @@
import { Inject } from 'typedi';
import { Plan } from '@/system/models';
import BaseController from '@/http/controllers/BaseController';
import SubscriptionService from '@/services/Subscription/SubscriptionService';
export default class PaymentMethodController {
export default class PaymentMethodController extends BaseController {
@Inject()
subscriptionService: SubscriptionService;
@@ -16,7 +17,7 @@ export default class PaymentMethodController {
* @return {Response|void}
*/
async validatePlanSlugExistance(req: Request, res: Response, next: Function) {
const { planSlug } = req.body;
const { planSlug } = this.matchedBodyData(req);
const foundPlan = await Plan.query().where('slug', planSlug).first();
if (!foundPlan) {

View File

@@ -1,17 +1,19 @@
import { Container, Service } from 'typedi';
import { Inject, Service } from 'typedi';
import { Router, Request, Response } from 'express';
import { check, param, query, ValidationSchema } from 'express-validator';
import { Voucher, Plan } from '@/system/models';
import validateMiddleware from '@/http/middleware/validateMiddleware';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import PaymentMethodController from '@/http/controllers/Subscription/PaymentMethod';
import PrettierMiddleware from '@/http/middleware/PrettierMiddleware';
import {
NotAllowedChangeSubscriptionPlan
} from '@/exceptions';
@Service()
export default class PaymentViaVoucherController extends PaymentMethodController {
@Inject('logger')
logger: any;
/**
* Router constructor.
*/
@@ -22,7 +24,6 @@ export default class PaymentViaVoucherController extends PaymentMethodController
'/payment',
this.paymentViaVoucherSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateVoucherCodeExistance.bind(this)),
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
asyncMiddleware(this.validateVoucherAndPlan.bind(this)),
@@ -48,7 +49,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController
* @param {Response} res
*/
async validateVoucherCodeExistance(req: Request, res: Response, next: Function) {
const { voucherCode } = req.body;
const { voucherCode } = this.matchedBodyData(req);
this.logger.info('[voucher_payment] trying to validate voucher code.', { voucherCode });
const foundVoucher = await Voucher.query()
.modify('filterActiveVoucher')
@@ -70,7 +72,8 @@ export default class PaymentViaVoucherController extends PaymentMethodController
* @param {Function} next
*/
async validateVoucherAndPlan(req: Request, res: Response, next: Function) {
const { planSlug, voucherCode } = req.body;
const { planSlug, voucherCode } = this.matchedBodyData(req);
this.logger.info('[voucher_payment] trying to validate voucher with the plan.', { voucherCode });
const voucher = await Voucher.query().findOne('voucher_code', voucherCode);
const plan = await Plan.query().findOne('slug', planSlug);
@@ -90,11 +93,12 @@ export default class PaymentViaVoucherController extends PaymentMethodController
* @return {Response}
*/
async paymentViaVoucher(req: Request, res: Response, next: Function) {
const { planSlug, voucherCode } = req.body;
const { planSlug, voucherCode } = this.matchedBodyData(req);
const { tenant } = req;
try {
await this.subscriptionService.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
await this.subscriptionService
.subscriptionViaVoucher(tenant.id, planSlug, voucherCode);
return res.status(200).send({
type: 'PAYMENT.SUCCESSFULLY.MADE',

View File

@@ -1,16 +1,15 @@
import { Router, Request, Response } from 'express'
import { repeat, times, orderBy } from 'lodash';
import { check, oneOf, param, query, ValidationChain } from 'express-validator';
import { Container, Service, Inject } from 'typedi';
import { check, oneOf, ValidationChain } from 'express-validator';
import { Service, Inject } from 'typedi';
import { Voucher, Plan } from '@/system/models';
import BaseController from '@/http/controllers/BaseController';
import VoucherService from '@/services/Payment/Voucher';
import validateMiddleware from '@/http/middleware/validateMiddleware';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import PrettierMiddleware from '@/http/middleware/prettierMiddleware';
import { IVouchersFilter } from '@/interfaces';
@Service()
export default class VouchersController {
export default class VouchersController extends BaseController {
@Inject()
voucherService: VoucherService;
@@ -24,14 +23,12 @@ export default class VouchersController {
'/generate',
this.generateVoucherSchema,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validatePlanExistance.bind(this)),
asyncMiddleware(this.generateVoucher.bind(this)),
);
router.post(
'/disable/:voucherId',
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.validateVoucherExistance.bind(this)),
asyncMiddleware(this.validateNotDisabledVoucher.bind(this)),
asyncMiddleware(this.disableVoucher.bind(this)),
@@ -40,18 +37,15 @@ export default class VouchersController {
'/send',
this.sendVoucherSchemaValidation,
validateMiddleware,
PrettierMiddleware,
asyncMiddleware(this.sendVoucher.bind(this)),
);
router.delete(
'/:voucherId',
PrettierMiddleware,
asyncMiddleware(this.validateVoucherExistance.bind(this)),
asyncMiddleware(this.deleteVoucher.bind(this)),
);
router.get(
'/',
PrettierMiddleware,
asyncMiddleware(this.listVouchers.bind(this)),
);
return router;
@@ -106,7 +100,8 @@ export default class VouchersController {
* @param {Function} next
*/
async validatePlanExistance(req: Request, res: Response, next: Function) {
const planId: number = req.body.planId || req.params.planId;
const body = this.matchedBodyData(req);
const planId: number = body.planId || req.params.planId;
const foundPlan = await Plan.query().findById(planId);
if (!foundPlan) {
@@ -124,7 +119,9 @@ export default class VouchersController {
* @param {Function}
*/
async validateVoucherExistance(req: Request, res: Response, next: Function) {
const voucherId = req.body.voucherId || req.params.voucherId;
const body = this.matchedBodyData(req);
const voucherId = body.voucherId || req.params.voucherId;
const foundVoucher = await Voucher.query().findById(voucherId);
if (!foundVoucher) {
@@ -160,7 +157,7 @@ export default class VouchersController {
* @return {Response}
*/
async generateVoucher(req: Request, res: Response, next: Function) {
const { loop = 10, period, periodInterval, planId } = req.body;
const { loop = 10, period, periodInterval, planId } = this.matchedBodyData(req);
try {
await this.voucherService.generateVouchers(
@@ -211,7 +208,7 @@ export default class VouchersController {
* @return {Response}
*/
async sendVoucher(req: Request, res: Response) {
const { phoneNumber, email, period, periodInterval, planId } = req.body;
const { phoneNumber, email, period, periodInterval, planId } = this.matchedBodyData(req);
const voucher = await Voucher.query()
.modify('filterActiveVoucher')

View File

@@ -2,6 +2,7 @@ import { Router } from 'express'
import { Container, Service } from 'typedi';
import JWTAuth from '@/http/middleware/jwtAuth';
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
import AttachCurrentTenantUser from '@/http/middleware/AttachCurrentTenantUser';
import PaymentViaVoucherController from '@/http/controllers/Subscription/PaymentViaVoucher';
@Service()
@@ -13,6 +14,7 @@ export default class SubscriptionController {
const router = Router();
router.use(JWTAuth);
router.use(AttachCurrentTenantUser);
router.use(TenancyMiddleware);
router.use('/voucher', Container.get(PaymentViaVoucherController).router());

View File

@@ -1,7 +1,18 @@
import express from 'express';
import { Container } from 'typedi';
// Middlewares
import JWTAuth from '@/http/middleware/jwtAuth';
import AttachCurrentTenantUser from '@/http/middleware/AttachCurrentTenantUser';
import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware';
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
import EnsureTenantIsInitialized from '@/http/middleware/EnsureTenantIsInitialized';
import SettingsMiddleware from '@/http/middleware/SettingsMiddleware';
// Routes
import Authentication from '@/http/controllers/Authentication';
import InviteUsers from '@/http/controllers/InviteUsers';
import Organization from '@/http/controllers/Organization';
import Users from '@/http/controllers/Users';
import Items from '@/http/controllers/Items';
import ItemCategories from '@/http/controllers/ItemCategories';
@@ -11,7 +22,7 @@ import Views from '@/http/controllers/Views';
import Accounting from '@/http/controllers/Accounting';
import FinancialStatements from '@/http/controllers/FinancialStatements';
import Expenses from '@/http/controllers/Expenses';
import Options from '@/http/controllers/Options';
import Settings from '@/http/controllers/Settings';
import Currencies from '@/http/controllers/Currencies';
import Customers from '@/http/controllers/Customers';
import Vendors from '@/http/controllers/Vendors';
@@ -20,19 +31,16 @@ import Purchases from '@/http/controllers/Purchases';
import Resources from './controllers/Resources';
import ExchangeRates from '@/http/controllers/ExchangeRates';
import Media from '@/http/controllers/Media';
import JWTAuth from '@/http/middleware/jwtAuth';
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
import Ping from '@/http/controllers/Ping';
import Agendash from '@/http/controllers/Agendash';
import Subscription from '@/http/controllers/Subscription';
import VouchersController from '@/http/controllers/Subscription/Vouchers';
import TenantDependencyInjection from '@/http/middleware/TenantDependencyInjection';
import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware';
export default (app) => {
app.use('/api/auth', Container.get(Authentication).router());
app.use('/api/invite', InviteUsers.router());
app.use('/api/invite', Container.get(InviteUsers).router());
app.use('/api/organization', Container.get(Organization).router());
app.use('/api/vouchers', Container.get(VouchersController).router());
app.use('/api/subscription', Container.get(Subscription).router());
app.use('/api/ping', Container.get(Ping).router());
@@ -40,20 +48,23 @@ export default (app) => {
const dashboard = express.Router();
dashboard.use(JWTAuth);
dashboard.use(AttachCurrentTenantUser)
dashboard.use(TenancyMiddleware);
dashboard.use(SubscriptionMiddleware('main'));
dashboard.use('/api/currencies', Currencies.router());
dashboard.use(EnsureTenantIsInitialized);
dashboard.use(SettingsMiddleware);
dashboard.use('/api/users', Users.router());
dashboard.use('/api/currencies', Currencies.router());
dashboard.use('/api/accounts', Accounts.router());
dashboard.use('/api/account_types', AccountTypes.router());
dashboard.use('/api/accounting', Accounting.router());
dashboard.use('/api/views', Views.router());
dashboard.use('/api/items', Container.get(Items).router());
dashboard.use('/api/item_categories', Container.get(ItemCategories));
dashboard.use('/api/item_categories', Container.get(ItemCategories).router());
dashboard.use('/api/expenses', Expenses.router());
dashboard.use('/api/financial_statements', FinancialStatements.router());
dashboard.use('/api/options', Options.router());
dashboard.use('/api/settings', Container.get(Settings).router());
dashboard.use('/api/sales', Sales.router());
dashboard.use('/api/customers', Customers.router());
dashboard.use('/api/vendors', Vendors.router());

View File

@@ -0,0 +1,29 @@
import { Container } from 'typedi';
import { SystemUser } from '@/system/models';
/**
* Attach user to req.currentUser
* @param {Request} req Express req Object
* @param {Response} res Express res Object
* @param {NextFunction} next Express next Function
*/
const attachCurrentUser = async (req: Request, res: Response, next: Function) => {
const Logger = Container.get('logger');
try {
Logger.info('[attach_user_middleware] finding system user by id.');
const user = await SystemUser.query().findById(req.token.id);
if (!user) {
Logger.info('[attach_user_middleware] the system user not found.');
return res.boom.unauthorized();
}
req.user = user;
return next();
} catch (e) {
Logger.error('[attach_user_middleware] error attaching user to req: %o', e);
return next(e);
}
};
export default attachCurrentUser;

View File

@@ -0,0 +1,17 @@
import { Container } from 'typedi';
export default (req: Request, res: Response, next: Function) => {
const Logger = Container.get('logger');
if (!req.tenant) {
Logger.info('[ensure_tenant_intialized_middleware] no tenant model.');
throw new Error('Should load this middleware after `TenancyMiddleware`.');
}
if (!req.tenant.initialized) {
Logger.info('[ensure_tenant_intialized_middleware] tenant database not initalized.');
return res.status(400).send({
errors: [{ type: 'TENANT.DATABASE.NOT.INITALIZED' }],
});
}
next();
};

View File

@@ -0,0 +1,8 @@
import { NextFunction, Request } from 'express';
function loggerMiddleware(request: Request, response: Response, next: NextFunction) {
console.log(`${request.method} ${request.path}`);
next();
}
export default loggerMiddleware;

View File

@@ -0,0 +1,29 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import SettingsStore from '@/services/Settings/SettingsStore';
export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantId } = req.user;
const { knex } = req;
const Logger = Container.get('logger');
const tenantContainer = Container.of(`tenant-${tenantId}`);
if (tenantContainer && !tenantContainer.has('settings')) {
Logger.info('[settings_middleware] initialize settings store.');
const settings = new SettingsStore(knex);
Logger.info('[settings_middleware] load settings from storage or cache.');
await settings.load();
tenantContainer.set('settings', settings);
}
Logger.info('[settings_middleware] get settings instance from container.');
const settings = tenantContainer.get('settings');
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
}

View File

@@ -1,34 +1,29 @@
import fs from 'fs';
import path from 'path';
import TenantsManager from '@/system/TenantsManager';
import TenantModel from '@/models/TenantModel';
import { Container } from 'typedi';
function loadModelsFromDirectory() {
const models = {};
fs.readdirSync('src/models/').forEach((filename) => {
const model = {
path: path.join(__dirname, 'src/models/', filename),
name: filename.replace(/\.[^/.]+$/, ''),
};
// eslint-disable-next-line global-require
model.resource = require(`@/models/${model.name}`);
models[model.name] = model;
});
return models;
}
import TenantsManager from '@/system/TenantsManager';
import tenantModelsLoader from '@/loaders/tenantModels';
export default async (req, res, next) => {
const Logger = Container.get('logger');
const organizationId = req.headers['organization-id'] || req.query.organization;
const notFoundOrganization = () => res.boom.unauthorized(
'Organization identication not found.',
{ errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND', code: 100 }] },
);
const notFoundOrganization = () => {
Logger.info('[tenancy_middleware] organization id not found.');
return res.boom.unauthorized(
'Organization identication not found.',
{ errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND', code: 100 }] },
);
}
// In case the given organization not found.
if (!organizationId) {
return notFoundOrganization();
}
const tenant = await TenantsManager.getTenant(organizationId);
const tenantsManager = Container.get(TenantsManager);
Logger.info('[tenancy_middleware] trying get tenant by org. id from storage.');
const tenant = await tenantsManager.getTenant(organizationId);
Logger.info('[tenancy_middleware] initializing tenant knex instance.');
const tenantKnex = tenantsManager.knexInstance(organizationId);
// When the given organization id not found on the system storage.
if (!tenant) {
@@ -36,30 +31,22 @@ export default async (req, res, next) => {
}
// When user tenant not match the given organization id.
if (tenant.id !== req.user.tenantId) {
Logger.info('[tenancy_middleware] authorized user not match org. tenant.');
return res.boom.unauthorized();
}
const knex = TenantsManager.knexInstance(organizationId);
const models = loadModelsFromDirectory();
TenantModel.knexBinded = knex;
req.knex = knex;
const models = tenantModelsLoader(tenantKnex);
req.knex = tenantKnex;
req.organizationId = organizationId;
req.tenant = tenant;
req.tenantId = tenant.id;
req.models = {
...Object.values(models).reduce((acc, model) => {
if (typeof model.resource.default !== 'undefined' &&
typeof model.resource.default.requestModel === 'function' &&
model.resource.default.requestModel() &&
model.name !== 'TenantModel') {
acc[model.name] = model.resource.default.bindKnex(knex);
}
return acc;
}, {}),
};
Container.of(`tenant-${tenant.id}`).set('models', {
...req.models,
});
req.models = models;
const tenantContainer = Container.of(`tenant-${tenant.id}`);
tenantContainer.set('models', models);
tenantContainer.set('knex', tenantKnex);
tenantContainer.set('tenant', tenant);
Logger.info('[tenancy_middleware] tenant dependencies injected to container.');
next();
};
}

View File

@@ -1,13 +0,0 @@
import { Request, Response } from 'express';
import { Container } from 'typedi';
export default async (req: Request, res: Response, next: Function) => {
const { organizationId, knex } = req;
if (!organizationId || !knex) {
throw new Error('Should load `TenancyMiddleware` before this middleware.');
}
Container.of(`tenant-${organizationId}`).set('knex', knex);
next();
};

View File

@@ -1,16 +0,0 @@
/* eslint-disable consistent-return */
const authorization = (resourceName) => (...permissions) => (req, res, next) => {
const { user } = req;
const onError = () => {
res.boom.unauthorized();
};
user.hasPermissions(resourceName, permissions)
.then((authorized) => {
if (!authorized) {
return onError();
}
next();
}).catch(onError);
};
export default authorization;

View File

@@ -1,31 +1,31 @@
/* eslint-disable consistent-return */
import { Container } from 'typedi';
import jwt from 'jsonwebtoken';
import SystemUser from '@/system/models/SystemUser';
import config from '@/../config/config';
const authMiddleware = (req, res, next) => {
const { JWT_SECRET_KEY } = process.env;
const Logger = Container.get('logger');
const token = req.headers['x-access-token'] || req.query.token;
const onError = () => { res.boom.unauthorized(); };
const onError = () => {
Logger.info('[auth_middleware] jwt verify error.');
res.boom.unauthorized();
};
const onSuccess = (decoded) => {
req.token = decoded;
Logger.info('[auth_middleware] jwt verify success.');
next();
};
if (!token) { return onError(); }
const verify = new Promise((resolve, reject) => {
jwt.verify(token, JWT_SECRET_KEY, async (error, decoded) => {
jwt.verify(token, config.jwtSecret, async (error, decoded) => {
if (error) {
reject(error);
} else {
// eslint-disable-next-line no-underscore-dangle
req.user = await SystemUser.query().findById(decoded._id);
if (!req.user) {
return onError();
}
resolve(decoded);
}
});
});
verify.then(() => { next(); }).catch(onError);
verify.then(onSuccess).catch(onError);
};
export default authMiddleware;

View File

@@ -1,34 +0,0 @@
import { Request, Response } from 'express';
import { camelCase, snakeCase, mapKeys } from 'lodash';
/**
* create a middleware to change json format from snake case to camelcase in request
* then change back to snake case in response
*
*/
export default (req: Request, res: Response, next: Function) => {
/**
* camelize `req.body`
*/
if (req.body && typeof req.body === 'object') {
req.body = mapKeys(req.body, (value: any, key: string) => camelCase(key));
}
/**
* camelize `req.query`
*/
if (req.query && typeof req.query === 'object') {
req.query = mapKeys(req.query, (value: any, key: string) => camelCase(key));
}
/**
* wrap `res.json()`
*/
const sendJson = res.json;
res.json = (data: any) => {
const mapped = mapKeys(data, (value: any, key: string) => snakeCase(key));
return sendJson.call(res, mapped);
};
return next();
};

View File

@@ -0,0 +1,28 @@
export interface IMetadata {
key: string,
value: string|boolean|number,
group: string,
_markAsDeleted?: boolean,
_markAsInserted?: boolean,
_markAsUpdated?: boolean,
};
export interface IMetaQuery {
key: string,
group: string,
};
export interface IMetableStore {
find(query: string|IMetaQuery): IMetadata;
all(): IMetadata[];
get(query: string|IMetaQuery, defaultValue: any): string|number|boolean;
remove(query: string|IMetaQuery): void;
removeAll(): void;
toArray(): IMetadata[];
};
export interface IMetableStoreStorage {
save(): Promise<void>;
}

View File

@@ -0,0 +1,11 @@
export interface IOptionDTO {
key: string,
value: string|number,
group: string,
};
export interface IOptionsDTO {
options: IOptionDTO[],
};

View File

@@ -6,4 +6,8 @@ export interface ISystemUser {
export interface ISystemUserDTO {
}
export interface IInviteUserInput {
}

View File

@@ -34,7 +34,18 @@ import {
import {
ISystemUser,
ISystemUserDTO,
IInviteUserInput,
} from './User';
import {
IMetadata,
IMetaQuery,
IMetableStore,
IMetableStoreStorage,
} from './Metable';
import {
IOptionDTO,
IOptionsDTO,
} from './Options';
export {
IBillPaymentEntry,
@@ -69,4 +80,13 @@ export {
IRegisterDTO,
ISystemUser,
ISystemUserDTO,
IInviteUserInput,
IMetadata,
IMetaQuery,
IMetableStore,
IMetableStoreStorage,
IOptionDTO,
IOptionsDTO,
};

View File

@@ -25,15 +25,15 @@ export default class ComputeItemCostJob {
const Logger = Container.get('logger');
const { startingDate, itemId, costMethod = 'FIFO' } = job.attrs.data;
Logger.debug(`Compute item cost - started: ${job.attrs.data}`);
Logger.info(`Compute item cost - started: ${job.attrs.data}`);
try {
await InventoryService.computeItemCost(startingDate, itemId, costMethod);
Logger.debug(`Compute item cost - completed: ${job.attrs.data}`);
Logger.info(`Compute item cost - completed: ${job.attrs.data}`);
done();
} catch(e) {
console.log(e);
Logger.error(`Compute item cost: ${job.attrs.data}, error: ${e}`);
Logger.info(`Compute item cost: ${job.attrs.data}, error: ${e}`);
done(e);
}
}
@@ -58,9 +58,8 @@ export default class ComputeItemCostJob {
async onJobFinished() {
const agenda = Container.get('agenda');
const startingDate = this.startingDate;
this.depends = Math.max(this.depends - 1, 0);
console.log(startingDate);
this.depends = Math.max(this.depends - 1, 0);
if (this.depends === 0) {
this.startingDate = null;

View File

@@ -3,7 +3,7 @@ import SubscriptionService from '@/services/Subscription/Subscription';
export default class MailNotificationSubscribeEnd {
/**
*
* Job handler.
* @param {Job} job -
*/
handler(job) {
@@ -12,15 +12,15 @@ export default class MailNotificationSubscribeEnd {
const subscriptionService = Container.get(SubscriptionService);
const Logger = Container.get('logger');
Logger.debug(`Send mail notification subscription end soon - started: ${job.attrs.data}`);
Logger.info(`Send mail notification subscription end soon - started: ${job.attrs.data}`);
try {
subscriptionService.mailMessages.sendRemainingTrialPeriod(
phoneNumber, remainingDays,
);
Logger.debug(`Send mail notification subscription end soon - finished: ${job.attrs.data}`);
Logger.info(`Send mail notification subscription end soon - finished: ${job.attrs.data}`);
} catch(error) {
Logger.error(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
Logger.info(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
done(e);
}
}

View File

@@ -12,15 +12,15 @@ export default class MailNotificationTrialEnd {
const subscriptionService = Container.get(SubscriptionService);
const Logger = Container.get('logger');
Logger.debug(`Send mail notification subscription end soon - started: ${job.attrs.data}`);
Logger.info(`Send mail notification subscription end soon - started: ${job.attrs.data}`);
try {
subscriptionService.mailMessages.sendRemainingTrialPeriod(
phoneNumber, remainingDays,
);
Logger.debug(`Send mail notification subscription end soon - finished: ${job.attrs.data}`);
Logger.info(`Send mail notification subscription end soon - finished: ${job.attrs.data}`);
} catch(error) {
Logger.error(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
Logger.info(`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
done(e);
}
}

View File

@@ -1,44 +1,28 @@
import fs from 'fs';
import path from 'path';
import Mustache from 'mustache';
import { Container } from 'typedi';
import { Container, Inject } from 'typedi';
import AuthenticationService from '@/services/Authentication';
export default class WelcomeEmailJob {
@Inject()
authService: AuthenticationService;
export default class ResetPasswordMailJob {
/**
*
* @param job
* @param done
* Handle send welcome mail job.
* @param {Job} job
* @param {Function} done
*/
handler(job, done) {
const { user, token } = job.attrs.data;
public async handler(job, done: Function): Promise<void> {
const { email, organizationName, firstName } = job.attrs.data;
const Logger = Container.get('logger');
const Mail = Container.get('mail');
const filePath = path.join(global.rootPath, 'views/mail/ResetPassword.html');
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, {
url: `https://google.com/reset/${token}`,
first_name: user.firstName,
last_name: user.lastName,
// contact_us_email: config.contactUsMail,
});
const mailOptions = {
to: user.email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
subject: 'Bigcapital - Password Reset',
html: rendered,
};
Mail.sendMail(mailOptions, (error) => {
if (error) {
Logger.info('[send_reset_password] failed send reset password mail', { error, user });
done(error);
return;
}
Logger.info('[send_reset_password] user has been sent reset password email successfuly.', { user });
done();
});
res.status(200).send({ email: passwordReset.email });
Logger.info(`Send reset password mail - started: ${job.attrs.data}`);
try {
await this.authService.mailMessages.sendResetPasswordMessage();
Logger.info(`Send reset password mail - finished: ${job.attrs.data}`);
done()
} catch (error) {
Logger.info(`Send reset password mail - error: ${job.attrs.data}, error: ${error}`);
done(error);
}
}
}
}

View File

@@ -13,15 +13,15 @@ export default class SMSNotificationSubscribeEnd {
const subscriptionService = Container.get(SubscriptionService);
const Logger = Container.get('logger');
Logger.debug(`Send SMS notification subscription end soon - started: ${job.attrs.data}`);
Logger.info(`Send SMS notification subscription end soon - started: ${job.attrs.data}`);
try {
subscriptionService.smsMessages.sendRemainingSubscriptionPeriod(
phoneNumber, remainingDays,
);
Logger.debug(`Send SMS notification subscription end soon - finished: ${job.attrs.data}`);
Logger.info(`Send SMS notification subscription end soon - finished: ${job.attrs.data}`);
} catch(error) {
Logger.error(`Send SMS notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
Logger.info(`Send SMS notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
done(e);
}
}

View File

@@ -13,15 +13,15 @@ export default class SMSNotificationTrialEnd {
const subscriptionService = Container.get(SubscriptionService);
const Logger = Container.get('logger');
Logger.debug(`Send notification subscription end soon - started: ${job.attrs.data}`);
Logger.info(`Send notification subscription end soon - started: ${job.attrs.data}`);
try {
subscriptionService.smsMessages.sendRemainingTrialPeriod(
phoneNumber, remainingDays,
);
Logger.debug(`Send notification subscription end soon - finished: ${job.attrs.data}`);
Logger.info(`Send notification subscription end soon - finished: ${job.attrs.data}`);
} catch(error) {
Logger.error(`Send notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
Logger.info(`Send notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
done(e);
}
}

View File

@@ -3,9 +3,10 @@ import VoucherService from '@/services/Payment/Voucher';
export default class SendVoucherViaPhoneJob {
public async handler(job, done: Function): Promise<void> {
const { phoneNumber, voucherCode } = job.attrs.data;
const Logger = Container.get('logger');
const voucherService = Container.get(VoucherService);
const { phoneNumber, voucherCode } = job.attrs.data;
Logger.debug(`Send voucher via phone number - started: ${job.attrs.data}`);

View File

@@ -1,8 +1,28 @@
import { Container, Inject } from 'typedi';
import InviteUserService from '@/services/InviteUsers';
export default class UserInviteMailJob {
@Inject()
inviteUsersService: InviteUserService;
handler(job, done) {
/**
* Handle invite user job.
* @param {Job} job
* @param {Function} done
*/
public async handler(job, done: Function): Promise<void> {
const { email, organizationName, firstName } = job.attrs.data;
const Logger = Container.get('logger');
Logger.info(`Send invite user mail - started: ${job.attrs.data}`);
try {
await this.inviteUsersService.mailMessages.sendInviteMail();
Logger.info(`Send invite user mail - finished: ${job.attrs.data}`);
done()
} catch (error) {
Logger.info(`Send invite user mail - error: ${job.attrs.data}, error: ${error}`);
done(error);
}
}
}
}

View File

@@ -0,0 +1,28 @@
import { Container, Inject } from 'typedi';
import AuthenticationService from '@/services/Authentication';
export default class WelcomeSMSJob {
@Inject()
authService: AuthenticationService;
/**
* Handle send welcome mail job.
* @param {Job} job
* @param {Function} done
*/
public async handler(job, done: Function): Promise<void> {
const { email, organizationName, firstName } = job.attrs.data;
const Logger = Container.get('logger');
Logger.info(`Send welcome SMS message - started: ${job.attrs.data}`);
try {
await this.authService.smsMessages.sendWelcomeMessage();
Logger.info(`Send welcome SMS message - finished: ${job.attrs.data}`);
done()
} catch (error) {
Logger.info(`Send welcome SMS message - error: ${job.attrs.data}, error: ${error}`);
done(error);
}
}
}

View File

@@ -1,38 +1,28 @@
import fs from 'fs';
import Mustache from 'mustache';
import path from 'path';
import { Container } from 'typedi';
import { Container, Inject } from 'typedi';
import AuthenticationService from '@/services/Authentication';
export default class WelcomeEmailJob {
@Inject()
authService: AuthenticationService;
/**
*
* Handle send welcome mail job.
* @param {Job} job
* @param {Function} done
*/
public async handler(job, done: Function): Promise<void> {
const { email, organizationName, firstName } = job.attrs.data;
const Logger = Container.get('logger');
const Mail = Container.get('mail');
const filePath = path.join(global.rootPath, 'views/mail/Welcome.html');
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, {
email, organizationName, firstName,
});
const mailOptions = {
to: email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
subject: 'Welcome to Bigcapital',
html: rendered,
};
Mail.sendMail(mailOptions, (error) => {
if (error) {
Logger.error('Failed send welcome mail', { error, form });
done(error);
return;
}
Logger.info('User has been sent welcome email successfuly.', { form });
done();
});
Logger.info(`Send welcome mail message - started: ${job.attrs.data}`);
try {
await this.authService.mailMessages.sendWelcomeMessage();
Logger.info(`Send welcome mail message - finished: ${job.attrs.data}`);
done()
} catch (error) {
Logger.info(`Send welcome mail message - error: ${job.attrs.data}, error: ${error}`);
done(error);
}
}
}

View File

@@ -7,15 +7,15 @@ export default class WriteInvoicesJournalEntries {
const Logger = Container.get('logger');
const { startingDate } = job.attrs.data;
Logger.debug(`Write sales invoices journal entries - started: ${job.attrs.data}`);
Logger.info(`Write sales invoices journal entries - started: ${job.attrs.data}`);
try {
await SalesInvoicesCost.writeJournalEntries(startingDate, true);
Logger.debug(`Write sales invoices journal entries - completed: ${job.attrs.data}`);
Logger.info(`Write sales invoices journal entries - completed: ${job.attrs.data}`);
done();
} catch(e) {
console.log(e);
Logger.error(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`);
Logger.info(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`);
done(e);
}
}

View File

@@ -1,266 +0,0 @@
export default class MetableCollection {
/**
* Constructor method.
*/
constructor() {
this.metadata = [];
this.KEY_COLUMN = 'key';
this.VALUE_COLUMN = 'value';
this.TYPE_COLUMN = 'type';
this.model = null;
this.extraColumns = [];
this.extraQuery = (query, meta) => {
query.where('key', meta[this.KEY_COLUMN]);
};
}
/**
* Set model of this metadata collection.
* @param {Object} model -
*/
setModel(model) {
this.model = model;
}
/**
* Sets a extra columns.
* @param {Array} columns -
*/
setExtraColumns(columns) {
this.extraColumns = columns;
}
/**
* Find the given metadata key.
* @param {String} key -
* @return {object} - Metadata object.
*/
findMeta(payload) {
const { key, extraColumns } = this.parsePayload(payload);
return this.allMetadata().find((meta) => {
const isSameKey = meta.key === key;
const sameExtraColumns = this.extraColumns.some((extraColumn) => {
return !extraColumns || (extraColumns[extraColumn] === meta[extraColumn]);
});
return isSameKey && sameExtraColumns;
});
}
/**
* Retrieve all metadata.
*/
allMetadata() {
return this.metadata.filter((meta) => !meta.markAsDeleted);
}
/**
* Retrieve metadata of the given key.
* @param {String} key -
* @param {Mixied} defaultValue -
*/
getMeta(payload, defaultValue) {
const metadata = this.findMeta(payload);
return metadata ? metadata.value : defaultValue || false;
}
/**
* Markes the metadata to should be deleted.
* @param {String} key -
*/
removeMeta(key) {
const metadata = this.findMeta(key);
if (metadata) {
metadata.markAsDeleted = true;
}
}
/**
* Remove all meta data of the given group.
* @param {*} group
*/
removeAllMeta(group = 'default') {
this.metadata = this.metadata.map((meta) => ({
...meta,
markAsDeleted: true,
}));
}
setExtraQuery(callback) {
this.extraQuery = callback;
}
/**
* Set the meta data to the stack.
* @param {String} key -
* @param {String} value -
*/
setMeta(payload, ...args) {
if (Array.isArray(key)) {
const metadata = key;
metadata.forEach((meta) => {
this.setMeta(meta.key, meta.value);
});
return;
}
const { key, value, ...extraColumns } = this.parsePayload(payload, args[0]);
const metadata = this.findMeta(payload);
if (metadata) {
metadata.value = value;
metadata.markAsUpdated = true;
} else {
this.metadata.push({
value, key, ...extraColumns, markAsInserted: true,
});
}
}
parsePayload(payload, value) {
return typeof payload !== 'object' ? { key: payload, value } : { ...payload };
}
/**
* Saved the modified/deleted and inserted metadata.
*/
async saveMeta() {
const inserted = this.metadata.filter((m) => (m.markAsInserted === true));
const updated = this.metadata.filter((m) => (m.markAsUpdated === true));
const deleted = this.metadata.filter((m) => (m.markAsDeleted === true));
const opers = [];
if (deleted.length > 0) {
deleted.forEach((meta) => {
const deleteOper = this.model.query().onBuild((query, result) => {
this.extraQuery(query, meta);
return result;
}).delete();
opers.push(deleteOper);
});
}
inserted.forEach((meta) => {
const insertOper = this.model.query().insert({
[this.KEY_COLUMN]: meta.key,
[this.VALUE_COLUMN]: meta.value,
...this.extraColumns.reduce((obj, column) => {
if (typeof meta[column] !== 'undefined') {
obj[column] = meta[column];
}
return obj;
}, {}),
});
opers.push(insertOper);
});
updated.forEach((meta) => {
const updateOper = this.model.query().onBuild((query) => {
this.extraQuery(query, meta);
}).patch({
[this.VALUE_COLUMN]: meta.value,
});
opers.push(updateOper);
});
await Promise.all(opers);
}
/**
* Loads the metadata from the storage.
* @param {String|Array} key -
* @param {Boolean} force -
*/
async load() {
const metadata = await this.query();
const metadataArray = this.mapMetadataCollection(metadata);
metadataArray.forEach((meta) => {
this.metadata.push(meta);
});
}
/**
* Format the metadata before saving to the database.
* @param {String|Number|Boolean} value -
* @param {String} valueType -
* @return {String|Number|Boolean} -
*/
static formatMetaValue(value, valueType) {
let parsedValue;
switch (valueType) {
case 'number':
parsedValue = `${value}`;
break;
case 'boolean':
parsedValue = value ? '1' : '0';
break;
case 'json':
parsedValue = JSON.stringify(parsedValue);
break;
default:
parsedValue = value;
break;
}
return parsedValue;
}
/**
* Mapping and parse metadata to collection entries.
* @param {Meta} attr -
* @param {String} parseType -
*/
mapMetadata(attr, parseType = 'parse') {
return {
key: attr[this.KEY_COLUMN],
value: (parseType === 'parse')
? MetableCollection.parseMetaValue(
attr[this.VALUE_COLUMN],
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
)
: MetableCollection.formatMetaValue(
attr[this.VALUE_COLUMN],
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
),
...this.extraColumns.map((extraCol) => ({
[extraCol]: attr[extraCol] || null,
})),
};
}
/**
* Parse the metadata to the collection.
* @param {Array} collection -
*/
mapMetadataToCollection(metadata, parseType = 'parse') {
return metadata.map((model) => this.mapMetadataToCollection(model, parseType));
}
/**
* Load metadata to the metable collection.
* @param {Array} meta -
*/
from(meta) {
if (Array.isArray(meta)) {
meta.forEach((m) => { this.from(m); });
return;
}
this.metadata.push(meta);
}
toArray() {
return this.metadata;
}
/**
* Static method to load metadata to the collection.
* @param {Array} meta
*/
static from(meta) {
const collection = new MetableCollection();
collection.from(meta);
return collection;
}
}

View File

@@ -0,0 +1,204 @@
import { Model } from 'objection';
import { omit, isEmpty } from 'lodash';
import {
IMetadata,
IMetaQuery,
IMetableStore,
} from '@/interfaces';
import { itemsStartWith } from '@/utils';
export default class MetableStore implements IMetableStore{
metadata: IMetadata[];
model: Model;
extraColumns: string[];
/**
* Constructor method.
*/
constructor() {
this.metadata = [];
this.model = null;
this.extraColumns = [];
}
/**
* Sets a extra columns.
* @param {Array} columns -
*/
setExtraColumns(columns: string[]): void {
this.extraColumns = columns;
}
/**
* Find the given metadata key.
* @param {string|IMetaQuery} query -
* @returns {IMetadata} - Metadata object.
*/
find(query: string|IMetaQuery): IMetadata {
const { key, value, ...extraColumns } = this.parseQuery(query);
return this.metadata.find((meta: IMetadata) => {
const isSameKey = meta.key === key;
const sameExtraColumns = this.extraColumns
.some((extraColumn: string) => extraColumns[extraColumn] === meta[extraColumn]);
const isSameExtraColumns = (sameExtraColumns || isEmpty(extraColumns));
return isSameKey && isSameExtraColumns;
});
}
/**
* Retrieve all metadata.
* @returns {IMetadata[]}
*/
all(): IMetadata[] {
return this.metadata
.filter((meta: IMetadata) => !meta._markAsDeleted)
.map((meta: IMetadata) => omit(
meta,
itemsStartWith(Object.keys(meta), '_')
));
}
/**
* Retrieve metadata of the given key.
* @param {String} key -
* @param {Mixied} defaultValue -
*/
get(query: string|IMetaQuery, defaultValue: any): any|false {
const metadata = this.find(query);
return metadata ? metadata.value : defaultValue || false;
}
/**
* Markes the metadata to should be deleted.
* @param {String} key -
*/
remove(query: string|IMetaQuery): void {
const metadata: IMetadata = this.find(query);
if (metadata) {
metadata._markAsDeleted = true;
}
}
/**
* Remove all meta data of the given group.
* @param {string} group
*/
removeAll(group: string = 'default'): void {
this.metadata = this.metadata.map((meta) => ({
...meta,
_markAsDeleted: true,
}));
}
/**
* Set the meta data to the stack.
* @param {String} key -
* @param {String} value -
*/
set(query: IMetaQuery|IMetadata[]|string, metaValue?: any): void {
if (Array.isArray(query)) {
const metadata = query;
metadata.forEach((meta: IMetadata) => {
this.set(meta.key, meta.value);
});
return;
}
const { key, value, ...extraColumns } = this.parseQuery(query);
const metadata = this.find(query);
const newValue = metaValue || value;
if (metadata) {
metadata.value = newValue;
metadata._markAsUpdated = true;
} else {
this.metadata.push({
value: newValue,
key,
...extraColumns,
_markAsInserted: true,
});
}
}
/**
* Parses query query.
* @param query
* @param value
*/
parseQuery(query: string|IMetaQuery): IMetaQuery {
return typeof query !== 'object' ? { key: query } : { ...query };
}
/**
* Format the metadata before saving to the database.
* @param {string|number|boolean} value -
* @param {string} valueType -
* @return {string|number|boolean} -
*/
static formatMetaValue(
value: string|boolean|number,
valueType: string
) : string|number|boolean {
let parsedValue;
switch (valueType) {
case 'number':
parsedValue = `${value}`;
break;
case 'boolean':
parsedValue = value ? '1' : '0';
break;
case 'json':
parsedValue = JSON.stringify(parsedValue);
break;
default:
parsedValue = value;
break;
}
return parsedValue;
}
/**
* Parse the metadata to the collection.
* @param {Array} collection -
*/
mapMetadataToCollection(metadata: IMetadata[], parseType: string = 'parse') {
return metadata.map((model) => this.mapMetadataToCollection(model, parseType));
}
/**
* Load metadata to the metable collection.
* @param {Array} meta -
*/
from(meta: []) {
if (Array.isArray(meta)) {
meta.forEach((m) => { this.from(m); });
return;
}
this.metadata.push(meta);
}
/**
*
* @returns {array}
*/
toArray(): IMetadata[] {
return this.metadata;
}
/**
* Static method to load metadata to the collection.
* @param {Array} meta
*/
static from(meta) {
const collection = new MetableCollection();
collection.from(meta);
return collection;
}
}

View File

@@ -0,0 +1,213 @@
import { Model } from 'objection';
import {
IMetadata,
IMetableStoreStorage,
} from '@/interfaces';
import MetableStore from './MetableStore';
export default class MetableDBStore extends MetableStore implements IMetableStoreStorage{
model: Model;
KEY_COLUMN: string;
VALUE_COLUMN: string;
TYPE_COLUMN: string;
extraQuery: Function;
loaded: Boolean;
/**
* Constructor method.
*/
constructor() {
super();
this.loaded = false;
this.KEY_COLUMN = 'key';
this.VALUE_COLUMN = 'value';
this.TYPE_COLUMN = 'type';
this.model = null;
this.extraQuery = (query, meta) => {
query.where('key', meta[this.KEY_COLUMN]);
};
}
/**
* Set model of this metadata collection.
* @param {Object} model -
*/
setModel(model: Model) {
this.model = model;
}
/**
* Sets a extra query callback.
* @param callback
*/
setExtraQuery(callback) {
this.extraQuery = callback;
}
/**
* Saves the modified, deleted and insert metadata.
*/
save() {
this.validateStoreIsLoaded();
return Promise.all([
this.saveUpdated(this.metadata),
this.saveDeleted(this.metadata),
this.saveInserted(this.metadata),
]);
}
/**
* Saves the updated metadata.
* @param {IMetadata[]} metadata -
* @returns {Promise}
*/
saveUpdated(metadata: IMetadata[]) {
const updated = metadata.filter((m) => (m._markAsUpdated === true));
const opers = [];
updated.forEach((meta) => {
const updateOper = this.model.query().onBuild((query) => {
this.extraQuery(query, meta);
}).patch({
[this.VALUE_COLUMN]: meta.value,
}).then(() => {
meta._markAsUpdated = false;
});
opers.push(updateOper);
});
return Promise.all(opers);
}
/**
* Saves the deleted metadata.
* @param {IMetadata[]} metadata -
* @returns {Promise}
*/
saveDeleted(metadata: IMetadata[]) {
const deleted = metadata.filter((m: IMetadata) => (m._markAsDeleted === true));
const opers: Promise<void> = [];
if (deleted.length > 0) {
deleted.forEach((meta) => {
const deleteOper = this.model.query().onBuild((query) => {
this.extraQuery(query, meta);
}).delete().then(() => {
meta._markAsDeleted = false;
});
opers.push(deleteOper);
});
}
return Promise.all(opers);
}
/**
* Saves the inserted metadata.
* @param {IMetadata[]} metadata -
* @returns {Promise}
*/
saveInserted(metadata: IMetadata[]) {
const inserted = metadata.filter((m: IMetadata) => (m._markAsInserted === true));
const opers: Promise<void> = [];
inserted.forEach((meta) => {
const insertData = {
[this.KEY_COLUMN]: meta.key,
[this.VALUE_COLUMN]: meta.value,
...this.extraColumns.reduce((obj, column) => {
if (typeof meta[column] !== 'undefined') {
obj[column] = meta[column];
}
return obj;
}, {}),
};
const insertOper = this.model.query()
.insert(insertData)
.then(() => {
meta._markAsInserted = false;
});
opers.push(insertOper);
});
return Promise.all(opers);
}
/**
* Loads the metadata from the storage.
* @param {String|Array} key -
* @param {Boolean} force -
*/
async load() {
const metadata = await this.model.query();
const mappedMetadata = this.mapMetadataCollection(metadata);
mappedMetadata.forEach((meta: IMetadata) => {
this.metadata.push(meta);
});
this.loaded = true;
}
/**
* Format the metadata before saving to the database.
* @param {String|Number|Boolean} value -
* @param {String} valueType -
* @return {String|Number|Boolean} -
*/
static formatMetaValue(value: string|number|boolean, valueType: striung|false) {
let parsedValue: string|number|boolean;
switch (valueType) {
case 'number':
parsedValue = `${value}`;
break;
case 'boolean':
parsedValue = value ? '1' : '0';
break;
case 'json':
parsedValue = JSON.stringify(parsedValue);
break;
default:
parsedValue = value;
break;
}
return parsedValue;
}
/**
* Mapping and parse metadata to collection entries.
* @param {Meta} attr -
* @param {String} parseType -
*/
mapMetadata(metadata: IMetadata) {
return {
key: metadata[this.KEY_COLUMN],
value: MetableDBStore.formatMetaValue(
metadata[this.VALUE_COLUMN],
this.TYPE_COLUMN ? metadata[this.TYPE_COLUMN] : false,
),
...this.extraColumns.reduce((obj, extraCol: string) => {
obj[extraCol] = metadata[extraCol] || null;
return obj;
}, {}),
};
}
/**
* Parse the metadata to the collection.
* @param {Array} collection -
*/
mapMetadataCollection(metadata: IMetadata[]) {
return metadata.map((model) => this.mapMetadata(model));
}
/**
* Throw error in case the store is not loaded yet.
*/
private validateStoreIsLoaded() {
if (!this.loaded) {
throw new Error('You could not save the store before loaded from the storage.');
}
}
}

View File

@@ -0,0 +1,14 @@
import knexManager from 'knex-db-manager';
import knexfile from '@/../config/systemKnexfile';
import config from '@/../config/config';
const knexConfig = knexfile[process.env.NODE_ENV];
export default () => knexManager.databaseManagerFactory({
knex: knexConfig,
dbManager: {
collate: [],
superUser: config.manager.superUser,
superPassword: config.manager.superPassword,
},
});

View File

@@ -1,13 +1,15 @@
import { Container } from 'typedi';
import LoggerInstance from '@/services/Logger';
import LoggerInstance from '@/loaders/Logger';
import agendaFactory from '@/loaders/agenda';
import SmsClientLoader from '@/loaders/smsClient';
import mailInstance from '@/loaders/mail';
import dbManagerFactory from '@/loaders/dbManager';
export default ({ mongoConnection, knex }) => {
try {
const agendaInstance = agendaFactory({ mongoConnection });
const smsClientInstance = SmsClientLoader();
const dbManager = dbManagerFactory();
Container.set('agenda', agendaInstance);
LoggerInstance.info('Agenda has been injected into container');
@@ -24,6 +26,9 @@ export default ({ mongoConnection, knex }) => {
Container.set('mail', mailInstance);
LoggerInstance.info('Mail instance has been injected into container');
Container.set('dbManager', dbManager);
LoggerInstance.info('Database manager has been injected into container.');
return { agenda: agendaInstance };
} catch (e) {
LoggerInstance.error('Error on dependency injector loader: %o', e);

View File

@@ -1,4 +1,4 @@
import Logger from '@/services/Logger';
import Logger from '@/loaders/Logger';
import mongooseLoader from '@/loaders/mongoose';
import jobsLoader from '@/loaders/jobs';
import expressLoader from '@/loaders/express';

View File

@@ -1,7 +1,8 @@
import Agenda from 'agenda';
import WelcomeEmailJob from '@/Jobs/welcomeEmail';
import ResetPasswordMailJob from '@/Jobs/ResetPasswordMail';
import ComputeItemCost from '@/Jobs/ComputeItemCost';
import WelcomeEmailJob from '@/jobs/WelcomeEmail';
import WelcomeSMSJob from '@/jobs/WelcomeSMS';
import ResetPasswordMailJob from '@/jobs/ResetPasswordMail';
import ComputeItemCost from '@/jobs/ComputeItemCost';
import RewriteInvoicesJournalEntries from '@/jobs/writeInvoicesJEntries';
import SendVoucherViaPhoneJob from '@/jobs/SendVoucherPhone';
import SendVoucherViaEmailJob from '@/jobs/SendVoucherEmail';
@@ -12,16 +13,24 @@ import SendMailNotificationTrialEnd from '@/jobs/MailNotificationTrialEnd';
import UserInviteMailJob from '@/jobs/UserInviteMail';
export default ({ agenda }: { agenda: Agenda }) => {
// Welcome mail and SMS message.
agenda.define(
'welcome-email',
{ priority: 'high' },
new WelcomeEmailJob().handler,
);
agenda.define(
'welcome-sms',
{ priority: 'high' },
new WelcomeSMSJob().handler
);
// Reset password mail.
agenda.define(
'reset-password-mail',
{ priority: 'high' },
new ResetPasswordMailJob().handler,
);
// User invite mail.
agenda.define(
'user-invite-mail',
{ priority: 'high' },

View File

@@ -0,0 +1,70 @@
import { mapValues } from 'lodash';
import Account from '@/models/Account';
import AccountBalance from '@/models/AccountBalance';
import AccountTransaction from '@/models/AccountTransaction';
import AccountType from '@/models/AccountType';
import Bill from '@/models/Bill';
import BillPayment from '@/models/BillPayment';
import BillPaymentEntry from '@/models/BillPaymentEntry';
import Currency from '@/models/Currency';
import Customer from '@/models/Customer';
import Vendor from '@/models/Vendor';
import ExchangeRate from '@/models/ExchangeRate';
import Expense from '@/models/Expense';
import ExpenseCategory from '@/models/ExpenseCategory';
import View from '@/models/View';
import ViewRole from '@/models/ViewRole';
import ViewColumn from '@/models/ViewColumn';
import Setting from '@/models/Setting';
import SaleInvoice from '@/models/SaleInvoice';
import SaleInvoiceEntry from '@/models/SaleInvoiceEntry';
import SaleReceipt from '@/models/SaleReceipt';
import SaleReceiptEntry from '@/models/SaleReceiptEntry';
import SaleEstimate from '@/models/SaleEstimate';
import SaleEstimateEntry from '@/models/SaleEstimateEntry';
import PaymentReceive from '@/models/PaymentReceive';
import PaymentReceiveEntry from '@/models/PaymentReceiveEntry';
import Option from '@/models/Option';
import Resource from '@/models/Resource';
import InventoryCostLotTracker from '@/models/InventoryCostLotTracker';
import InventoryTransaction from '@/models/InventoryTransaction';
import ResourceField from '@/models/ResourceField';
import ResourceFieldMetadata from '@/models/ResourceFieldMetadata';
export default (knex) => {
const models = {
Option,
Account,
AccountBalance,
AccountTransaction,
AccountType,
Bill,
BillPayment,
BillPaymentEntry,
Currency,
Customer,
Vendor,
ExchangeRate,
Expense,
ExpenseCategory,
View,
ViewRole,
ViewColumn,
Setting,
SaleInvoice,
SaleInvoiceEntry,
SaleReceipt,
SaleReceiptEntry,
SaleEstimate,
SaleEstimateEntry,
PaymentReceive,
PaymentReceiveEntry,
Resource,
InventoryTransaction,
InventoryCostLotTracker,
ResourceField,
ResourceFieldMetadata,
};
return mapValues(models, (model) => model.bindKnex(knex));
}

View File

@@ -1,60 +0,0 @@
import TenantModel from '@/models/Model';
export default class Budget extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'budgets';
}
static get virtualAttributes() {
return ['rangeBy', 'rangeIncrement'];
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
filterByYear(query, year) {
query.where('year', year);
},
filterByIncomeStatement(query) {
query.where('account_types', 'income_statement');
},
filterByProfitLoss(query) {
query.where('accounts_types', 'profit_loss');
},
};
}
get rangeBy() {
switch (this.period) {
case 'half-year':
case 'quarter':
return 'month';
default:
return this.period;
}
}
get rangeIncrement() {
switch (this.period) {
case 'half-year':
return 6;
case 'quarter':
return 3;
default:
return 1;
}
}
get rangeOffset() {
switch (this.period) {
case 'half-year': return 5;
case 'quarter': return 2;
default: return 0;
}
}
}

View File

@@ -1,10 +0,0 @@
import TenantModel from '@/models/TenantModel';
export default class Budget extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'budget_entries';
}
}

View File

@@ -1,5 +1,4 @@
import TenantModel from '@/models/TenantModel';
import MetableCollection from '@/lib/Metable/MetableCollection';
import definedOptions from '@/data/options';
@@ -11,27 +10,6 @@ export default class Option extends TenantModel {
return 'options';
}
/**
* Override the model query.
* @param {...any} args -
*/
static query(...args) {
return super.query(...args).runAfter((result) => {
if (result instanceof MetableCollection) {
result.setModel(this.tenant());
result.setExtraColumns(['group']);
}
return result;
});
}
/**
* Model collection.
*/
static get collection() {
return MetableCollection;
}
/**
* Validates the given options is defined or either not.
* @param {Array} options

View File

@@ -1,43 +0,0 @@
import { Model } from 'objection';
import path from 'path';
import TenantModel from '@/models/TenantModel';
export default class Permission extends TenantModel {
/**
* Table name of Role model.
* @type {String}
*/
static get tableName() {
return 'permissions';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Role = require('@/models/Role');
return {
/**
* Permission model may belongs to role model.
*/
// role: {
// relation: Model.BelongsToOneRelation,
// modelBase: path.join(__dirname, 'Role').bindKnex(this.knexBinded),
// join: {
// from: 'permissions.role_id',
// to: 'roles.id',
// },
// },
// resource: {
// relation: Model.BelongsToOneRelation,
// modelBase: path.join(__dirname, 'Resource'),
// join: {
// from: 'permissions.',
// to: '',
// }
// }
};
}
}

View File

@@ -31,7 +31,6 @@ export default class Resource extends mixin(TenantModel, [CachableModel]) {
static get relationMappings() {
const View = require('@/models/View');
const ResourceField = require('@/models/ResourceField');
const Permission = require('@/models/Permission');
return {
/**
@@ -57,22 +56,6 @@ export default class Resource extends mixin(TenantModel, [CachableModel]) {
to: 'resource_fields.resourceId',
},
},
/**
* Resource model may has many associated permissions.
*/
permissions: {
relation: Model.ManyToManyRelation,
modelClass: this.relationBindKnex(Permission.default),
join: {
from: 'resources.id',
through: {
from: 'role_has_permissions.resourceId',
to: 'role_has_permissions.permissionId',
},
to: 'permissions.id',
},
},
};
}
}

View File

@@ -1,5 +1,4 @@
import TenantModel from '@/models/TenantModel';
import ResourceFieldMetadataCollection from '@/collection/ResourceFieldMetadataCollection';
export default class ResourceFieldMetadata extends TenantModel {
/**
@@ -8,11 +7,4 @@ export default class ResourceFieldMetadata extends TenantModel {
static get tableName() {
return 'resource_custom_fields_metadata';
}
/**
* Override the resource field metadata collection.
*/
static get collection() {
return ResourceFieldMetadataCollection;
}
}

View File

@@ -1,91 +0,0 @@
import { Model } from 'objection';
import TenantModel from '@/models/TenantModel';
export default class Role extends TenantModel {
/**
* Table name of Role model.
* @type {String}
*/
static get tableName() {
return 'roles';
}
/**
* Timestamp columns.
*/
static get hasTimestamps() {
return false;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Permission = require('@/models/Permission');
const Resource = require('@/models/Resource');
const User = require('@/models/TenantUser');
const ResourceField = require('@/models/ResourceField');
return {
/**
* Role may has many permissions.
*/
permissions: {
relation: Model.ManyToManyRelation,
modelClass: Permission.default.bindKnex(this.knexBinded),
join: {
from: 'roles.id',
through: {
from: 'role_has_permissions.roleId',
to: 'role_has_permissions.permissionId',
},
to: 'permissions.id',
},
},
/**
* Role may has many resources.
*/
resources: {
relation: Model.ManyToManyRelation,
modelClass: Resource.default.bindKnex(this.knexBinded),
join: {
from: 'roles.id',
through: {
from: 'role_has_permissions.roleId',
to: 'role_has_permissions.resourceId',
},
to: 'resources.id',
},
},
/**
* Role may has resource field.
*/
field: {
relation: Model.BelongsToOneRelation,
modelClass: ResourceField.default.bindKnex(this.knexBinded),
join: {
from: 'roles.fieldId',
to: 'resource_fields.id',
},
},
/**
* Role may has many associated users.
*/
users: {
relation: Model.ManyToManyRelation,
modelClass: User.default.bindKnex(this.knexBinded),
join: {
from: 'roles.id',
through: {
from: 'user_has_roles.roleId',
to: 'user_has_roles.userId',
},
to: 'users.id',
},
},
};
}
}

View File

@@ -1,7 +1,5 @@
import bcrypt from 'bcryptjs';
import { Model } from 'objection';
import TenantModel from '@/models/TenantModel';
// import PermissionsService from '@/services/PermissionsService';
export default class TenantUser extends TenantModel {
/**
@@ -24,29 +22,7 @@ export default class TenantUser extends TenantModel {
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Role = require('@/models/Role');
return {
roles: {
relation: Model.ManyToManyRelation,
modelClass: this.relationBindKnex(Role.default),
join: {
from: 'users.id',
through: {
from: 'user_has_roles.userId',
to: 'user_has_roles.roleId',
},
to: 'roles.id',
},
},
};
}
/**
* Verify the password of the user.
* @param {String} password - The given password.

View File

@@ -0,0 +1,35 @@
import { Service } from "typedi";
@Service()
export default class AuthenticationMailMesssages {
sendWelcomeMessage() {
const Logger = Container.get('logger');
const Mail = Container.get('mail');
const filePath = path.join(global.rootPath, 'views/mail/Welcome.html');
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, {
email, organizationName, firstName,
});
const mailOptions = {
to: email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
subject: 'Welcome to Bigcapital',
html: rendered,
};
Mail.sendMail(mailOptions, (error) => {
if (error) {
Logger.error('Failed send welcome mail', { error, form });
done(error);
return;
}
Logger.info('User has been sent welcome email successfuly.', { form });
done();
});
}
sendResetPasswordMessage() {
}
}

View File

@@ -0,0 +1,13 @@
import { Service } from "typedi";
@Service()
export default class AuthenticationSMSMessages {
smsClient: any;
sendWelcomeMessage() {
const message: string = `Hi ${firstName}, Welcome to Bigcapital, You've joined the new workspace,
if you need any help please don't hesitate to contact us.`
}
}

View File

@@ -3,8 +3,8 @@ import JWT from 'jsonwebtoken';
import uniqid from 'uniqid';
import { omit } from 'lodash';
import {
EventDispatcher
EventDispatcherInterface
EventDispatcher,
EventDispatcherInterface,
} from '@/decorators/eventDispatcher';
import {
SystemUser,
@@ -22,6 +22,8 @@ import { hashPassword } from '@/utils';
import { ServiceError, ServiceErrors } from "@/exceptions";
import config from '@/../config/config';
import events from '@/subscribers/events';
import AuthenticationMailMessages from '@/services/Authentication/AuthenticationMailMessages';
import AuthenticationSMSMessages from '@/services/Authentication/AuthenticationSMSMessages';
@Service()
export default class AuthenticationService {
@@ -34,6 +36,12 @@ export default class AuthenticationService {
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject()
smsMessages: AuthenticationSMSMessages;
@Inject()
mailMessages: AuthenticationMailMessages;
/**
* Signin and generates JWT token.
* @throws {ServiceError}
@@ -70,6 +78,7 @@ export default class AuthenticationService {
this.logger.info('[login] Logging success.', { user, token });
// Triggers `onLogin` event.
this.eventDispatcher.dispatch(events.auth.login, {
emailOrPhone, password,
});
@@ -191,6 +200,7 @@ export default class AuthenticationService {
const passwordReset = await PasswordReset.query().insert({ email, token });
const user = await SystemUser.query().findOne('email', email);
// Triggers `onSendResetPassword` event.
this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token });
return passwordReset;
@@ -225,25 +235,26 @@ export default class AuthenticationService {
// Delete the reset password token.
await PasswordReset.query().where('email', user.email).delete();
this.eventDispatcher.dispatch(events.auth.sendResetPassword, { user, token, password });
// Triggers `onResetPassword` event.
this.eventDispatcher.dispatch(events.auth.resetPassword, { user, token, password });
this.logger.info('[reset_password] reset password success.');
}
/**
* Generates JWT token for the given user.
* @param {IUser} user
* @param {ISystemUser} user
* @return {string} token
*/
generateToken(user: IUser): string {
generateToken(user: ISystemUser): string {
const today = new Date();
const exp = new Date(today);
exp.setDate(today.getDate() + 60);
this.logger.silly(`Sign JWT for userId: ${user._id}`);
this.logger.silly(`Sign JWT for userId: ${user.id}`);
return JWT.sign(
{
_id: user._id, // We are gonna use this in the middleware 'isAuth'
id: user.id, // We are gonna use this in the middleware 'isAuth'
exp: exp.getTime() / 1000,
},
config.jwtSecret,

View File

@@ -0,0 +1,31 @@
import { Service } from "typedi";
@Service()
export default class InviteUsersMailMessages {
sendInviteMail() {
const filePath = path.join(global.rootPath, 'views/mail/UserInvite.html');
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, {
acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`,
fullName: `${user.firstName} ${user.lastName}`,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
organizationName: organizationOptions.getMeta('organization_name'),
});
const mailOptions = {
to: user.email,
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
subject: `${user.fullName} has invited you to join a Bigcapital`,
html: rendered,
};
mail.sendMail(mailOptions, (error) => {
if (error) {
Logger.log('error', 'Failed send user invite mail', { error, form });
}
Logger.log('info', 'User has been sent invite user email successfuly.', { form });
});
}
}

View File

@@ -0,0 +1,172 @@
import { Service, Inject } from "typedi";
import uniqid from 'uniqid';
import {
EventDispatcher,
EventDispatcherInterface,
} from '@/decorators/eventDispatcher';
import { ServiceError, ServiceErrors } from "@/exceptions";
import { SystemUser, Invite } from "@/system/models";
import { hashPassword } from '@/utils';
import TenancyService from '@/services/Tenancy/TenancyService';
import TenantsManager from "@/system/TenantsManager";
import InviteUsersMailMessages from "@/services/InviteUsers/InviteUsersMailMessages";
import events from '@/subscribers/events';
import {
ISystemUser,
IInviteUserInput,
} from '@/interfaces';
@Service()
export default class InviteUserService {
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject()
tenancy: TenancyService;
@Inject()
tenantsManager: TenantsManager;
@Inject('logger')
logger: any;
@Inject()
mailMessages: InviteUsersMailMessages;
/**
* Accept the received invite.
* @param {string} token
* @param {IInviteUserInput} inviteUserInput
* @throws {ServiceErrors}
* @returns {Promise<void>}
*/
async acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise<void> {
const inviteToken = await this.getInviteOrThrowError(token);
await this.validateUserEmailAndPhone(inviteUserInput);
this.logger.info('[aceept_invite] trying to hash the user password.');
const hashedPassword = await hashPassword(inviteUserInput.password);
const user = SystemUser.query()
.where('email', inviteUserInput.email)
.patch({
...inviteUserInput,
active: 1,
email: inviteToken.email,
invite_accepted_at: moment().format('YYYY/MM/DD'),
password: hashedPassword,
tenant_id: inviteToken.tenantId,
});
const deleteInviteTokenOper = Invite.query().where('token', inviteToken.token).delete();
await Promise.all([
insertUserOper,
deleteInviteTokenOper,
]);
// Triggers `onUserAcceptInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
inviteToken, user,
});
}
/**
* Sends invite mail to the given email from the given tenant and user.
* @param {number} tenantId -
* @param {string} email -
* @param {IUser} authorizedUser -
*
* @return {Promise<IInvite>}
*/
public async sendInvite(tenantId: number, email: string, authorizedUser: ISystemUser): Promise<IInvite> {
const { Option } = this.tenancy.models(tenantId);
await this.throwErrorIfUserEmailExists(email);
const invite = await Invite.query().insert({
email,
tenant_id: authorizedUser.tenantId,
token: uniqid(),
});
// Triggers `onUserSendInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.sendInvite, {
invite,
});
return { invite };
}
/**
* Validate the given invite token.
* @param {string} token - the given token string.
* @throws {ServiceError}
*/
public async checkInvite(token: string) {
const inviteToken = await this.getInviteOrThrowError(token)
// Find the tenant that associated to the given token.
const tenant = await Tenant.query().findOne('id', inviteToken.tenantId);
const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId);
const organizationOptions = await Option.bindKnex(tenantDb).query()
.where('key', 'organization_name');
// Triggers `onUserCheckInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.checkInvite, {
inviteToken, organizationOptions,
});
return { inviteToken, organizationOptions };
}
private async throwErrorIfUserEmailExists(email: string) {
const foundUser = await SystemUser.query().findOne('email', email);
if (foundUser) {
throw new ServiceError('email_already_invited');
}
}
/**
* Retrieve invite model from the given token or throw error.
* @param {string} token - Then given token string.
* @throws {ServiceError}
*/
private async getInviteOrThrowError(token: string) {
const inviteToken = await Invite.query().findOne('token', token);
if (!inviteToken) {
this.logger.info('[aceept_invite] the invite token is invalid.');
throw new ServiceError('invite_token_invalid');
}
}
/**
* Validate the given user email and phone number uniquine.
* @param {IInviteUserInput} inviteUserInput
*/
private async validateUserEmailAndPhone(inviteUserInput: IInviteUserInput) {
const foundUser = await SystemUser.query()
.onBuild(query => {
query.where('email', inviteUserInput.email);
if (inviteUserInput.phoneNumber) {
query.where('phone_number', inviteUserInput.phoneNumber);
}
});
const serviceErrors: ServiceError[] = [];
if (foundUser && foundUser.email === inviteUserInput.email) {
this.logger.info('[send_user_invite] the given email exists.');
serviceErrors.push(new ServiceError('email_exists'));
}
if (foundUser && foundUser.phoneNumber === inviteUserInput.phoneNumber) {
this.logger.info('[send_user_invite] the given phone number exists.');
serviceErrors.push(new ServiceError('phone_number_exists'));
}
if (serviceErrors.length > 0) {
throw new ServiceErrors(serviceErrors);
}
}
}

View File

@@ -1,6 +0,0 @@
import Moment from 'moment';
import { extendMoment } from 'moment-range';
const moment = extendMoment(Moment);
export default moment;

View File

@@ -0,0 +1,56 @@
import { Service, Inject, Container } from 'typedi';
import { Tenant } from '@/system/models';
import TenantsManager from '@/system/TenantsManager';
import { ServiceError } from '@/exceptions';
import { ITenant } from '@/interfaces';
@Service()
export default class OrganizationService {
@Inject()
tenantsManager: TenantsManager;
@Inject('dbManager')
dbManager: any;
@Inject('logger')
logger: any;
/**
* Builds the database schema and seed data of the given organization id.
* @param {srting} organizationId
* @return {Promise<void>}
*/
async build(organizationId: string): Promise<void> {
const tenant = await Tenant.query().findOne('organization_id', organizationId);
this.throwIfTenantNotExists(tenant);
this.throwIfTenantInitizalized(tenant);
this.logger.info('[tenant_db_build] tenant DB creating.', { tenant });
await this.dbManager.createDb(`bigcapital_tenant_${tenant.organizationId}`);
const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId);
this.logger.info('[tenant_db_build] tenant DB migrating to latest version.', { tenant });
await tenantDb.migrate.latest();
this.logger.info('[tenant_db_build] mark tenant as initialized.', { tenant });
await tenant.$query().update({ initialized: true });
}
private throwIfTenantNotExists(tenant: ITenant) {
if (!tenant) {
this.logger.info('[tenant_db_build] organization id not found.');
throw new ServiceError('tenant_not_found');
}
}
private throwIfTenantInitizalized(tenant: ITenant) {
if (tenant.initialized) {
throw new ServiceError('tenant_initialized');
}
}
destroy() {
}
}

View File

@@ -1,77 +0,0 @@
import cache from 'memory-cache';
import { difference } from 'lodash';
import Role from '@/models/Role';
export default {
cacheKey: 'ratteb.cache,',
cacheExpirationTime: null,
permissions: [],
cache: null,
/**
* Initialize the cache.
*/
initializeCache() {
if (!this.cache) {
this.cache = new cache.Cache();
}
},
/**
* Purge all cached permissions.
*/
forgetCachePermissions() {
this.cache.del(this.cacheKey);
this.permissions = [];
},
/**
* Get all stored permissions.
*/
async getPermissions() {
if (this.permissions.length <= 0) {
const cachedPerms = this.cache.get(this.cacheKey);
if (!cachedPerms) {
this.permissions = await this.getPermissionsFromStorage();
this.cache.put(this.cacheKey, this.permissions);
} else {
this.permissions = cachedPerms;
}
}
return this.permissions;
},
/**
* Fetches all roles and permissions from the storage.
*/
async getPermissionsFromStorage() {
const roles = await Role.fetchAll({
withRelated: ['resources.permissions'],
});
return roles.toJSON();
},
/**
* Detarmine the given resource has the permissions.
* @param {String} resource -
* @param {Array} permissions -
*/
async hasPermissions(resource, permissions) {
await this.getPermissions();
const userRoles = this.permissions.filter((role) => role.id === this.id);
const perms = [];
userRoles.forEach((role) => {
const roleResources = role.resources || [];
const foundResource = roleResources.find((r) => r.name === resource);
if (foundResource && foundResource.permissions) {
foundResource.permissions.forEach((p) => perms.push(p.name));
}
});
const notAllowedPerms = difference(permissions, perms);
return (notAllowedPerms.length <= 0);
},
};

View File

@@ -27,6 +27,9 @@ export default class PaymentReceiveService {
@Inject()
journalService: JournalPosterService;
@Inject('logger')
logger: any;
/**
* Creates a new payment receive and store it to the storage
* with associated invoices payment and journal transactions.
@@ -43,6 +46,8 @@ export default class PaymentReceiveService {
} = this.tenancy.models(tenantId);
const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount');
this.logger.info('[payment_receive] inserting to the storage.');
const storedPaymentReceive = await PaymentReceive.query()
.insert({
amount: paymentAmount,
@@ -50,12 +55,15 @@ export default class PaymentReceiveService {
});
const storeOpers: Array<any> = [];
this.logger.info('[payment_receive] inserting associated entries to the storage.');
paymentReceive.entries.forEach((entry: any) => {
const oper = PaymentReceiveEntry.query()
.insert({
payment_receive_id: storedPaymentReceive.id,
...entry,
});
this.logger.info('[payment_receive] increment the sale invoice payment amount.');
// Increment the invoice payment amount.
const invoice = SaleInvoice.query()
.where('id', entry.invoice_id)
@@ -64,6 +72,8 @@ export default class PaymentReceiveService {
storeOpers.push(oper);
storeOpers.push(invoice);
});
this.logger.info('[payment_receive] decrementing customer balance.');
const customerIncrementOper = Customer.decrementBalance(
paymentReceive.customer_id,
paymentAmount,

View File

@@ -16,6 +16,9 @@ export default class SaleEstimateService {
@Inject()
itemsEntriesService: HasItemsEntries;
@Inject('logger')
logger: any;
/**
* Creates a new estimate with associated entries.
* @async
@@ -31,12 +34,15 @@ export default class SaleEstimateService {
amount,
...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']),
};
this.logger.info('[sale_estimate] inserting sale estimate to the storage.');
const storedEstimate = await SaleEstimate.query()
.insert({
...omit(estimate, ['entries']),
});
const storeEstimateEntriesOpers: any[] = [];
this.logger.info('[sale_estimate] inserting sale estimate entries to the storage.');
estimate.entries.forEach((entry: any) => {
const oper = ItemEntry.query()
.insert({
@@ -48,6 +54,8 @@ export default class SaleEstimateService {
});
await Promise.all([...storeEstimateEntriesOpers]);
this.logger.info('[sale_estimate] insert sale estimated success.');
return storedEstimate;
}
@@ -67,6 +75,7 @@ export default class SaleEstimateService {
amount,
...formatDateFields(estimateDTO, ['estimate_date', 'expiration_date']),
};
this.logger.info('[sale_estimate] editing sale estimate on the storage.');
const updatedEstimate = await SaleEstimate.query()
.update({
...omit(estimate, ['entries']),
@@ -96,14 +105,14 @@ export default class SaleEstimateService {
*/
async deleteEstimate(tenantId: number, estimateId: number) {
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
this.logger.info('[sale_estimate] delete sale estimate and associated entries from the storage.');
await ItemEntry.query()
.where('reference_id', estimateId)
.where('reference_type', 'SaleEstimate')
.delete();
await SaleEstimate.query()
.where('id', estimateId)
.delete();
await SaleEstimate.query().where('id', estimateId).delete();
}
/**
@@ -113,10 +122,10 @@ export default class SaleEstimateService {
* @param {Numeric} estimateId
* @return {Boolean}
*/
async isEstimateExists(estimateId: number) {
async isEstimateExists(tenantId: number, estimateId: number) {
const { SaleEstimate } = this.tenancy.models(tenantId);
const foundEstimate = await SaleEstimate.query()
.where('id', estimateId);
const foundEstimate = await SaleEstimate.query().where('id', estimateId);
return foundEstimate.length !== 0;
}
@@ -192,7 +201,6 @@ export default class SaleEstimateService {
const foundEstimates = await SaleEstimate.query()
.onBuild((query: any) => {
query.where('estimate_number', estimateNumber);
if (excludeEstimateId) {
query.whereNot('id', excludeEstimateId);
}

View File

@@ -23,6 +23,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@Inject()
itemsEntriesService: HasItemsEntries;
@Inject('logger')
logger: any;
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
@@ -43,12 +46,15 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
paymentAmount: 0,
invLotNumber,
};
this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
const storedInvoice = await SaleInvoice.query()
.insert({
...omit(saleInvoice, ['entries']),
});
const opers: Array<any> = [];
this.logger.info('[sale_invoice] inserting sale invoice entries to the storage.');
saleInvoice.entries.forEach((entry: any) => {
const oper = ItemEntry.query()
.insertAndFetch({
@@ -61,15 +67,16 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
opers.push(oper);
});
this.logger.info('[sale_invoice] trying to increment the customer balance.');
// Increment the customer balance after deliver the sale invoice.
const incrementOper = Customer.incrementBalance(
saleInvoice.customer_id,
balance,
);
// Await all async operations.
await Promise.all([
...opers, incrementOper,
]);
await Promise.all([ ...opers, incrementOper ]);
// Records the inventory transactions for inventory items.
await this.recordInventoryTranscactions(tenantId, saleInvoice, storedInvoice.id);
@@ -100,6 +107,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
balance,
invLotNumber: oldSaleInvoice.invLotNumber,
};
this.logger.info('[sale_invoice] trying to update sale invoice.');
const updatedSaleInvoices: ISaleInvoice = await SaleInvoice.query()
.where('id', saleInvoiceId)
.update({
@@ -114,6 +123,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
const patchItemsEntriesOper = this.itemsEntriesService.patchItemsEntries(
tenantId, saleInvoice.entries, storedEntries, 'SaleInvoice', saleInvoiceId,
);
this.logger.info('[sale_invoice] change customer different balance.');
// Changes the diff customer balance between old and new amount.
const changeCustomerBalanceOper = Customer.changeDiffBalance(
saleInvoice.customer_id,
@@ -155,12 +166,14 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
.findById(saleInvoiceId)
.withGraphFetched('entries');
this.logger.info('[sale_invoice] delete sale invoice with entries.');
await SaleInvoice.query().where('id', saleInvoiceId).delete();
await ItemEntry.query()
.where('reference_id', saleInvoiceId)
.where('reference_type', 'SaleInvoice')
.delete();
this.logger.info('[sale_invoice] revert the customer balance.');
const revertCustomerBalanceOper = Customer.changeBalance(
oldSaleInvoice.customerId,
oldSaleInvoice.balance * -1,
@@ -203,7 +216,13 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {number} saleInvoiceId -
* @param {boolean} override -
*/
recordInventoryTranscactions(tenantId: number, saleInvoice, saleInvoiceId: number, override?: boolean){
recordInventoryTranscactions(
tenantId: number,
saleInvoice,
saleInvoiceId: number,
override?: boolean
){
this.logger.info('[sale_invoice] saving inventory transactions');
const inventortyTransactions = saleInvoice.entries
.map((entry) => ({
...pick(entry, ['item_id', 'quantity', 'rate',]),
@@ -228,6 +247,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
const { InventoryTransaction } = this.tenancy.models(tenantId);
const opers: Promise<[]>[] = [];
this.logger.info('[sale_invoice] reverting inventory transactions');
inventoryTransactions.forEach((trans: any) => {
switch(trans.direction) {
@@ -359,7 +380,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* Writes the sale invoice journal entries.
* @param {SaleInvoice} saleInvoice -
*/
async writeNonInventoryInvoiceJournals(tenantId: number, saleInvoice: ISaleInvoice, override: boolean) {
async writeNonInventoryInvoiceJournals(
tenantId: number,
saleInvoice: ISaleInvoice,
override: boolean
) {
const { Account, AccountTransaction } = this.tenancy.models(tenantId);
const accountsDepGraph = await Account.depGraph().query();

View File

@@ -0,0 +1,15 @@
import Knex from 'knex';
import MetableStoreDB from '@/lib/Metable/MetableStoreDB';
import Setting from '@/models/Setting';
export default class SettingsStore extends MetableStoreDB {
/**
* Constructor method.
* @param {number} tenantId
*/
constructor(knex: Knex) {
super();
this.setExtraColumns(['group']);
this.setModel(Setting.bindKnex(knex));
}
}

View File

@@ -15,7 +15,6 @@ export default class HasTenancyService {
* @param {number} tenantId - The tenant id.
*/
models(tenantId: number) {
console.log(tenantId);
return this.tenantContainer(tenantId).get('models');
}
}

View File

@@ -7,4 +7,10 @@ export default {
sendResetPassword: 'onSendResetPassword',
resetPassword: 'onResetPassword',
},
}
inviteUser: {
acceptInvite: 'onUserAcceptInvite',
sendInvite: 'onUserSendInvite',
checkInvite: 'onUserCheckInvite'
}
}

View File

@@ -0,0 +1,29 @@
import { Container } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch';
import events from '@/subscribers/events';
@EventSubscriber()
export class InviteUserSubscriber {
@On(events.inviteUser.acceptInvite)
public onAcceptInvite(payload) {
const { inviteToken, user } = payload;
const agenda = Container.get('agenda');
}
@On(events.inviteUser.checkInvite)
public onCheckInvite(payload) {
const { inviteToken, organizationOptions } = payload;
const agenda = Container.get('agenda');
}
@On(events.inviteUser.sendInvite)
public onSendInvite(payload) {
const { invite } = payload;
const agenda = Container.get('agenda');
}
}

View File

@@ -1,12 +0,0 @@
export default class TenantEnviroment {
static get currentTenant() {
return this.currentTenantWebsite;
}
static set currentTenant(website) {
this.currentTenantWebsite = website;
}
}

View File

@@ -3,6 +3,7 @@ exports.up = function(knex) {
return knex.schema.createTable('tenants', (table) => {
table.bigIncrements();
table.string('organization_id');
table.boolean('initialized').defaultTo(false);
table.timestamps();
});
};

View File

@@ -1,6 +1,5 @@
import { mixin } from 'objection';
import SystemModel from '@/system/models/SystemModel';
import MetableCollection from '@/lib/Metable/MetableCollection';
export default class Option extends SystemModel {
/**
@@ -9,21 +8,4 @@ export default class Option extends SystemModel {
static get tableName() {
return 'options';
}
/**
* Override the model query.
* @param {...any} args -
*/
static query(...args) {
return super.query(...args).runAfter((result) => {
if (result instanceof MetableCollection) {
result.setModel(Option);
}
return result;
});
}
static get collection() {
return MetableCollection;
}
}