- 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

@@ -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,13 +0,0 @@
import winston from 'winston';
const transports = {
console: new winston.transports.Console({ level: 'warn' }),
file: new winston.transports.File({ filename: 'stdout.log' }),
};
export default winston.createLogger({
transports: [
transports.console,
transports.file,
],
});

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');
}
}