feat: remove path alias.

feat: remove Webpack and depend on nodemon.
feat: refactoring expenses.
feat: optimize system users with caching.
feat: architecture tenant optimize.
This commit is contained in:
Ahmed Bouhuolia
2020-09-15 00:51:39 +02:00
parent ad00f140d1
commit a22c8395f3
293 changed files with 3391 additions and 1637 deletions

View File

@@ -1,8 +1,8 @@
import { sumBy, chain } from 'lodash';
import JournalPoster from "./JournalPoster";
import JournalEntry from "./JournalEntry";
import { AccountTransaction } from '@/models';
import { IInventoryTransaction } from '@/interfaces';
import { AccountTransaction } from 'models';
import { IInventoryTransaction } from 'interfaces';
import AccountsService from '../Accounts/AccountsService';
import { IInventoryTransaction, IInventoryTransaction } from '../../interfaces';
@@ -120,6 +120,21 @@ export default class JournalCommands{
this.journal.credit(creditEntry);
}
async revertJournalEntries(
referenceId: number|number[],
referenceType: string
) {
const { AccountTransaction } = this.models;
const transactions = await AccountTransaction.query()
.where('reference_type', referenceType)
.whereIn('reference_id', Array.isArray(referenceId) ? referenceId : [referenceId])
.withGraphFetched('account.type');
this.journal.loadEntries(transactions);
this.journal.removeEntries();
}
/**
* Removes and revert accounts balance journal entries that associated
* to the given inventory transactions.

View File

@@ -1,14 +1,14 @@
import { omit } from 'lodash';
import { Container } from 'typedi';
import JournalEntry from '@/services/Accounting/JournalEntry';
import TenancyService from '@/services/Tenancy/TenancyService';
import JournalEntry from 'services/Accounting/JournalEntry';
import TenancyService from 'services/Tenancy/TenancyService';
import {
IJournalEntry,
IJournalPoster,
IAccountChange,
IAccountsChange,
TEntryType,
} from '@/interfaces';
} from 'interfaces';
export default class JournalPoster implements IJournalPoster {
tenantId: number;

View File

@@ -1,8 +1,8 @@
import { Inject, Service } from 'typedi';
import { kebabCase } from 'lodash'
import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { IAccountDTO, IAccount } from '@/interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
import { IAccountDTO, IAccount } from 'interfaces';
import { difference } from 'lodash';
import { Account } from 'src/models';

View File

@@ -2,8 +2,8 @@ import fs from 'fs';
import { Service, Container } from "typedi";
import Mustache from 'mustache';
import path from 'path';
import { ISystemUser } from '@/interfaces';
import config from '@/../config/config';
import { ISystemUser } from 'interfaces';
import config from 'config';
@Service()
export default class AuthenticationMailMesssages {
@@ -16,7 +16,7 @@ export default class AuthenticationMailMesssages {
sendWelcomeMessage(user: ISystemUser, organizationName: string): Promise<void> {
const Mail = Container.get('mail');
const filePath = path.join(global.rootPath, 'views/mail/Welcome.html');
const filePath = path.join(global.__root, 'views/mail/Welcome.html');
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, {
email: user.email,
@@ -50,7 +50,7 @@ export default class AuthenticationMailMesssages {
sendResetPasswordMessage(user: ISystemUser, token: string): Promise<void> {
const Mail = Container.get('mail');
const filePath = path.join(global.rootPath, 'views/mail/ResetPassword.html');
const filePath = path.join(global.__root, 'views/mail/ResetPassword.html');
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, {
resetPasswordUrl: `${config.baseURL}/reset/${token}`,

View File

@@ -1,5 +1,5 @@
import { Service, Inject } from "typedi";
import { ISystemUser, ITenant } from "@/interfaces";
import { ISystemUser, ITenant } from "interfaces";
@Service()
export default class AuthenticationSMSMessages {

View File

@@ -6,34 +6,27 @@ import moment from "moment";
import {
EventDispatcher,
EventDispatcherInterface,
} from '@/decorators/eventDispatcher';
import {
SystemUser,
PasswordReset,
Tenant,
} from '@/system/models';
} from 'decorators/eventDispatcher';
import { SystemUser, PasswordReset } from 'system/models';
import {
IRegisterDTO,
ITenant,
ISystemUser,
IPasswordReset,
} from '@/interfaces';
import TenantsManager from "@/system/TenantsManager";
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';
} from 'interfaces';
import { hashPassword } from 'utils';
import { ServiceError, ServiceErrors } from 'exceptions';
import config from 'config';
import events from 'subscribers/events';
import AuthenticationMailMessages from 'services/Authentication/AuthenticationMailMessages';
import AuthenticationSMSMessages from 'services/Authentication/AuthenticationSMSMessages';
import TenantsManager from 'services/Tenancy/TenantsManager';
@Service()
export default class AuthenticationService {
@Inject('logger')
logger: any;
@Inject()
tenantsManager: TenantsManager;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@@ -43,6 +36,12 @@ export default class AuthenticationService {
@Inject()
mailMessages: AuthenticationMailMessages;
@Inject('repositories')
sysRepositories: any;
@Inject()
tenantsManager: TenantsManager;
/**
* Signin and generates JWT token.
* @throws {ServiceError}
@@ -50,20 +49,16 @@ export default class AuthenticationService {
* @param {string} password - Password.
* @return {Promise<{user: IUser, token: string}>}
*/
async signIn(emailOrPhone: string, password: string): Promise<{user: IUser, token: string }> {
async signIn(emailOrPhone: string, password: string): Promise<{user: IUser, token: string, tenant: ITenant }> {
this.logger.info('[login] Someone trying to login.', { emailOrPhone, password });
const user = await SystemUser.query()
.where('email', emailOrPhone)
.orWhere('phone_number', emailOrPhone)
.withGraphFetched('tenant')
.first();
const { systemUserRepository } = this.sysRepositories;
const user = await systemUserRepository.findByCrediential(emailOrPhone);
if (!user) {
this.logger.info('[login] invalid data');
throw new ServiceError('invalid_details');
}
this.logger.info('[login] check password validation.');
if (!user.verifyPassword(password)) {
throw new ServiceError('invalid_password');
@@ -78,9 +73,7 @@ export default class AuthenticationService {
const token = this.generateToken(user);
this.logger.info('[login] updating user last login at.');
await SystemUser.query()
.where('id', user.id)
.patch({ last_login_at: moment().toMySqlDateTime() });
await systemUserRepository.patchLastLoginAt(user.id);
this.logger.info('[login] Logging success.', { user, token });
@@ -88,11 +81,15 @@ export default class AuthenticationService {
this.eventDispatcher.dispatch(events.auth.login, {
emailOrPhone, password,
});
const tenant = await user.$relatedQuery('tenant');
// Remove password property from user object.
Reflect.deleteProperty(user, 'password');
return { user, token };
// Remove id property from tenant object.
Reflect.deleteProperty(tenant, 'id');
return { user, token, tenant };
}
/**
@@ -101,18 +98,17 @@ export default class AuthenticationService {
* @param {IRegisterDTO} registerDTO - Register data object.
*/
private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) {
const user: ISystemUser = await SystemUser.query()
.where('email', registerDTO.email)
.orWhere('phone_number', registerDTO.phoneNumber)
.first();
const { systemUserRepository } = this.sysRepositories;
const isEmailExists = await systemUserRepository.getByEmail(registerDTO.email);
const isPhoneExists = await systemUserRepository.getByPhoneNumber(registerDTO.phoneNumber);
const errorReasons: ServiceErrors[] = [];
const errorReasons: ServiceError[] = [];
if (user && user.phoneNumber === registerDTO.phoneNumber) {
if (isPhoneExists) {
this.logger.info('[register] phone number exists on the storage.');
errorReasons.push(new ServiceError('phone_number_exists'));
}
if (user && user.email === registerDTO.email) {
if (isEmailExists) {
this.logger.info('[register] email exists on the storage.');
errorReasons.push(new ServiceError('email_exists'));
}
@@ -136,13 +132,13 @@ export default class AuthenticationService {
this.logger.info('[register] Trying hashing the password.')
const hashedPassword = await hashPassword(registerDTO.password);
const registeredUser = await SystemUser.query().insert({
const { systemUserRepository } = this.sysRepositories;
const registeredUser = await systemUserRepository.create({
...omit(registerDTO, 'country', 'organizationName'),
active: true,
password: hashedPassword,
tenant_id: tenant.id,
});
// Triggers `onRegister` event.
this.eventDispatcher.dispatch(events.auth.register, {
registerDTO, user: registeredUser
@@ -156,30 +152,7 @@ export default class AuthenticationService {
* @return {Promise<ITenant>}
*/
private async newTenantOrganization(): Promise<ITenant> {
const organizationId = uniqid();
const tenantOrganization = await Tenant.query().insert({
organization_id: organizationId,
});
return tenantOrganization;
}
/**
* Initialize tenant database.
* @param {number} tenantId - The given tenant id.
* @return {void}
*/
async initializeTenant(tenantId: number): Promise<void> {
const dbManager = Container.get('dbManager');
const tenant = await Tenant.query().findById(tenantId);
this.logger.info('[tenant_init] Tenant DB creating.', { tenant });
await dbManager.createDb(`bigcapital_tenant_${tenant.organizationId}`);
const tenantDb = this.tenantsManager.knexInstance(tenant.organizationId);
this.logger.info('[tenant_init] Tenant DB migrating to latest version.', { tenant });
await tenantDb.migrate.latest();
return this.tenantsManager.createTenant();
}
/**
@@ -188,12 +161,14 @@ export default class AuthenticationService {
* @param {string} email - email address.
*/
private async validateEmailExistance(email: string) {
const foundEmail = await SystemUser.query().findOne('email', email);
const { systemUserRepository } = this.sysRepositories;
const userByEmail = await systemUserRepository.getByEmail(email);
if (!foundEmail) {
if (!userByEmail) {
this.logger.info('[send_reset_password] The given email not found.');
throw new ServiceError('email_not_found');
}
return userByEmail;
}
/**
@@ -203,7 +178,7 @@ export default class AuthenticationService {
*/
async sendResetPassword(email: string): Promise<IPasswordReset> {
this.logger.info('[send_reset_password] Trying to send reset password.');
await this.validateEmailExistance(email);
const user = await this.validateEmailExistance(email);
// Delete all stored tokens of reset password that associate to the give email.
this.logger.info('[send_reset_password] trying to delete all tokens by email.');
@@ -213,7 +188,6 @@ export default class AuthenticationService {
this.logger.info('[send_reset_password] insert the generated token.');
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 });
@@ -228,7 +202,8 @@ export default class AuthenticationService {
* @return {Promise<void>}
*/
async resetPassword(token: string, password: string): Promise<void> {
const tokenModel = await PasswordReset.query().findOne('token', token)
const { systemUserRepository } = this.sysRepositories;
const tokenModel = await PasswordReset.query().findOne('token', token);
if (!tokenModel) {
this.logger.info('[reset_password] token invalid.');
@@ -242,7 +217,7 @@ export default class AuthenticationService {
await this.deletePasswordResetToken(tokenModel.email);
throw new ServiceError('token_expired');
}
const user = await SystemUser.query().findOne('email', tokenModel.email)
const user = await systemUserRepository.getByEmail(tokenModel.email);
if (!user) {
throw new ServiceError('user_not_found');
@@ -250,10 +225,8 @@ export default class AuthenticationService {
const hashedPassword = await hashPassword(password);
this.logger.info('[reset_password] saving a new hashed password.');
await SystemUser.query()
.where('email', tokenModel.email)
.update({ password: hashedPassword });
await systemUserRepository.edit(user.id, { password: hashedPassword });
// Deletes the used token.
await this.deletePasswordResetToken(tokenModel.email);

View File

@@ -1,12 +1,13 @@
import { Inject, Service } from 'typedi';
import { difference } from 'lodash';
import { ServiceError } from "@/exceptions";
import TenancyService from '@/services/Tenancy/TenancyService';
import { difference, upperFirst } from 'lodash';
import { ServiceError } from "exceptions";
import TenancyService from 'services/Tenancy/TenancyService';
import {
IContact,
IContactNewDTO,
IContactEditDTO,
} from "@/interfaces";
} from "interfaces";
import JournalPoster from '../Accounting/JournalPoster';
type TContactService = 'customer' | 'vendor';
@@ -128,4 +129,31 @@ export default class ContactsService {
await Contact.query().whereIn('id', contactsIds).delete();
}
/**
* Reverts journal entries of the given contacts.
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
*/
async revertJEntriesContactsOpeningBalance(
tenantId: number,
contactsIds: number[],
contactService: TContactService
) {
const { AccountTransaction } = this.tenancy.models(tenantId);
const journal = new JournalPoster(tenantId);
const contactsTransactions = await AccountTransaction.query()
.whereIn('reference_id', contactsIds)
.where('reference_type', `${upperFirst(contactService)}OpeningBalance`);
journal.loadEntries(contactsTransactions);
journal.removeEntries();
await Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
]);
}
}

View File

@@ -1,14 +1,14 @@
import { Inject, Service } from 'typedi';
import { omit, difference } from 'lodash';
import JournalPoster from "@/services/Accounting/JournalPoster";
import JournalCommands from "@/services/Accounting/JournalCommands";
import ContactsService from '@/services/Contacts/ContactsService';
import JournalPoster from "services/Accounting/JournalPoster";
import JournalCommands from "services/Accounting/JournalCommands";
import ContactsService from 'services/Contacts/ContactsService';
import {
ICustomerNewDTO,
ICustomerEditDTO,
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService';
} from 'interfaces';
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
import { ICustomer } from 'src/interfaces';
@Service()
@@ -44,7 +44,7 @@ export default class CustomersService {
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer');
// Writes the customer opening balance journal entries.
if (customer.openingBalance) {
if (customer.openingBalance) {
await this.writeCustomerOpeningBalanceJournal(
tenantId,
customer.id,
@@ -71,8 +71,16 @@ export default class CustomersService {
* @return {Promise<void>}
*/
async deleteCustomer(tenantId: number, customerId: number) {
const { Contact } = this.tenancy.models(tenantId);
await this.getCustomerByIdOrThrowError(tenantId, customerId);
await this.customerHasNoInvoicesOrThrowError(tenantId, customerId);
return this.contactService.deleteContact(tenantId, customerId, 'customer');
await Contact.query().findById(customerId).delete();
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId, [customerId], 'customer',
);
}
/**
@@ -107,6 +115,15 @@ export default class CustomersService {
]);
}
/**
* Retrieve the given customer by id or throw not found.
* @param {number} tenantId
* @param {number} customerId
*/
getCustomerByIdOrThrowError(tenantId: number, customerId: number) {
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'customer');
}
/**
* Retrieve the given customers or throw error if one of them not found.
* @param {numebr} tenantId
@@ -129,6 +146,12 @@ export default class CustomersService {
await this.customersHaveNoInvoicesOrThrowError(tenantId, customersIds);
await Contact.query().whereIn('id', customersIds).delete();
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId,
customersIds,
'Customer'
);
}
/**

View File

@@ -1,15 +1,15 @@
import { Inject, Service } from 'typedi';
import { difference } from 'lodash';
import JournalPoster from "@/services/Accounting/JournalPoster";
import JournalCommands from "@/services/Accounting/JournalCommands";
import ContactsService from '@/services/Contacts/ContactsService';
import JournalPoster from "services/Accounting/JournalPoster";
import JournalCommands from "services/Accounting/JournalCommands";
import ContactsService from 'services/Contacts/ContactsService';
import {
IVendorNewDTO,
IVendorEditDTO,
IVendor
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService';
} from 'interfaces';
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class VendorsService {
@@ -39,7 +39,8 @@ export default class VendorsService {
* @return {Promise<void>}
*/
async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) {
const contactDTO = this.vendorToContactDTO(vendorDTO)
const contactDTO = this.vendorToContactDTO(vendorDTO);
const vendor = await this.contactService.newContact(tenantId, contactDTO, 'vendor');
// Writes the vendor opening balance journal entries.
@@ -63,6 +64,15 @@ export default class VendorsService {
return this.contactService.editContact(tenantId, vendorId, contactDTO, 'vendor');
}
/**
* Retrieve the given vendor details by id or throw not found.
* @param {number} tenantId
* @param {number} customerId
*/
getVendorByIdOrThrowError(tenantId: number, customerId: number) {
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'vendor');
}
/**
* Deletes the given vendor from the storage.
* @param {number} tenantId
@@ -70,8 +80,16 @@ export default class VendorsService {
* @return {Promise<void>}
*/
async deleteVendor(tenantId: number, vendorId: number) {
const { Contact } = this.tenancy.models(tenantId);
await this.getVendorByIdOrThrowError(tenantId, vendorId);
await this.vendorHasNoBillsOrThrowError(tenantId, vendorId);
return this.contactService.deleteContact(tenantId, vendorId, 'vendor');
await Contact.query().findById(vendorId).delete();
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId, [vendorId], 'vendor',
);
}
/**
@@ -128,6 +146,10 @@ export default class VendorsService {
await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds);
await Contact.query().whereIn('id', vendorsIds).delete();
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId, vendorsIds, 'vendor',
);
}
/**
@@ -139,7 +161,7 @@ export default class VendorsService {
const { vendorRepository } = this.tenancy.repositories(tenantId);
const bills = await vendorRepository.getBills(vendorId);
if (bills) {
if (bills.length > 0) {
throw new ServiceError('vendor_has_bills')
}
}

View File

@@ -1,7 +1,7 @@
import Resource from '@/models/Resource';
import ResourceField from '@/models/ResourceField';
import ResourceFieldMetadata from '@/models/ResourceFieldMetadata';
import ResourceFieldMetadataCollection from '@/collection/ResourceFieldMetadataCollection';
import Resource from 'models/Resource';
import ResourceField from 'models/ResourceField';
import ResourceFieldMetadata from 'models/ResourceFieldMetadata';
import ResourceFieldMetadataCollection from 'collection/ResourceFieldMetadataCollection';
export default class ResourceCustomFieldRepository {
/**

View File

@@ -4,11 +4,11 @@ import {
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter';
} from 'lib/DynamicFilter';
import {
mapViewRolesToConditionals,
mapFilterRolesToDynamicFilter,
} from '@/lib/ViewRolesBuilder';
} from 'lib/ViewRolesBuilder';
export const DYNAMIC_LISTING_ERRORS = {
LOGIC_INVALID: 'VIEW.LOGIC.EXPRESSION.INVALID',

View File

@@ -1,4 +1,4 @@
import { DYNAMIC_LISTING_ERRORS } from '@/services/DynamicListing/DynamicListing';
import { DYNAMIC_LISTING_ERRORS } from 'services/DynamicListing/DynamicListing';
export const dynamicListingErrorsToResponse = (error) => {
let _errors;

View File

@@ -0,0 +1,432 @@
import { Service, Inject } from "typedi";
import { difference, sumBy } from 'lodash';
import moment from "moment";
import { ServiceError } from "exceptions";
import TenancyService from 'services/Tenancy/TenancyService';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import JournalCommands from 'services/Accounting/JournalCommands';
import { IExpense, IAccount, IExpenseDTO, IExpenseCategory, IExpensesService, ISystemUser } from 'interfaces';
const ERRORS = {
EXPENSE_NOT_FOUND: 'expense_not_found',
PAYMENT_ACCOUNT_NOT_FOUND: 'payment_account_not_found',
SOME_ACCOUNTS_NOT_FOUND: 'some_expenses_not_found',
TOTAL_AMOUNT_EQUALS_ZERO: 'total_amount_equals_zero',
PAYMENT_ACCOUNT_HAS_INVALID_TYPE: 'payment_account_has_invalid_type',
EXPENSES_ACCOUNT_HAS_INVALID_TYPE: 'expenses_account_has_invalid_type',
EXPENSE_ACCOUNT_ALREADY_PUBLISED: 'expense_already_published',
};
@Service()
export default class ExpensesService implements IExpensesService {
@Inject()
tenancy: TenancyService;
@Inject('logger')
logger: any;
/**
* Retrieve the payment account details or returns not found server error in case the
* given account not found on the storage.
* @param {number} tenantId
* @param {number} paymentAccountId
* @returns {Promise<IAccount>}
*/
async getPaymentAccountOrThrowError(tenantId: number, paymentAccountId: number) {
this.logger.info('[expenses] trying to get the given payment account.', { tenantId, paymentAccountId });
const { accountRepository } = this.tenancy.repositories(tenantId);
const paymentAccount = await accountRepository.getById(paymentAccountId)
if (!paymentAccount) {
this.logger.info('[expenses] the given payment account not found.', { tenantId, paymentAccountId });
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND);
}
return paymentAccount;
}
/**
* Retrieve expense accounts or throw error in case one of the given accounts
* not found not the storage.
* @param {number} tenantId
* @param {number} expenseAccountsIds
* @throws {ServiceError}
* @returns {Promise<IAccount[]>}
*/
async getExpensesAccountsOrThrowError(tenantId: number, expenseAccountsIds: number[]) {
this.logger.info('[expenses] trying to get expenses accounts.', { tenantId, expenseAccountsIds });
const { Account } = this.tenancy.models(tenantId);
const storedExpenseAccounts = await Account.query().whereIn(
'id', expenseAccountsIds,
);
const storedExpenseAccountsIds = storedExpenseAccounts.map((a: IAccount) => a.id);
const notStoredAccountsIds = difference(
expenseAccountsIds,
storedExpenseAccountsIds
);
if (notStoredAccountsIds.length > 0) {
this.logger.info('[expenses] some of expense accounts not found.', { tenantId, expenseAccountsIds });
throw new ServiceError(ERRORS.SOME_ACCOUNTS_NOT_FOUND);
}
return storedExpenseAccounts;
}
/**
* Validates expense categories not equals zero.
* @param {IExpenseDTO|ServiceError} expenseDTO
* @throws {ServiceError}
*/
validateCategoriesNotEqualZero(expenseDTO: IExpenseDTO) {
this.logger.info('[expenses] validate the expenses categoires not equal zero.', { expenseDTO });
const totalAmount = sumBy(expenseDTO.categories, 'amount') || 0;
if (totalAmount <= 0) {
this.logger.info('[expenses] the given expense categories equal zero.', { expenseDTO });
throw new ServiceError(ERRORS.TOTAL_AMOUNT_EQUALS_ZERO);
}
}
/**
* Validate expenses accounts type.
* @param {number} tenantId
* @param {number[]} expensesAccountsIds
*/
async validateExpensesAccountsType(tenantId: number, expensesAccounts: number[]) {
this.logger.info('[expenses] trying to validate expenses accounts type.', { tenantId, expensesAccounts });
const { accountTypeRepository } = this.tenancy.repositories(tenantId);
const expensesTypes = await accountTypeRepository.getByRootType('expense');
const expensesTypesIds = expensesTypes.map(t => t.id);
const invalidExpenseAccounts: number[] = [];
expensesAccounts.forEach((expenseAccount) => {
if (expensesTypesIds.indexOf(expenseAccount.accountTypeId) === -1) {
invalidExpenseAccounts.push(expenseAccount.id);
}
});
if (invalidExpenseAccounts.length > 0) {
throw new ServiceError(ERRORS.EXPENSES_ACCOUNT_HAS_INVALID_TYPE);
}
}
/**
* Validates payment account type in case has invalid type throws errors.
* @param {number} tenantId
* @param {number} paymentAccountId
* @throws {ServiceError}
*/
async validatePaymentAccountType(tenantId: number, paymentAccount: number[]) {
this.logger.info('[expenses] trying to validate payment account type.', { tenantId, paymentAccount });
const { accountTypeRepository } = this.tenancy.repositories(tenantId);
const validAccountsType = await accountTypeRepository.getByKeys([
'current_asset', 'fixed_asset',
]);
const validAccountsTypeIds = validAccountsType.map(t => t.id);
if (validAccountsTypeIds.indexOf(paymentAccount.accountTypeId) === -1) {
this.logger.info('[expenses] the given payment account has invalid type', { tenantId, paymentAccount });
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_HAS_INVALID_TYPE);
}
}
async revertJournalEntries(
tenantId: number,
expenseId: number|number[],
) {
const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal);
if (revertOld) {
await journalCommands.revertJournalEntries(expenseId, 'Expense');
}
return Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
]);
}
/**
* Writes expense journal entries.
* @param {number} tenantId
* @param {IExpense} expense
* @param {IUser} authorizedUser
*/
async writeJournalEntries(
tenantId: number,
expense: IExpense,
revertOld: boolean,
authorizedUser: ISystemUser
) {
this.logger.info('[expense[ trying to write expense journal entries.', { tenantId, expense });
const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal);
if (revertOld) {
await journalCommands.revertJournalEntries(expense.id, 'Expense');
}
const mixinEntry = {
referenceType: 'Expense',
referenceId: expense.id,
date: expense.paymentDate,
userId: authorizedUser.id,
draft: !expense.publish,
};
const paymentJournalEntry = new JournalEntry({
credit: expense.totalAmount,
account: expense.paymentAccountId,
...mixinEntry,
});
journal.credit(paymentJournalEntry);
expense.categories.forEach((category: IExpenseCategory) => {
const expenseJournalEntry = new JournalEntry({
account: category.expenseAccountId,
debit: category.amount,
note: category.description,
...mixinEntry,
});
journal.debit(expenseJournalEntry);
});
return Promise.all([
journal.saveBalance(),
journal.saveEntries(),
journal.deleteEntries(),
]);
}
/**
* Retrieve the given expenses or throw not found error.
* @param {number} tenantId
* @param {number} expenseId
* @returns {IExpense|ServiceError}
*/
async getExpenseOrThrowError(tenantId: number, expenseId: number) {
const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to get the given expense.', { tenantId, expenseId });
const expense = await expenseRepository.getById(expenseId);
if (!expense) {
this.logger.info('[expense] the given expense not found.', { tenantId, expenseId });
throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND);
}
return expense;
}
async getExpensesOrThrowError(tenantId: number, expensesIds: number[]) {
}
/**
* Validates expenses is not already published before.
* @param {IExpense} expense
*/
validateExpenseIsNotPublished(expense: IExpense) {
if (expense.published) {
throw new ServiceError(ERRORS.EXPENSE_ACCOUNT_ALREADY_PUBLISED);
}
}
/**
* Mapping expense DTO to model.
* @param {IExpenseDTO} expenseDTO
* @return {IExpense}
*/
expenseDTOToModel(expenseDTO: IExpenseDTO) {
const totalAmount = sumBy(expenseDTO.categories, 'amount');
return {
published: false,
categories: [],
...expenseDTO,
totalAmount,
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),
}
}
/**
* Mapping the expenses accounts ids from expense DTO.
* @param {IExpenseDTO} expenseDTO
* @return {number[]}
*/
mapExpensesAccountsIdsFromDTO(expenseDTO: IExpenseDTO) {
return expenseDTO.categories.map((category) => category.expenseAccountId);
}
/**
* Precedures.
* ---------
* 1. Validate expense existance.
* 2. Validate payment account existance on the storage.
* 3. Validate expense accounts exist on the storage.
* 4. Validate payment account type.
* 5. Validate expenses accounts type.
* 6. Validate the given expense categories not equal zero.
* 7. Stores the expense to the storage.
* ---------
* @param {number} tenantId
* @param {number} expenseId
* @param {IExpenseDTO} expenseDTO
*/
async editExpense(
tenantId: number,
expenseId: number,
expenseDTO: IExpenseDTO,
authorizedUser: ISystemUser
): Promise<IExpense> {
const { expenseRepository } = this.tenancy.repositories(tenantId);
const expense = await this.getExpenseOrThrowError(tenantId, expenseId);
// 1. Validate payment account existance on the storage.
const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId,
expenseDTO.paymentAccountId,
);
// 2. Validate expense accounts exist on the storage.
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
tenantId,
this.mapExpensesAccountsIdsFromDTO(expenseDTO),
);
// 3. Validate payment account type.
await this.validatePaymentAccountType(tenantId, paymentAccount);
// 4. Validate expenses accounts type.
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
// 5. Validate the given expense categories not equal zero.
this.validateCategoriesNotEqualZero(expenseDTO);
// 6. Update the expense on the storage.
const expenseObj = this.expenseDTOToModel(expenseDTO);
const expenseModel = await expenseRepository.update(expenseId, expenseObj, null);
// 7. In case expense published, write journal entries.
if (expenseObj.published) {
await this.writeJournalEntries(tenantId, expenseModel, true, authorizedUser);
}
this.logger.info('[expense] the expense updated on the storage successfully.', { tenantId, expenseDTO });
return expenseModel;
}
/**
* Precedures.
* ---------
* 1. Validate payment account existance on the storage.
* 2. Validate expense accounts exist on the storage.
* 3. Validate payment account type.
* 4. Validate expenses accounts type.
* 5. Validate the given expense categories not equal zero.
* 6. Stores the expense to the storage.
* ---------
* @param {number} tenantId
* @param {IExpenseDTO} expenseDTO
*/
async newExpense(tenantId: number, expenseDTO: IExpenseDTO, authorizedUser: ISystemUser): Promise<IExpense> {
const { expenseRepository } = this.tenancy.repositories(tenantId);
// 1. Validate payment account existance on the storage.
const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId,
expenseDTO.paymentAccountId,
);
// 2. Validate expense accounts exist on the storage.
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
tenantId,
this.mapExpensesAccountsIdsFromDTO(expenseDTO),
);
// 3. Validate payment account type.
await this.validatePaymentAccountType(tenantId, paymentAccount);
// 4. Validate expenses accounts type.
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
// 5. Validate the given expense categories not equal zero.
this.validateCategoriesNotEqualZero(expenseDTO);
// 6. Save the expense to the storage.
const expenseObj = this.expenseDTOToModel(expenseDTO);
const expenseModel = await expenseRepository.create(expenseObj);
// 7. In case expense published, write journal entries.
if (expenseObj.published) {
await this.writeJournalEntries(tenantId, expenseModel, false, authorizedUser);
}
this.logger.info('[expense] the expense stored to the storage successfully.', { tenantId, expenseDTO });
return expenseModel;
}
/**
* Publish the given expense.
* @param {number} tenantId
* @param {number} expenseId
* @return {Promise<void>}
*/
async publishExpense(tenantId: number, expenseId: number) {
const { expenseRepository } = this.tenancy.repositories(tenantId);
const expense = await this.getExpenseOrThrowError(tenantId, expenseId);
if (expense instanceof ServiceError) {
throw expense;
}
this.validateExpenseIsNotPublished(expense);
this.logger.info('[expense] trying to publish the expense.', { tenantId, expenseId });
await expenseRepository.publish(expenseId);
this.logger.info('[expense] the expense published successfully.', { tenantId, expenseId });
}
/**
* Deletes the given expense.
* @param {number} tenantId
* @param {number} expenseId
*/
async deleteExpense(tenantId: number, expenseId: number) {
const expense = await this.getExpenseOrThrowError(tenantId, expenseId);
const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to delete the expense.', { tenantId, expenseId });
await expenseRepository.delete(expenseId);
if (expense.published) {
await this.revertJournalEntries(tenantId, expenseId);
}
this.logger.info('[expense] the expense deleted successfully.', { tenantId, expenseId });
}
/**
* Deletes the given expenses in bulk.
* @param {number} tenantId
* @param {number[]} expensesIds
*/
async deleteBulkExpenses(tenantId: number, expensesIds: number[]) {
const expenses = await this.getExpensesOrThrowError(tenantId, expensesIds);
const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to delete the given expenses.', { tenantId, expensesIds });
await expenseRepository.bulkDelete(expensesIds);
await this.revertJournalEntries(tenantId, expensesIds);
this.logger.info('[expense] the given expenses deleted successfully.', { tenantId, expensesIds });
}
/**
* Deletes the given expenses in bulk.
* @param {number} tenantId
* @param {number[]} expensesIds
*/
async publishBulkExpenses(tenantId: number, expensesIds: number[]) {
const expenses = await this.getExpensesOrThrowError(tenantId, expensesIds);
const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to publish the given expenses.', { tenantId, expensesIds });
await expenseRepository.publishBulk(expensesIds);
this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds });
}
}

View File

@@ -1,7 +1,7 @@
import { Container, Service, Inject } from 'typedi';
import InventoryAverageCost from '@/services/Inventory/InventoryAverageCost';
import InventoryCostLotTracker from '@/services/Inventory/InventoryCostLotTracker';
import TenancyService from '@/services/Tenancy/TenancyService';
import InventoryAverageCost from 'services/Inventory/InventoryAverageCost';
import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker';
import TenancyService from 'services/Tenancy/TenancyService';
type TCostMethod = 'FIFO' | 'LIFO' | 'AVG';

View File

@@ -1,6 +1,6 @@
import { pick } from 'lodash';
import { IInventoryTransaction } from '@/interfaces';
import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod';
import { IInventoryTransaction } from 'interfaces';
import InventoryCostMethod from 'services/Inventory/InventoryCostMethod';
export default class InventoryAverageCostMethod extends InventoryCostMethod implements IInventoryCostMethod {
startingDate: Date;

View File

@@ -1,7 +1,7 @@
import { pick, chain } from 'lodash';
import moment from 'moment';
import { IInventoryLotCost, IInventoryTransaction } from "@/interfaces";
import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod';
import { IInventoryLotCost, IInventoryTransaction } from "interfaces";
import InventoryCostMethod from 'services/Inventory/InventoryCostMethod';
type TCostMethod = 'FIFO' | 'LIFO';

View File

@@ -1,7 +1,7 @@
import { omit } from 'lodash';
import { Inject } from 'typedi';
import TenancyService from '@/services/Tenancy/TenancyService';
import { IInventoryLotCost } from '@/interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import { IInventoryLotCost } from 'interfaces';
export default class InventoryCostMethod {
@Inject()

View File

@@ -4,7 +4,7 @@ import { Service } from "typedi";
export default class InviteUsersMailMessages {
sendInviteMail() {
const filePath = path.join(global.rootPath, 'views/mail/UserInvite.html');
const filePath = path.join(global.__root, 'views/mail/UserInvite.html');
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, {

View File

@@ -4,19 +4,18 @@ import moment from 'moment';
import {
EventDispatcher,
EventDispatcherInterface,
} from '@/decorators/eventDispatcher';
import { ServiceError } from "@/exceptions";
import { SystemUser, Invite, Tenant } from "@/system/models";
import { Option } from '@/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';
} from 'decorators/eventDispatcher';
import { ServiceError } from "exceptions";
import { Invite, Tenant } from "system/models";
import { Option } from 'models';
import { hashPassword } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService';
import InviteUsersMailMessages from "services/InviteUsers/InviteUsersMailMessages";
import events from 'subscribers/events';
import {
ISystemUser,
IInviteUserInput,
} from '@/interfaces';
} from 'interfaces';
@Service()
export default class InviteUserService {
@@ -26,15 +25,15 @@ export default class InviteUserService {
@Inject()
tenancy: TenancyService;
@Inject()
tenantsManager: TenantsManager;
@Inject('logger')
logger: any;
@Inject()
mailMessages: InviteUsersMailMessages;
@Inject('repositories')
sysRepositories: any;
/**
* Accept the received invite.
* @param {string} token
@@ -50,24 +49,26 @@ export default class InviteUserService {
const hashedPassword = await hashPassword(inviteUserInput.password);
this.logger.info('[accept_invite] trying to update user details.');
const updateUserOper = SystemUser.query()
.where('email', inviteToken.email)
.patch({
...inviteUserInput,
active: 1,
invite_accepted_at: moment().format('YYYY-MM-DD'),
password: hashedPassword,
});
const { systemUserRepository } = this.sysRepositories;
const user = await systemUserRepository.getByEmail(inviteToken.email);
const updateUserOper = systemUserRepository.edit(user.id, {
...inviteUserInput,
active: 1,
invite_accepted_at: moment().format('YYYY-MM-DD'),
password: hashedPassword,
});
this.logger.info('[accept_invite] trying to delete the given token.');
const deleteInviteTokenOper = Invite.query().where('token', inviteToken.token).delete();
// Await all async operations.
const [user] = await Promise.all([updateUserOper, deleteInviteTokenOper]);
const [updatedUser] = await Promise.all([updateUserOper, deleteInviteTokenOper]);
// Triggers `onUserAcceptInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
inviteToken, user,
inviteToken, user: updatedUser,
});
}
@@ -79,7 +80,7 @@ export default class InviteUserService {
*
* @return {Promise<IInvite>}
*/
public async sendInvite(tenantId: number, email: string, authorizedUser: ISystemUser): Promise<IInvite> {
public async sendInvite(tenantId: number, email: string, authorizedUser: ISystemUser): Promise<{ invite: IInvite, user: ISystemUser }> {
await this.throwErrorIfUserEmailExists(email);
this.logger.info('[send_invite] trying to store invite token.');
@@ -90,11 +91,12 @@ export default class InviteUserService {
});
this.logger.info('[send_invite] trying to store user with email and tenant.');
const user = await SystemUser.query().insert({
const { systemUserRepository } = this.sysRepositories;
const user = await systemUserRepository.create({
email,
tenant_id: authorizedUser.tenantId,
active: 1,
})
});
// Triggers `onUserSendInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.sendInvite, {
@@ -130,12 +132,14 @@ export default class InviteUserService {
* Throws error in case the given user email not exists on the storage.
* @param {string} email
*/
private async throwErrorIfUserEmailExists(email: string) {
const foundUser = await SystemUser.query().findOne('email', email);
private async throwErrorIfUserEmailExists(email: string): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.getByEmail(email);
if (foundUser) {
throw new ServiceError('email_already_invited');
}
return foundUser;
}
/**
@@ -158,12 +162,9 @@ export default class InviteUserService {
* Validate the given user email and phone number uniquine.
* @param {IInviteUserInput} inviteUserInput
*/
private async validateUserPhoneNumber(inviteUserInput: IInviteUserInput) {
const foundUser = await SystemUser.query()
.onBuild(query => {
query.where('phone_number', inviteUserInput.phoneNumber);
query.first();
});
private async validateUserPhoneNumber(inviteUserInput: IInviteUserInput): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.getByPhoneNumber(inviteUserInput.phoneNumber)
if (foundUser) {
throw new ServiceError('phone_number_exists');

View File

@@ -1,6 +1,6 @@
import { difference } from "lodash";
import { Service, Inject } from "typedi";
import TenancyService from '@/services/Tenancy/TenancyService';
import TenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class ItemsService {

View File

@@ -1,54 +1,94 @@
import { Service, Inject, Container } from 'typedi';
import { Tenant, SystemUser } from '@/system/models';
import TenantsManager from '@/system/TenantsManager';
import { ServiceError } from '@/exceptions';
import { ITenant } from '@/interfaces';
import { Service, Inject } from 'typedi';
import { ServiceError } from 'exceptions';
import { ITenant } from 'interfaces';
import {
EventDispatcher,
EventDispatcherInterface,
} from '@/decorators/eventDispatcher';
import events from '@/subscribers/events';
} from 'decorators/eventDispatcher';
import events from 'subscribers/events';
import {
TenantAlreadyInitialized,
TenantAlreadySeeded,
TenantDatabaseNotBuilt
} from 'exceptions';
import TenantsManager from 'services/Tenancy/TenantsManager';
const ERRORS = {
TENANT_NOT_FOUND: 'tenant_not_found',
TENANT_ALREADY_INITIALIZED: 'tenant_already_initialized',
TENANT_ALREADY_SEEDED: 'tenant_already_seeded',
TENANT_DB_NOT_BUILT: 'tenant_db_not_built',
};
@Service()
export default class OrganizationService {
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject()
tenantsManager: TenantsManager;
@Inject('dbManager')
dbManager: any;
@Inject('logger')
logger: any;
@Inject('repositories')
sysRepositories: any;
@Inject()
tenantsManager: TenantsManager;
/**
* Builds the database schema and seed data of the given organization id.
* @param {srting} organizationId
* @param {srting} organizationId
* @return {Promise<void>}
*/
async build(organizationId: string): Promise<void> {
const tenant = await Tenant.query().findOne('organization_id', organizationId);
this.throwIfTenantNotExists(tenant);
const tenant = await this.getTenantByOrgIdOrThrowError(organizationId);
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);
const tenantHasDB = await this.tenantsManager.hasDatabase(tenant);
this.logger.info('[tenant_db_build] tenant DB migrating to latest version.', { tenant });
await tenantDb.migrate.latest();
try {
if (!tenantHasDB) {
this.logger.info('[organization] trying to create tenant database.', { organizationId });
await this.tenantsManager.createDatabase(tenant);
}
this.logger.info('[organization] trying to migrate tenant database.', { organizationId });
await this.tenantsManager.migrateTenant(tenant);
this.logger.info('[tenant_db_build] mark tenant as initialized.', { tenant });
await tenant.$query().update({ initialized: true });
// Throws `onOrganizationBuild` event.
this.eventDispatcher.dispatch(events.organization.build, { tenant });
// Retrieve the tenant system user.
const user = await SystemUser.query().findOne('tenant_id', tenant.id);
} catch (error) {
if (error instanceof TenantAlreadyInitialized) {
throw new ServiceError(ERRORS.TENANT_ALREADY_INITIALIZED);
} else {
throw error;
}
}
}
// Throws `onOrganizationBuild` event.
this.eventDispatcher.dispatch(events.organization.build, { tenant, user });
/**
* Seeds initial core data to the given organization tenant.
* @param {number} organizationId
* @return {Promise<void>}
*/
async seed(organizationId: string): Promise<void> {
const tenant = await this.getTenantByOrgIdOrThrowError(organizationId);
this.throwIfTenantSeeded(tenant);
try {
this.logger.info('[organization] trying to seed tenant database.', { organizationId });
await this.tenantsManager.seedTenant(tenant);
// Throws `onOrganizationBuild` event.
this.eventDispatcher.dispatch(events.organization.seeded, { tenant });
} catch (error) {
if (error instanceof TenantAlreadySeeded) {
throw new ServiceError(ERRORS.TENANT_ALREADY_SEEDED);
} else if (error instanceof TenantDatabaseNotBuilt) {
throw new ServiceError(ERRORS.TENANT_DB_NOT_BUILT);
} else {
throw error;
}
}
}
/**
@@ -58,7 +98,7 @@ export default class OrganizationService {
private throwIfTenantNotExists(tenant: ITenant) {
if (!tenant) {
this.logger.info('[tenant_db_build] organization id not found.');
throw new ServiceError('tenant_not_found');
throw new ServiceError(ERRORS.TENANT_NOT_FOUND);
}
}
@@ -67,8 +107,32 @@ export default class OrganizationService {
* @param {ITenant} tenant
*/
private throwIfTenantInitizalized(tenant: ITenant) {
if (tenant.initialized) {
throw new ServiceError('tenant_initialized');
if (tenant.initializedAt) {
throw new ServiceError(ERRORS.TENANT_ALREADY_INITIALIZED);
}
}
/**
* Throws service if the tenant already seeded.
* @param {ITenant} tenant
*/
private throwIfTenantSeeded(tenant: ITenant) {
if (tenant.seededAt) {
throw new ServiceError(ERRORS.TENANT_ALREADY_SEEDED);
}
}
/**
* Retrieve tenant model by the given organization id or throw not found
* error if the tenant not exists on the storage.
* @param {string} organizationId
* @return {ITenant}
*/
private async getTenantByOrgIdOrThrowError(organizationId: string) {
const { tenantRepository } = this.sysRepositories;
const tenant = await tenantRepository.getByOrgId(organizationId);
this.throwIfTenantNotExists(tenant);
return tenant;
}
}

View File

@@ -1,10 +1,10 @@
import { Service, Container, Inject } from 'typedi';
import cryptoRandomString from 'crypto-random-string';
import { times } from 'lodash';
import { License } from "@/system/models";
import { ILicense } from '@/interfaces';
import LicenseMailMessages from '@/services/Payment/LicenseMailMessages';
import LicenseSMSMessages from '@/services/Payment/LicenseSMSMessages';
import { License } from "system/models";
import { ILicense } from 'interfaces';
import LicenseMailMessages from 'services/Payment/LicenseMailMessages';
import LicenseSMSMessages from 'services/Payment/LicenseSMSMessages';
@Service()
export default class LicenseService {
@@ -27,8 +27,6 @@ export default class LicenseService {
let licenseCode: string;
let repeat: boolean = true;
console.log(License);
while(repeat) {
licenseCode = cryptoRandomString({ length: 10, type: 'numeric' });
const foundLicenses = await License.query().where('license_code', licenseCode);

View File

@@ -13,7 +13,7 @@ export default class SubscriptionMailMessages {
const Logger = Container.get('logger');
const Mail = Container.get('mail');
const filePath = path.join(global.rootPath, 'views/mail/LicenseReceive.html');
const filePath = path.join(global.__root, 'views/mail/LicenseReceive.html');
const template = fs.readFileSync(filePath, 'utf8');
const rendered = Mustache.render(template, { licenseCode });

View File

@@ -1,9 +1,9 @@
import { License } from "@/system/models";
import PaymentMethod from '@/services/Payment/PaymentMethod';
import { Plan } from '@/system/models';
import { IPaymentMethod, ILicensePaymentModel } from '@/interfaces';
import { ILicensePaymentModel } from "@/interfaces";
import { PaymentInputInvalid, PaymentAmountInvalidWithPlan } from '@/exceptions';
import { License } from "system/models";
import PaymentMethod from 'services/Payment/PaymentMethod';
import { Plan } from 'system/models';
import { IPaymentMethod, ILicensePaymentModel } from 'interfaces';
import { ILicensePaymentModel } from "interfaces";
import { PaymentInputInvalid, PaymentAmountInvalidWithPlan } from 'exceptions';
export default class LicensePaymentMethod extends PaymentMethod implements IPaymentMethod {
/**

View File

@@ -1,5 +1,5 @@
import { Container, Inject } from 'typedi';
import SMSClient from '@/services/SMSClient';
import SMSClient from 'services/SMSClient';
export default class SubscriptionSMSMessages {
@Inject('SMSClient')

View File

@@ -1,5 +1,5 @@
import moment from 'moment';
import { IPaymentModel } from '@/interfaces';
import { IPaymentModel } from 'interfaces';
export default class PaymentMethod implements IPaymentModel {

View File

@@ -1,5 +1,5 @@
import { IPaymentMethod, IPaymentContext } from "@/interfaces";
import { Plan } from '@/system/models';
import { IPaymentMethod, IPaymentContext } from "interfaces";
import { Plan } from 'system/models';
export default class PaymentContext<PaymentModel> implements IPaymentContext{
paymentMethod: IPaymentMethod;

View File

@@ -1,14 +1,14 @@
import { Inject, Service } from 'typedi';
import { omit, sumBy } from 'lodash';
import moment from 'moment';
import { IBillPaymentOTD, IBillPayment } from '@/interfaces';
import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries';
import AccountsService from '@/services/Accounts/AccountsService';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import TenancyService from '@/services/Tenancy/TenancyService';
import { formatDateFields } from '@/utils';
import { IBillPaymentOTD, IBillPayment } from 'interfaces';
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import JournalPosterService from 'services/Sales/JournalPosterService';
import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
/**
* Bill payments service.

View File

@@ -1,16 +1,16 @@
import { omit, sumBy, pick } from 'lodash';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
import AccountsService from '@/services/Accounts/AccountsService';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import InventoryService from '@/services/Inventory/Inventory';
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
import TenancyService from '@/services/Tenancy/TenancyService';
import { formatDateFields } from '@/utils';
import{ IBillOTD, IBill, IItem } from '@/interfaces';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import AccountsService from 'services/Accounts/AccountsService';
import JournalPosterService from 'services/Sales/JournalPosterService';
import InventoryService from 'services/Inventory/Inventory';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
import{ IBillOTD, IBill, IItem } from 'interfaces';
/**
* Vendor bills services.

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import SMSClientInterface from '@/services/SMSClient/SMSClientInterfaces';
import config from '@/../config/config';
import SMSClientInterface from 'services/SMSClient/SMSClientInterfaces';
import config from 'config';
export default class EasySMSClient implements SMSClientInterface {
clientName: string = 'easysms';

View File

@@ -1,4 +1,4 @@
import SMSClientInterface from '@/services/SMSClient/SMSClientInterface';
import SMSClientInterface from 'services/SMSClient/SMSClientInterface';
export default class SMSAPI {
smsClient: SMSClientInterface;

View File

@@ -1,7 +1,7 @@
import { difference, omit } from 'lodash';
import { Service, Inject } from 'typedi';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ItemEntry } from '@/models';
import TenancyService from 'services/Tenancy/TenancyService';
import { ItemEntry } from 'models';
@Service()
export default class HasItemEntries {

View File

@@ -1,6 +1,6 @@
import { Service, Inject } from 'typedi';
import JournalPoster from '@/services/Accounting/JournalPoster';
import TenancyService from '@/services/Tenancy/TenancyService';
import JournalPoster from 'services/Accounting/JournalPoster';
import TenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class JournalPosterService {

View File

@@ -1,16 +1,16 @@
import { omit, sumBy, chain } from 'lodash';
import moment from 'moment';
import { Service, Inject } from 'typedi';
import { IPaymentReceiveOTD } from '@/interfaces';
import AccountsService from '@/services/Accounts/AccountsService';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries';
import PaymentReceiveEntryRepository from '@/repositories/PaymentReceiveEntryRepository';
import CustomerRepository from '@/repositories/CustomerRepository';
import TenancyService from '@/services/Tenancy/TenancyService';
import { formatDateFields } from '@/utils';
import { IPaymentReceiveOTD } from 'interfaces';
import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import JournalPosterService from 'services/Sales/JournalPosterService';
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository';
import CustomerRepository from 'repositories/CustomerRepository';
import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
/**
* Payment receive service.

View File

@@ -1,8 +1,8 @@
import { omit, difference, sumBy, mixin } from 'lodash';
import { Service, Inject } from 'typedi';
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
import { formatDateFields } from '@/utils';
import TenancyService from '@/services/Tenancy/TenancyService';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import { formatDateFields } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService';
/**
* Sale estimate service.

View File

@@ -1,12 +1,12 @@
import { Service, Inject } from 'typedi';
import { omit, sumBy, difference, pick, chain } from 'lodash';
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from '@/interfaces';
import JournalPoster from '@/services/Accounting/JournalPoster';
import HasItemsEntries from '@/services/Sales/HasItemsEntries';
import InventoryService from '@/services/Inventory/Inventory';
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
import TenancyService from '@/services/Tenancy/TenancyService';
import { formatDateFields } from '@/utils';
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from 'interfaces';
import JournalPoster from 'services/Accounting/JournalPoster';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import InventoryService from 'services/Inventory/Inventory';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
/**
* Sales invoices service

View File

@@ -1,9 +1,9 @@
import { Container, Service, Inject } from 'typedi';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
import InventoryService from '@/services/Inventory/Inventory';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ISaleInvoice, IItemEntry } from '@/interfaces';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import InventoryService from 'services/Inventory/Inventory';
import TenancyService from 'services/Tenancy/TenancyService';
import { ISaleInvoice, IItemEntry } from 'interfaces';
@Service()
export default class SaleInvoicesCost {

View File

@@ -1,9 +1,9 @@
import { omit, difference, sumBy } from 'lodash';
import { Service, Inject } from 'typedi';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import HasItemEntries from '@/services/Sales/HasItemsEntries';
import TenancyService from '@/services/Tenancy/TenancyService';
import { formatDateFields } from '@/utils';
import JournalPosterService from 'services/Sales/JournalPosterService';
import HasItemEntries from 'services/Sales/HasItemsEntries';
import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
@Service()
export default class SalesReceiptService {

View File

@@ -1,4 +1,4 @@
import SessionModel from '@/services/SessionModel';
import SessionModel from 'services/SessionModel';
export default class SessionQueryBuilder extends SessionModel.QueryBuilder {
/**

View File

@@ -1,4 +1,4 @@
import SessionQueryBuilder from '@/services/SessionModel/SessionQueryBuilder';
import SessionQueryBuilder from 'services/SessionModel/SessionQueryBuilder';
export default class SessionModel {
/**

View File

@@ -1,6 +1,6 @@
import Knex from 'knex';
import MetableStoreDB from '@/lib/Metable/MetableStoreDB';
import Setting from '@/models/Setting';
import MetableStoreDB from 'lib/Metable/MetableStoreDB';
import Setting from 'models/Setting';
export default class SettingsStore extends MetableStoreDB {
/**

View File

@@ -1,5 +1,5 @@
import { Service, Inject } from 'typedi';
import SMSClient from '@/services/SMSClient';
import SMSClient from 'services/SMSClient';
@Service()
export default class SubscriptionSMSMessages {

View File

@@ -1,8 +1,8 @@
import { Inject, Service } from 'typedi';
import { Tenant, Plan } from '@/system/models';
import { IPaymentContext } from '@/interfaces';
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
import { NoPaymentModelWithPricedPlan } from '@/exceptions';
import { Tenant, Plan } from 'system/models';
import { IPaymentContext } from 'interfaces';
import { NotAllowedChangeSubscriptionPlan } from 'exceptions';
import { NoPaymentModelWithPricedPlan } from 'exceptions';
export default class Subscription<PaymentModel> {
paymentContext: IPaymentContext|null;

View File

@@ -1,11 +1,11 @@
import { Service, Inject } from 'typedi';
import { Plan, Tenant } from '@/system/models';
import Subscription from '@/services/Subscription/Subscription';
import LicensePaymentMethod from '@/services/Payment/LicensePaymentMethod';
import PaymentContext from '@/services/Payment';
import SubscriptionSMSMessages from '@/services/Subscription/SMSMessages';
import SubscriptionMailMessages from '@/services/Subscription/MailMessages';
import { ILicensePaymentModel } from '@/interfaces';
import { Plan, Tenant } from 'system/models';
import Subscription from 'services/Subscription/Subscription';
import LicensePaymentMethod from 'services/Payment/LicensePaymentMethod';
import PaymentContext from 'services/Payment';
import SubscriptionSMSMessages from 'services/Subscription/SMSMessages';
import SubscriptionMailMessages from 'services/Subscription/MailMessages';
import { ILicensePaymentModel } from 'interfaces';
@Service()
export default class SubscriptionService {
@@ -18,6 +18,9 @@ export default class SubscriptionService {
@Inject('logger')
logger: any;
@Inject('repositories')
sysRepositories: any;
/**
* Handles the payment process via license code and than subscribe to
* the given tenant.
@@ -35,8 +38,10 @@ export default class SubscriptionService {
this.logger.info('[subscription_via_license] try to subscribe via given license.', {
tenantId, paymentModel
});
const { tenantRepository } = this.sysRepositories;
const plan = await Plan.query().findOne('slug', planSlug);
const tenant = await Tenant.query().findById(tenantId);
const tenant = await tenantRepository.getById(tenantId);
const paymentViaLicense = new LicensePaymentMethod();
const paymentContext = new PaymentContext(paymentViaLicense);

View File

@@ -0,0 +1,26 @@
import Container from "typedi"
import { Service, Inject } from 'typedi';
@Service()
export default class HasSystemService implements SystemService{
private container(key: string) {
return Container.get(key);
}
knex() {
return this.container('knex');
}
repositories() {
return this.container('repositories');
}
cache() {
return this.container('cache');
}
dbManager() {
return this.container('dbManager');
}
}

View File

@@ -1,22 +1,55 @@
import { Container, Service } from 'typedi';
import { Container, Service, Inject } from 'typedi';
import TenantsManagerService from 'services/Tenancy/TenantsManager';
import tenantModelsLoader from 'loaders/tenantModels';
import tenantRepositoriesLoader from 'loaders/tenantRepositories';
import tenantCacheLoader from 'loaders/tenantCache';
@Service()
export default class HasTenancyService {
@Inject()
tenantsManager: TenantsManagerService;
/**
* Retrieve the given tenant container.
* @param {number} tenantId
* @param {number} tenantId
* @return {Container}
*/
tenantContainer(tenantId: number) {
return Container.of(`tenant-${tenantId}`);
}
/**
* Singleton tenant service.
* @param {number} tenantId - Tenant id.
* @param {string} key - Service key.
* @param {Function} callback
*/
singletonService(tenantId: number, key: string, callback: Function) {
const container = this.tenantContainer(tenantId);
const Logger = Container.get('logger');
const hasServiceInstnace = container.has(key);
if (!hasServiceInstnace) {
const serviceInstance = callback();
container.set(key, serviceInstance);
Logger.info(`[tenant_DI] ${key} injected to tenant container.`, { tenantId, key });
return serviceInstance;
} else {
return container.get(key);
}
}
/**
* Retrieve knex instance of the given tenant id.
* @param {number} tenantId
*/
knex(tenantId: number) {
return this.tenantContainer(tenantId).get('knex');
return this.singletonService(tenantId, 'tenantManager', () => {
return this.tenantsManager.getKnexInstance(tenantId);
});
}
/**
@@ -24,30 +57,39 @@ export default class HasTenancyService {
* @param {number} tenantId - The tenant id.
*/
models(tenantId: number) {
return this.tenantContainer(tenantId).get('models');
const knexInstance = this.knex(tenantId);
return this.singletonService(tenantId, 'models', () => {
return tenantModelsLoader(knexInstance);
});
}
/**
* Retrieve repositories of the given tenant id.
* @param {number} tenantId
* @param {number} tenantId - Tenant id.
*/
repositories(tenantId: number) {
return this.tenantContainer(tenantId).get('repositories');
return this.singletonService(tenantId, 'repositories', () => {
return tenantRepositoriesLoader(tenantId);
})
}
/**
* Retrieve i18n locales methods.
* @param {number} tenantId
* @param {number} tenantId - Tenant id.
*/
i18n(tenantId: number) {
return this.tenantContainer(tenantId).get('i18n');
return this.singletonService(tenantId, 'i18n', () => {
});
}
/**
* Retrieve tenant cache instance.
* @param {number} tenantId -
* @param {number} tenantId - Tenant id.
*/
cache(tenantId: number) {
return this.tenantContainer(tenantId).get('cache');
return this.singletonService(tenantId, 'cache', () => {
return tenantCacheLoader(tenantId);
});
}
}

View File

@@ -0,0 +1,123 @@
import { Container } from 'typedi';
import Knex from 'knex';
import { knexSnakeCaseMappers } from 'objection';
import config from 'config';
import { ITenant, ITenantDBManager, ISystemService } from 'interfaces';
import SystemService from 'services/Tenancy/SystemService';
import { TenantDBAlreadyExists } from 'exceptions';
import { tenantKnexConfig, tenantSeedConfig } from 'config/knexConfig';
export default class TenantDBManager implements ITenantDBManager{
static knexCache: { [key: string]: Knex; } = {};
// System database manager.
dbManager: any;
// System knex instance.
sysKnex: Knex;
/**
* Constructor method.
* @param {ITenant} tenant
*/
constructor() {
const systemService = Container.get(SystemService);
this.dbManager = systemService.dbManager();
this.sysKnex = systemService.knex();
}
/**
* Retrieve the tenant database name.
* @return {string}
*/
private getDatabaseName(tenant: ITenant) {
return `${config.tenant.db_name_prefix}${tenant.organizationId}`;
}
/**
* Detarmines the tenant database weather exists.
* @return {Promise<boolean>}
*/
public async databaseExists(tenant: ITenant) {
const databaseName = this.getDatabaseName(tenant);
const results = await this.sysKnex
.raw('SELECT * FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "?"', databaseName);
return results[0].length > 0;
}
/**
* Creates a tenant database.
* @throws {TenantAlreadyInitialized}
* @return {Promise<void>}
*/
public async createDatabase(tenant: ITenant): Promise<void> {
await this.throwErrorIfTenantDBExists(tenant);
const databaseName = this.getDatabaseName(tenant);
await this.dbManager.createDb(databaseName);
}
/**
* Migrate tenant database schema to the latest version.
* @return {Promise<void>}
*/
public async migrate(tenant: ITenant): Promise<void> {
const knex = this.setupKnexInstance(tenant);
await knex.migrate.latest();
}
/**
* Seeds initial data to the tenant database.
* @return {Promise<void>}
*/
public async seed(tenant: ITenant): Promise<void> {
const knex = this.setupKnexInstance(tenant);
await knex.migrate.latest({
...tenantSeedConfig(tenant),
disableMigrationsListValidation: true,
});
}
/**
* Retrieve the knex instance of tenant.
* @return {Knex}
*/
public setupKnexInstance(tenant: ITenant) {
const key: string = `${tenant.id}`;
let knexInstance = TenantDBManager.knexCache[key];
if (!knexInstance) {
knexInstance = Knex({
...tenantKnexConfig(tenant),
...knexSnakeCaseMappers({ upperCase: true }),
});
TenantDBManager.knexCache[key] = knexInstance;
}
return knexInstance;
}
public getKnexInstance(tenantId: number) {
const key: string = `${tenantId}`;
let knexInstance = TenantDBManager.knexCache[key];
if (!knexInstance) {
throw new Error('Knex instance is not initialized yut.');
}
return knexInstance;
}
/**
* Throws error if the tenant database already exists.
* @return {Promise<void>}
*/
async throwErrorIfTenantDBExists(tenant: ITenant) {
const isExists = await this.databaseExists(tenant);
if (isExists) {
throw new TenantDBAlreadyExists();
}
}
}

View File

@@ -0,0 +1,158 @@
import { Container, Inject, Service } from 'typedi';
import { ServiceError } from 'exceptions';
import {
ITenantManager,
ITenant,
ITenantDBManager,
} from 'interfaces';
import {
EventDispatcherInterface,
EventDispatcher,
} from 'decorators/eventDispatcher';
import { TenantAlreadyInitialized, TenantAlreadySeeded, TenantDatabaseNotBuilt } from 'exceptions';
import TenantDBManager from 'services/Tenancy/TenantDBManager';
import events from 'subscribers/events';
const ERRORS = {
TENANT_ALREADY_CREATED: 'TENANT_ALREADY_CREATED',
TENANT_NOT_EXISTS: 'TENANT_NOT_EXISTS'
};
// Tenants manager service.
@Service()
export default class TenantsManagerService implements ITenantManager{
static instances: { [key: number]: ITenantManager } = {};
@EventDispatcher()
private eventDispatcher: EventDispatcherInterface;
@Inject('repositories')
private sysRepositories: any;
private tenantDBManager: ITenantDBManager;
/**
* Constructor method.
*/
constructor() {
this.tenantDBManager = new TenantDBManager();
}
/**
* Creates a new teant with unique organization id.
* @param {ITenant} tenant
* @return {Promise<ITenant>}
*/
public async createTenant(): Promise<ITenant> {
const { tenantRepository } = this.sysRepositories;
const tenant = await tenantRepository.newTenantWithUniqueOrgId();
return tenant;
}
/**
* Creates a new tenant database.
* @param {ITenant} tenant -
* @return {Promise<void>}
*/
public async createDatabase(tenant: ITenant): Promise<void> {
this.throwErrorIfTenantAlreadyInitialized(tenant);
await this.tenantDBManager.createDatabase(tenant);
this.eventDispatcher.dispatch(events.tenantManager.databaseCreated);
}
/**
* Detarmines the tenant has database.
* @param {ITenant} tenant
* @returns {Promise<boolean>}
*/
public async hasDatabase(tenant: ITenant): Promise<boolean> {
return this.tenantDBManager.databaseExists(tenant);
}
/**
* Migrates the tenant database.
* @param {ITenant} tenant
* @return {Promise<void>}
*/
public async migrateTenant(tenant: ITenant) {
this.throwErrorIfTenantAlreadyInitialized(tenant);
const { tenantRepository } = this.sysRepositories;
await this.tenantDBManager.migrate(tenant);
await tenantRepository.markAsInitialized(tenant.id);
this.eventDispatcher.dispatch(events.tenantManager.tenantMigrated, { tenant });
}
/**
* Seeds the tenant database.
* @param {ITenant} tenant
* @return {Promise<void>}
*/
public async seedTenant(tenant: ITenant) {
this.throwErrorIfTenantNotBuilt(tenant);
this.throwErrorIfTenantAlreadySeeded(tenant);
const { tenantRepository } = this.sysRepositories;
// Seed the tenant database.
await this.tenantDBManager.seed(tenant);
// Mark the tenant as seeded in specific date.
await tenantRepository.markAsSeeded(tenant.id);
this.eventDispatcher.dispatch(events.tenantManager.tenantSeeded);
}
/**
* Initialize knex instance or retrieve the instance of cache map.
* @param {ITenant} tenant
* @returns {Knex}
*/
public setupKnexInstance(tenant: ITenant) {
return this.tenantDBManager.setupKnexInstance(tenant);
}
/**
* Retrieve tenant knex instance or throw error in case was not initialized.
* @param {number} tenantId
* @returns {Knex}
*/
public getKnexInstance(tenantId: number) {
return this.tenantDBManager.getKnexInstance(tenantId);
}
/**
* Throws error if the tenant already seeded.
* @throws {TenantAlreadySeeded}
*/
private throwErrorIfTenantAlreadySeeded(tenant: ITenant) {
if (tenant.seededAt) {
throw new TenantAlreadySeeded();
}
}
/**
* Throws error if the tenant database is not built yut.
* @param tenant
*/
private throwErrorIfTenantNotBuilt(tenant: ITenant) {
if (!tenant.initializedAt) {
throw new TenantDatabaseNotBuilt();
}
}
/**
* Throws error if the tenant already migrated.
* @throws {TenantAlreadyInitialized}
*/
private throwErrorIfTenantAlreadyInitialized(tenant: ITenant) {
if (tenant.initializedAt) {
throw new TenantAlreadyInitialized();
}
}
}

View File

@@ -1,8 +1,9 @@
import { Inject, Service } from "typedi";
import TenancyService from '@/services/Tenancy/TenancyService';
import { SystemUser } from "@/system/models";
import { ServiceError, ServiceErrors } from "@/exceptions";
import { ISystemUser, ISystemUserDTO } from "@/interfaces";
import TenancyService from 'services/Tenancy/TenancyService';
import { SystemUser } from "system/models";
import { ServiceError, ServiceErrors } from "exceptions";
import { ISystemUser, ISystemUserDTO } from "interfaces";
import systemRepositories from "loaders/systemRepositories";
@Service()
export default class UsersService {
@@ -12,6 +13,9 @@ export default class UsersService {
@Inject('logger')
logger: any;
@Inject('repositories')
repositories: any;
/**
* Creates a new user.
* @param {number} tenantId
@@ -20,26 +24,17 @@ export default class UsersService {
* @return {Promise<ISystemUser>}
*/
async editUser(tenantId: number, userId: number, userDTO: ISystemUserDTO): Promise<ISystemUser> {
const foundUsers = await SystemUser.query()
.whereNot('id', userId)
.andWhere((query) => {
query.where('email', userDTO.email);
query.orWhere('phone_number', userDTO.phoneNumber);
})
.where('tenant_id', tenantId);
const { systemUserRepository } = this.repositories;
const sameUserEmail = foundUsers
.some((u: ISystemUser) => u.email === userDTO.email);
const samePhoneNumber = foundUsers
.some((u: ISystemUser) => u.phoneNumber === userDTO.phone_number);
const isEmailExists = await systemUserRepository.isEmailExists(userDTO.email, userId);
const isPhoneNumberExists = await systemUserRepository.isPhoneNumberExists(userDTO.phoneNumber, userId);
const serviceErrors: ServiceError[] = [];
if (sameUserEmail) {
if (isEmailExists) {
serviceErrors.push(new ServiceError('email_already_exists'));
}
if (samePhoneNumber) {
if (isPhoneNumberExists) {
serviceErrors.push(new ServiceError('phone_number_already_exist'));
}
if (serviceErrors.length > 0) {
@@ -47,9 +42,8 @@ export default class UsersService {
}
const updateSystemUser = await SystemUser.query()
.where('id', userId)
.update({
...userDTO,
});
.update({ ...userDTO });
return updateSystemUser;
}
@@ -60,9 +54,9 @@ export default class UsersService {
* @returns {ISystemUser}
*/
async getUserOrThrowError(tenantId: number, userId: number): void {
const user = await SystemUser.query().findOne({
id: userId, tenant_id: tenantId,
});
const { systemUserRepository } = this.repositories;
const user = await systemUserRepository.getByIdAndTenant(userId, tenantId);
if (!user) {
this.logger.info('[users] the given user not found.', { tenantId, userId });
throw new ServiceError('user_not_found');
@@ -76,11 +70,11 @@ export default class UsersService {
* @param {number} userId
*/
async deleteUser(tenantId: number, userId: number): Promise<void> {
const { systemUserRepository } = this.repositories;
await this.getUserOrThrowError(tenantId, userId);
this.logger.info('[users] trying to delete the given user.', { tenantId, userId });
await SystemUser.query().where('tenant_id', tenantId)
.where('id', userId).delete();
await systemUserRepository.deleteById(userId);
this.logger.info('[users] the given user deleted successfully.', { tenantId, userId });
}
@@ -91,12 +85,14 @@ export default class UsersService {
* @param {number} userId
*/
async activateUser(tenantId: number, userId: number): Promise<void> {
const { systemUserRepository } = this.repositories;
const user = await this.getUserOrThrowError(tenantId, userId);
this.throwErrorIfUserActive(user);
await SystemUser.query().findById(userId).update({ active: true });
await systemUserRepository.activateUser(userId);
}
/**
* Inactivate the given user id.
* @param {number} tenantId
@@ -104,10 +100,11 @@ export default class UsersService {
* @return {Promise<void>}
*/
async inactivateUser(tenantId: number, userId: number): Promise<void> {
const { systemUserRepository } = this.repositories;
const user = await this.getUserOrThrowError(tenantId, userId);
this.throwErrorIfUserInactive(user);
await SystemUser.query().findById(userId).update({ active: false });
await systemUserRepository.inactivateById(userId);
}
/**