fix: system repositories.

This commit is contained in:
a.bouhuolia
2020-12-17 17:19:16 +02:00
parent 7a847fc895
commit a67b1fbdd0
54 changed files with 1452 additions and 983 deletions

View File

@@ -1,9 +1,14 @@
import { Inject, Service } from 'typedi';
import { difference, chain, uniq } from 'lodash';
import { kebabCase } from 'lodash'
import { kebabCase } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
import { IAccountDTO, IAccount, IAccountsFilter, IFilterMeta } from 'interfaces';
import {
IAccountDTO,
IAccount,
IAccountsFilter,
IFilterMeta,
} from 'interfaces';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -27,15 +32,21 @@ export default class AccountsService {
/**
* Retrieve account type or throws service error.
* @param {number} tenantId -
* @param {number} accountTypeId -
* @return {IAccountType}
* @param {number} tenantId -
* @param {number} accountTypeId -
* @return {IAccountType}
*/
private async getAccountTypeOrThrowError(tenantId: number, accountTypeId: number) {
const { AccountType } = this.tenancy.models(tenantId);
private async getAccountTypeOrThrowError(
tenantId: number,
accountTypeId: number
) {
const { accountTypeRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[accounts] validating account type existance.', { tenantId, accountTypeId });
const accountType = await AccountType.query().findById(accountTypeId);
this.logger.info('[accounts] validating account type existance.', {
tenantId,
accountTypeId,
});
const accountType = await accountTypeRepository.findOneById(accountTypeId);
if (!accountType) {
this.logger.info('[accounts] account type not found.');
@@ -46,24 +57,34 @@ export default class AccountsService {
/**
* Retrieve parent account or throw service error.
* @param {number} tenantId
* @param {number} accountId
* @param {number} notAccountId
* @param {number} tenantId
* @param {number} accountId
* @param {number} notAccountId
*/
private async getParentAccountOrThrowError(tenantId: number, accountId: number, notAccountId?: number) {
private async getParentAccountOrThrowError(
tenantId: number,
accountId: number,
notAccountId?: number
) {
const { Account } = this.tenancy.models(tenantId);
this.logger.info('[accounts] validating parent account existance.', {
tenantId, accountId, notAccountId,
tenantId,
accountId,
notAccountId,
});
const parentAccount = await Account.query().findById(accountId)
const parentAccount = await Account.query()
.findById(accountId)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
});
if (!parentAccount) {
this.logger.info('[accounts] parent account not found.', { tenantId, accountId });
this.logger.info('[accounts] parent account not found.', {
tenantId,
accountId,
});
throw new ServiceError('parent_account_not_found');
}
return parentAccount;
@@ -71,17 +92,27 @@ export default class AccountsService {
/**
* Throws error if the account type was not unique on the storage.
* @param {number} tenantId
* @param {string} accountCode
* @param {number} notAccountId
* @param {number} tenantId
* @param {string} accountCode
* @param {number} notAccountId
*/
private async isAccountCodeUniqueOrThrowError(tenantId: number, accountCode: string, notAccountId?: number) {
private async isAccountCodeUniqueOrThrowError(
tenantId: number,
accountCode: string,
notAccountId?: number
) {
const { Account } = this.tenancy.models(tenantId);
this.logger.info('[accounts] validating the account code unique on the storage.', {
tenantId, accountCode, notAccountId,
});
const account = await Account.query().where('code', accountCode)
this.logger.info(
'[accounts] validating the account code unique on the storage.',
{
tenantId,
accountCode,
notAccountId,
}
);
const account = await Account.query()
.where('code', accountCode)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
@@ -89,17 +120,23 @@ export default class AccountsService {
});
if (account.length > 0) {
this.logger.info('[accounts] account code is not unique.', { tenantId, accountCode });
this.logger.info('[accounts] account code is not unique.', {
tenantId,
accountCode,
});
throw new ServiceError('account_code_not_unique');
}
}
/**
* Throws service error if parent account has different type.
* @param {IAccountDTO} accountDTO
* @param {IAccount} parentAccount
* @param {IAccountDTO} accountDTO
* @param {IAccount} parentAccount
*/
private throwErrorIfParentHasDiffType(accountDTO: IAccountDTO, parentAccount: IAccount) {
private throwErrorIfParentHasDiffType(
accountDTO: IAccountDTO,
parentAccount: IAccount
) {
if (accountDTO.accountTypeId !== parentAccount.accountTypeId) {
throw new ServiceError('parent_has_different_type');
}
@@ -107,18 +144,23 @@ export default class AccountsService {
/**
* Retrieve account of throw service error in case account not found.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
* @return {IAccount}
*/
private async getAccountOrThrowError(tenantId: number, accountId: number) {
const { accountRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[accounts] validating the account existance.', { tenantId, accountId });
this.logger.info('[accounts] validating the account existance.', {
tenantId,
accountId,
});
const account = await accountRepository.findOneById(accountId);
if (!account) {
this.logger.info('[accounts] the given account not found.', { accountId });
this.logger.info('[accounts] the given account not found.', {
accountId,
});
throw new ServiceError('account_not_found');
}
return account;
@@ -127,13 +169,13 @@ export default class AccountsService {
/**
* Diff account type between new and old account, throw service error
* if they have different account type.
*
*
* @param {IAccount|IAccountDTO} oldAccount
* @param {IAccount|IAccountDTO} newAccount
*/
private async isAccountTypeChangedOrThrowError(
oldAccount: IAccount|IAccountDTO,
newAccount: IAccount|IAccountDTO,
oldAccount: IAccount | IAccountDTO,
newAccount: IAccount | IAccountDTO
) {
if (oldAccount.accountTypeId !== newAccount.accountTypeId) {
throw new ServiceError('account_type_not_allowed_to_changed');
@@ -142,19 +184,29 @@ export default class AccountsService {
/**
* Validates the account name uniquiness.
* @param {number} tenantId
* @param {string} accountName
* @param {number} tenantId
* @param {string} accountName
* @param {number} notAccountId - Ignore the account id.
*/
private async validateAccountNameUniquiness(tenantId: number, accountName: string, notAccountId?: number) {
private async validateAccountNameUniquiness(
tenantId: number,
accountName: string,
notAccountId?: number
) {
const { Account } = this.tenancy.models(tenantId);
this.logger.info('[accounts] validating account name uniquiness.', { tenantId, accountName, notAccountId });
const foundAccount = await Account.query().findOne('name', accountName).onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
this.logger.info('[accounts] validating account name uniquiness.', {
tenantId,
accountName,
notAccountId,
});
const foundAccount = await Account.query()
.findOne('name', accountName)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
});
if (foundAccount) {
throw new ServiceError('account_name_not_unqiue');
}
@@ -162,9 +214,9 @@ export default class AccountsService {
/**
* Creates a new account on the storage.
* @param {number} tenantId
* @param {IAccount} accountDTO
* @returns {IAccount}
* @param {number} tenantId
* @param {IAccount} accountDTO
* @returns {IAccount}
*/
public async newAccount(tenantId: number, accountDTO: IAccountDTO) {
const { accountRepository } = this.tenancy.repositories(tenantId);
@@ -180,7 +232,8 @@ export default class AccountsService {
if (accountDTO.parentAccountId) {
const parentAccount = await this.getParentAccountOrThrowError(
tenantId, accountDTO.parentAccountId
tenantId,
accountDTO.parentAccountId
);
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
@@ -191,7 +244,10 @@ export default class AccountsService {
...accountDTO,
slug: kebabCase(accountDTO.name),
});
this.logger.info('[account] account created successfully.', { account, accountDTO });
this.logger.info('[account] account created successfully.', {
account,
accountDTO,
});
// Triggers `onAccountCreated` event.
this.eventDispatcher.dispatch(events.accounts.onCreated);
@@ -201,21 +257,31 @@ export default class AccountsService {
/**
* Edits details of the given account.
* @param {number} tenantId
* @param {number} accountId
* @param {IAccountDTO} accountDTO
* @param {number} tenantId
* @param {number} accountId
* @param {IAccountDTO} accountDTO
*/
public async editAccount(tenantId: number, accountId: number, accountDTO: IAccountDTO) {
this.logger.info('[account] trying to edit account.', { tenantId, accountId });
public async editAccount(
tenantId: number,
accountId: number,
accountDTO: IAccountDTO
) {
this.logger.info('[account] trying to edit account.', {
tenantId,
accountId,
});
const { accountRepository } = this.tenancy.repositories(tenantId);
const oldAccount = await this.getAccountOrThrowError(tenantId, accountId);
// Validate account name uniquiness.
await this.validateAccountNameUniquiness(tenantId, accountDTO.name, accountId);
await this.validateAccountNameUniquiness(
tenantId,
accountDTO.name,
accountId
);
await this.isAccountTypeChangedOrThrowError(oldAccount, accountDTO);
// Validate the account code not exists on the storage.
if (accountDTO.code && accountDTO.code !== oldAccount.code) {
await this.isAccountCodeUniqueOrThrowError(
@@ -226,17 +292,21 @@ export default class AccountsService {
}
if (accountDTO.parentAccountId) {
const parentAccount = await this.getParentAccountOrThrowError(
tenantId, accountDTO.parentAccountId, oldAccount.id,
tenantId,
accountDTO.parentAccountId,
oldAccount.id
);
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
}
// Update the account on the storage.
const account = await accountRepository.updateAndFetch({
id: oldAccount.id,
...accountDTO
});
const account = await accountRepository.update(
{ ...accountDTO, },
{ id: oldAccount.id }
);
this.logger.info('[account] account edited successfully.', {
account, accountDTO, tenantId
account,
accountDTO,
tenantId,
});
// Triggers `onAccountEdited` event.
this.eventDispatcher.dispatch(events.accounts.onEdited);
@@ -246,8 +316,8 @@ export default class AccountsService {
/**
* Retrieve the given account details.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
*/
public async getAccount(tenantId: number, accountId: number) {
return this.getAccountOrThrowError(tenantId, accountId);
@@ -255,22 +325,24 @@ export default class AccountsService {
/**
* Detarmine if the given account id exists on the storage.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
*/
public async isAccountExists(tenantId: number, accountId: number) {
const { Account } = this.tenancy.models(tenantId);
this.logger.info('[account] validating the account existance.', { tenantId, accountId });
const foundAccounts = await Account.query()
.where('id', accountId);
this.logger.info('[account] validating the account existance.', {
tenantId,
accountId,
});
const foundAccounts = await Account.query().where('id', accountId);
return foundAccounts.length > 0;
}
/**
* Throws error if the account was prefined.
* @param {IAccount} account
* @param {IAccount} account
*/
private throwErrorIfAccountPredefined(account: IAccount) {
if (account.predefined) {
@@ -281,14 +353,16 @@ export default class AccountsService {
/**
* Unlink the given parent account with children accounts.
* @param {number} tenantId -
* @param {number|number[]} parentAccountId -
* @param {number|number[]} parentAccountId -
*/
private async unassociateChildrenAccountsFromParent(
tenantId: number,
parentAccountId: number | number[],
parentAccountId: number | number[]
) {
const { Account } = this.tenancy.models(tenantId);
const accountsIds = Array.isArray(parentAccountId) ? parentAccountId : [parentAccountId];
const accountsIds = Array.isArray(parentAccountId)
? parentAccountId
: [parentAccountId];
await Account.query()
.whereIn('parent_account_id', accountsIds)
@@ -297,13 +371,17 @@ export default class AccountsService {
/**
* Throws service error if the account has associated transactions.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
*/
private async throwErrorIfAccountHasTransactions(tenantId: number, accountId: number) {
private async throwErrorIfAccountHasTransactions(
tenantId: number,
accountId: number
) {
const { AccountTransaction } = this.tenancy.models(tenantId);
const accountTransactions = await AccountTransaction.query().where(
'account_id', accountId,
'account_id',
accountId
);
if (accountTransactions.length > 0) {
throw new ServiceError('account_has_associated_transactions');
@@ -312,8 +390,8 @@ export default class AccountsService {
/**
* Deletes the account from the storage.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
*/
public async deleteAccount(tenantId: number, accountId: number) {
const { accountRepository } = this.tenancy.repositories(tenantId);
@@ -330,7 +408,8 @@ export default class AccountsService {
await accountRepository.deleteById(account.id);
this.logger.info('[account] account has been deleted successfully.', {
tenantId, accountId,
tenantId,
accountId,
});
// Triggers `onAccountDeleted` event.
@@ -339,8 +418,8 @@ export default class AccountsService {
/**
* Retrieve the given accounts details or throw error if one account not exists.
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {number} tenantId
* @param {number[]} accountsIds
* @return {IAccount[]}
*/
public async getAccountsOrThrowError(
@@ -349,13 +428,19 @@ export default class AccountsService {
): Promise<IAccount[]> {
const { Account } = this.tenancy.models(tenantId);
this.logger.info('[account] trying to validate accounts not exist.', { tenantId, accountsIds });
this.logger.info('[account] trying to validate accounts not exist.', {
tenantId,
accountsIds,
});
const storedAccounts = await Account.query().whereIn('id', accountsIds);
const storedAccountsIds = storedAccounts.map((account) => account.id);
const notFoundAccounts = difference(accountsIds, storedAccountsIds);
if (notFoundAccounts.length > 0) {
this.logger.error('[account] accounts not exists on the storage.', { tenantId, notFoundAccounts });
this.logger.error('[account] accounts not exists on the storage.', {
tenantId,
notFoundAccounts,
});
throw new ServiceError('accounts_not_found');
}
return storedAccounts;
@@ -367,7 +452,9 @@ export default class AccountsService {
* @return {IAccount[]} - Predefined accounts
*/
private validatePrefinedAccounts(accounts: IAccount[]) {
const predefined = accounts.filter((account: IAccount) => account.predefined);
const predefined = accounts.filter(
(account: IAccount) => account.predefined
);
if (predefined.length > 0) {
this.logger.error('[accounts] some accounts predefined.', { predefined });
@@ -378,10 +465,13 @@ export default class AccountsService {
/**
* Validating the accounts have associated transactions.
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {number} tenantId
* @param {number[]} accountsIds
*/
private async validateAccountsHaveTransactions(tenantId: number, accountsIds: number[]) {
private async validateAccountsHaveTransactions(
tenantId: number,
accountsIds: number[]
) {
const { AccountTransaction } = this.tenancy.models(tenantId);
const accountsTransactions = await AccountTransaction.query()
.whereIn('account_id', accountsIds)
@@ -403,11 +493,11 @@ export default class AccountsService {
/**
* Deletes the given accounts in bulk.
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {number} tenantId
* @param {number[]} accountsIds
*/
public async deleteAccounts(tenantId: number, accountsIds: number[]) {
const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.models(tenantId);
const accounts = await this.getAccountsOrThrowError(tenantId, accountsIds);
// Validate the accounts are not predefined.
@@ -420,10 +510,11 @@ export default class AccountsService {
await this.unassociateChildrenAccountsFromParent(tenantId, accountsIds);
// Delete the accounts in one query.
await Account.query().whereIn('id', accountsIds).delete();
await accountRepository.deleteWhereIdIn(accountsIds);
this.logger.info('[account] given accounts deleted in bulk successfully.', {
tenantId, accountsIds
tenantId,
accountsIds,
});
// Triggers `onBulkDeleted` event.
this.eventDispatcher.dispatch(events.accounts.onBulkDeleted);
@@ -431,12 +522,15 @@ export default class AccountsService {
/**
* Activate accounts in bulk.
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {boolean} activate
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {boolean} activate
*/
public async activateAccounts(tenantId: number, accountsIds: number[], activate: boolean = true) {
const { Account } = this.tenancy.models(tenantId);
public async activateAccounts(
tenantId: number,
accountsIds: number[],
activate: boolean = true
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given account or throw not found.
@@ -445,32 +539,41 @@ export default class AccountsService {
// Get all children accounts.
const accountsGraph = await accountRepository.getDependencyGraph();
const dependenciesAccounts = chain(accountsIds)
.map(accountId => accountsGraph.dependenciesOf(accountId))
.map((accountId) => accountsGraph.dependenciesOf(accountId))
.flatten()
.value();
// The children and parent accounts.
const patchAccountsIds = uniq([...dependenciesAccounts, accountsIds]);
this.logger.info('[account] trying activate/inactive the given accounts ids.', { accountsIds });
await Account.query().whereIn('id', patchAccountsIds)
.patch({
active: activate ? 1 : 0,
});
this.logger.info('[account] accounts have been activated successfully.', { tenantId, accountsIds });
this.logger.info(
'[account] trying activate/inactive the given accounts ids.',
{ accountsIds }
);
// Activate or inactivate the given accounts ids in bulk.
(activate) ?
await accountRepository.activateByIds(patchAccountsIds) :
await accountRepository.inactivateByIds(patchAccountsIds);
this.logger.info('[account] accounts have been activated successfully.', {
tenantId,
accountsIds,
});
// Triggers `onAccountBulkActivated` event.
this.eventDispatcher.dispatch(events.accounts.onActivated);
}
/**
* Activates/Inactivates the given account.
* @param {number} tenantId
* @param {number} accountId
* @param {boolean} activate
* @param {number} tenantId
* @param {number} accountId
* @param {boolean} activate
*/
public async activateAccount(tenantId: number, accountId: number, activate?: boolean) {
const { Account } = this.tenancy.models(tenantId);
public async activateAccount(
tenantId: number,
accountId: number,
activate?: boolean
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given account or throw not found error.
@@ -480,34 +583,44 @@ export default class AccountsService {
const accountsGraph = await accountRepository.getDependencyGraph();
const dependenciesAccounts = accountsGraph.dependenciesOf(accountId);
this.logger.info('[account] trying to activate/inactivate the given account id.');
await Account.query()
.whereIn('id', [...dependenciesAccounts, accountId])
.patch({
active: activate ? 1 : 0,
})
this.logger.info(
'[account] trying to activate/inactivate the given account id.'
);
const patchAccountsIds = [...dependenciesAccounts, accountId];
// Activate and inactivate the given accounts ids.
(activate) ?
await accountRepository.activateByIds(patchAccountsIds) :
await accountRepository.inactivateByIds(patchAccountsIds);
this.logger.info('[account] account have been activated successfully.', {
tenantId,
accountId
accountId,
});
// Triggers `onAccountActivated` event.
this.eventDispatcher.dispatch(events.accounts.onActivated);
}
/**
/**
* Retrieve accounts datatable list.
* @param {number} tenantId
* @param {IAccountsFilter} accountsFilter
* @param {number} tenantId
* @param {IAccountsFilter} accountsFilter
*/
public async getAccountsList(
tenantId: number,
filter: IAccountsFilter,
): Promise<{ accounts: IAccount[], filterMeta: IFilterMeta }> {
filter: IAccountsFilter
): Promise<{ accounts: IAccount[]; filterMeta: IFilterMeta }> {
const { Account } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter);
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Account,
filter
);
this.logger.info('[accounts] trying to get accounts datatable list.', { tenantId, filter });
this.logger.info('[accounts] trying to get accounts datatable list.', {
tenantId,
filter,
});
const accounts = await Account.query().onBuild((builder) => {
builder.withGraphFetched('type');
dynamicList.buildQuery()(builder);
@@ -527,7 +640,7 @@ export default class AccountsService {
* - Transfer the given account transactions to another account with the same root type.
* - Delete the given account.
* -------
* @param {number} tenantId -
* @param {number} tenantId -
* @param {number} accountId -
* @param {number} toAccountId -
* @param {boolean} deleteAfterClosing -
@@ -536,32 +649,47 @@ export default class AccountsService {
tenantId: number,
accountId: number,
toAccountId: number,
deleteAfterClosing: boolean,
deleteAfterClosing: boolean
) {
this.logger.info('[account] trying to close account.', { tenantId, accountId, toAccountId, deleteAfterClosing });
this.logger.info('[account] trying to close account.', {
tenantId,
accountId,
toAccountId,
deleteAfterClosing,
});
const { AccountTransaction } = this.tenancy.models(tenantId);
const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
const {
accountTypeRepository,
accountRepository,
} = this.tenancy.repositories(tenantId);
const account = await this.getAccountOrThrowError(tenantId, accountId);
const toAccount = await this.getAccountOrThrowError(tenantId, toAccountId);
this.throwErrorIfAccountPredefined(account);
const accountType = await accountTypeRepository.findOneById(account.accountTypeId);
const toAccountType = await accountTypeRepository.findOneById(toAccount.accountTypeId);
const accountType = await accountTypeRepository.findOneById(
account.accountTypeId
);
const toAccountType = await accountTypeRepository.findOneById(
toAccount.accountTypeId
);
if (accountType.rootType !== toAccountType.rootType) {
throw new ServiceError('close_account_and_to_account_not_same_type');
}
const updateAccountBalanceOper = await accountRepository.balanceChange(accountId, account.balance || 0);
const updateAccountBalanceOper = await accountRepository.balanceChange(
accountId,
account.balance || 0
);
// Move transactiosn operation.
const moveTransactionsOper = await AccountTransaction.query()
.where('account_id', accountId)
.patch({ accountId: toAccountId });
await Promise.all([ moveTransactionsOper, updateAccountBalanceOper ]);
await Promise.all([moveTransactionsOper, updateAccountBalanceOper]);
if (deleteAfterClosing) {
await accountRepository.deleteById(accountId);

View File

@@ -4,17 +4,17 @@ import TenancyService from 'services/Tenancy/TenancyService';
import { IAccountsTypesService, IAccountType } from 'interfaces';
@Service()
export default class AccountsTypesService implements IAccountsTypesService{
export default class AccountsTypesService implements IAccountsTypesService {
@Inject()
tenancy: TenancyService;
/**
* Retrieve all accounts types.
* @param {number} tenantId -
* @param {number} tenantId -
* @return {Promise<IAccountType>}
*/
async getAccountsTypes(tenantId: number): Promise<IAccountType> {
const { accountTypeRepository } = this.tenancy.repositories(tenantId);
return accountTypeRepository.all();
}
}
}

View File

@@ -1,7 +1,7 @@
import { Service, Inject, Container } from 'typedi';
import JWT from 'jsonwebtoken';
import uniqid from 'uniqid';
import { omit } from 'lodash';
import { omit, cloneDeep } from 'lodash';
import moment from 'moment';
import {
EventDispatcher,
@@ -62,7 +62,6 @@ export default class AuthenticationService implements IAuthenticationService {
emailOrPhone,
password,
});
const { systemUserRepository } = this.sysRepositories;
const loginThrottler = Container.get('rateLimiter.login');
@@ -108,10 +107,13 @@ export default class AuthenticationService implements IAuthenticationService {
});
const tenant = await user.$relatedQuery('tenant');
// Remove password property from user object.
Reflect.deleteProperty(user, 'password');
// Keep the user object immutable
const outputUser = cloneDeep(user);
return { user, token, tenant };
// Remove password property from user object.
Reflect.deleteProperty(outputUser, 'password');
return { user: outputUser, token, tenant };
}
/**
@@ -121,10 +123,10 @@ export default class AuthenticationService implements IAuthenticationService {
*/
private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) {
const { systemUserRepository } = this.sysRepositories;
const isEmailExists = await systemUserRepository.getByEmail(
const isEmailExists = await systemUserRepository.findOneByEmail(
registerDTO.email
);
const isPhoneExists = await systemUserRepository.getByPhoneNumber(
const isPhoneExists = await systemUserRepository.findOneByPhoneNumber(
registerDTO.phoneNumber
);
@@ -190,7 +192,7 @@ export default class AuthenticationService implements IAuthenticationService {
*/
private async validateEmailExistance(email: string): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
const userByEmail = await systemUserRepository.getByEmail(email);
const userByEmail = await systemUserRepository.findOneByEmail(email);
if (!userByEmail) {
this.logger.info('[send_reset_password] The given email not found.');
@@ -255,7 +257,7 @@ export default class AuthenticationService implements IAuthenticationService {
await this.deletePasswordResetToken(tokenModel.email);
throw new ServiceError('token_expired');
}
const user = await systemUserRepository.getByEmail(tokenModel.email);
const user = await systemUserRepository.findOneByEmail(tokenModel.email);
if (!user) {
throw new ServiceError('user_not_found');

View File

@@ -1,13 +1,9 @@
import { Inject, Service } from 'typedi';
import { difference, upperFirst, omit } from 'lodash';
import moment from 'moment';
import { ServiceError } from "exceptions";
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
import {
IContact,
IContactNewDTO,
IContactEditDTO,
} from "interfaces";
import { IContact, IContactNewDTO, IContactEditDTO } from 'interfaces';
import JournalPoster from '../Accounting/JournalPoster';
type TContactService = 'customer' | 'vendor';
@@ -26,8 +22,8 @@ export default class ContactsService {
/**
* Get the given contact or throw not found contact.
* @param {number} tenantId
* @param {number} contactId
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @return {Promise<IContact>}
*/
@@ -38,10 +34,13 @@ export default class ContactsService {
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[contact] trying to validate contact existance.', { tenantId, contactId });
this.logger.info('[contact] trying to validate contact existance.', {
tenantId,
contactId,
});
const contact = await contactRepository.findOne({
id: contactId,
...(contactService) && ({ contactService }),
...(contactService && { contactService }),
});
if (!contact) {
@@ -52,13 +51,15 @@ export default class ContactsService {
/**
* Converts contact DTO object to model object attributes to insert or update.
* @param {IContactNewDTO | IContactEditDTO} contactDTO
* @param {IContactNewDTO | IContactEditDTO} contactDTO
*/
private transformContactObj(contactDTO: IContactNewDTO | IContactEditDTO) {
return {
...omit(contactDTO, [
'billingAddress1', 'billingAddress2',
'shippingAddress1', 'shippingAddress2',
'billingAddress1',
'billingAddress2',
'shippingAddress1',
'shippingAddress2',
]),
billing_address_1: contactDTO?.billingAddress1,
billing_address_2: contactDTO?.billingAddress2,
@@ -69,54 +70,87 @@ export default class ContactsService {
/**
* Creates a new contact on the storage.
* @param {number} tenantId
* @param {number} tenantId
* @param {TContactService} contactService
* @param {IContactDTO} contactDTO
* @param {IContactDTO} contactDTO
*/
async newContact(
tenantId: number,
contactDTO: IContactNewDTO,
contactService: TContactService,
contactService: TContactService
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contactObj = this.transformContactObj(contactDTO);
this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, contactDTO });
const contact = await contactRepository.create({ contactService, ...contactObj });
this.logger.info('[contacts] trying to insert contact to the storage.', {
tenantId,
contactDTO,
});
const contact = await contactRepository.create({
contactService,
...contactObj,
});
this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact });
this.logger.info('[contacts] contact inserted successfully.', {
tenantId,
contact,
});
return contact;
}
/**
* Edit details of the given on the storage.
* @param {number} tenantId
* @param {number} contactId
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @param {IContactDTO} contactDTO
* @param {IContactDTO} contactDTO
*/
async editContact(tenantId: number, contactId: number, contactDTO: IContactEditDTO, contactService: TContactService) {
async editContact(
tenantId: number,
contactId: number,
contactDTO: IContactEditDTO,
contactService: TContactService
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contactObj = this.transformContactObj(contactDTO);
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
const contact = await this.getContactByIdOrThrowError(
tenantId,
contactId,
contactService
);
this.logger.info('[contacts] trying to edit the given contact details.', { tenantId, contactId, contactDTO });
this.logger.info('[contacts] trying to edit the given contact details.', {
tenantId,
contactId,
contactDTO,
});
await contactRepository.update({ ...contactObj }, { id: contactId });
}
/**
* Deletes the given contact from the storage.
* @param {number} tenantId
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @param {TContactService} contactService
* @return {Promise<void>}
*/
async deleteContact(tenantId: number, contactId: number, contactService: TContactService) {
async deleteContact(
tenantId: number,
contactId: number,
contactService: TContactService
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
const contact = await this.getContactByIdOrThrowError(
tenantId,
contactId,
contactService
);
this.logger.info('[contacts] trying to delete the given contact.', { tenantId, contactId });
this.logger.info('[contacts] trying to delete the given contact.', {
tenantId,
contactId,
});
// Deletes contact of the given id.
await contactRepository.deleteById(contactId);
@@ -124,26 +158,36 @@ export default class ContactsService {
/**
* Get contact details of the given contact id.
* @param {number} tenantId
* @param {number} contactId
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @returns {Promise<IContact>}
*/
async getContact(tenantId: number, contactId: number, contactService: TContactService) {
async getContact(
tenantId: number,
contactId: number,
contactService: TContactService
) {
return this.getContactByIdOrThrowError(tenantId, contactId, contactService);
}
/**
* Retrieve contacts or throw not found error if one of ids were not found
* on the storage.
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
* @return {Promise<IContact>}
*/
async getContactsOrThrowErrorNotFound(tenantId: number, contactsIds: number[], contactService: TContactService) {
async getContactsOrThrowErrorNotFound(
tenantId: number,
contactsIds: number[],
contactService: TContactService
) {
const { Contact } = this.tenancy.models(tenantId);
const contacts = await Contact.query().whereIn('id', contactsIds).where('contact_service', contactService);
const contacts = await Contact.query()
.whereIn('id', contactsIds)
.where('contact_service', contactService);
const storedContactsIds = contacts.map((contact: IContact) => contact.id);
const notFoundCustomers = difference(contactsIds, storedContactsIds);
@@ -156,12 +200,16 @@ export default class ContactsService {
/**
* Deletes the given contacts in bulk.
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
* @return {Promise<void>}
*/
async deleteBulkContacts(tenantId: number, contactsIds: number[], contactService: TContactService) {
async deleteBulkContacts(
tenantId: number,
contactsIds: number[],
contactService: TContactService
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService);
@@ -170,9 +218,9 @@ export default class ContactsService {
/**
* Reverts journal entries of the given contacts.
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
*/
async revertJEntriesContactsOpeningBalance(
tenantId: number,
@@ -189,17 +237,14 @@ export default class ContactsService {
journal.loadEntries(contactsTransactions);
journal.removeEntries();
await Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
]);
await Promise.all([journal.saveBalance(), journal.deleteEntries()]);
}
/**
* Chanages the opening balance of the given contact.
* @param {number} tenantId
* @param {number} tenantId
* @param {number} contactId
* @param {ICustomerChangeOpeningBalanceDTO} changeOpeningBalance
* @param {ICustomerChangeOpeningBalanceDTO} changeOpeningBalance
* @return {Promise<void>}
*/
public async changeOpeningBalance(
@@ -207,27 +252,34 @@ export default class ContactsService {
contactId: number,
contactService: string,
openingBalance: number,
openingBalanceAt?: Date|string,
openingBalanceAt?: Date | string
): Promise<void> {
const { contactRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given contact details or throw not found service error.
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
const contact = await this.getContactByIdOrThrowError(
tenantId,
contactId,
contactService
);
// Should the opening balance date be required.
if (!contact.openingBalanceAt && !openingBalanceAt) {
throw new ServiceError(ERRORS.OPENING_BALANCE_DATE_REQUIRED);
};
}
// Changes the customer the opening balance and opening balance date.
await contactRepository.update({
openingBalance: openingBalance,
await contactRepository.update(
{
openingBalance: openingBalance,
...(openingBalanceAt) && ({
openingBalanceAt: moment(openingBalanceAt).toMySqlDateTime(),
}),
}, {
id: contactId,
contactService,
});
...(openingBalanceAt && {
openingBalanceAt: moment(openingBalanceAt).toMySqlDateTime(),
}),
},
{
id: contactId,
contactService,
}
);
}
}
}

View File

@@ -181,11 +181,10 @@ export default class CustomersService {
pagination: IPaginationMeta,
filterMeta: IFilterMeta,
}> {
const { Contact } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Contact, customersFilter);
const { Customer } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Customer, customersFilter);
const { results, pagination } = await Contact.query().onBuild((query) => {
query.modify('customer');
const { results, pagination } = await Customer.query().onBuild((query) => {
dynamicList.buildQuery()(query);
}).pagination(
customersFilter.page - 1,

View File

@@ -4,17 +4,17 @@ import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import JournalPoster from "services/Accounting/JournalPoster";
import JournalCommands from "services/Accounting/JournalCommands";
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands';
import ContactsService from 'services/Contacts/ContactsService';
import {
import {
IVendorNewDTO,
IVendorEditDTO,
IVendor,
IVendorsFilter,
IPaginationMeta,
IFilterMeta
} from 'interfaces';
IFilterMeta,
} from 'interfaces';
import { ServiceError } from 'exceptions';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
@@ -39,10 +39,10 @@ export default class VendorsService {
/**
* Converts vendor to contact DTO.
* @param {IVendorNewDTO|IVendorEditDTO} vendorDTO
* @param {IVendorNewDTO|IVendorEditDTO} vendorDTO
* @returns {IContactDTO}
*/
private vendorToContactDTO(vendorDTO: IVendorNewDTO|IVendorEditDTO) {
private vendorToContactDTO(vendorDTO: IVendorNewDTO | IVendorEditDTO) {
return {
...vendorDTO,
active: defaultTo(vendorDTO.active, true),
@@ -51,31 +51,49 @@ export default class VendorsService {
/**
* Creates a new vendor.
* @param {number} tenantId
* @param {IVendorNewDTO} vendorDTO
* @param {number} tenantId
* @param {IVendorNewDTO} vendorDTO
* @return {Promise<void>}
*/
public async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) {
this.logger.info('[vendor] trying create a new vendor.', { tenantId, vendorDTO });
this.logger.info('[vendor] trying create a new vendor.', {
tenantId,
vendorDTO,
});
const contactDTO = this.vendorToContactDTO(vendorDTO);
const vendor = await this.contactService.newContact(tenantId, contactDTO, 'vendor');
const vendor = await this.contactService.newContact(
tenantId,
contactDTO,
'vendor'
);
// Triggers `onVendorCreated` event.
await this.eventDispatcher.dispatch(events.vendors.onCreated, {
tenantId, vendorId: vendor.id, vendor,
tenantId,
vendorId: vendor.id,
vendor,
});
return vendor;
}
/**
* Edits details of the given vendor.
* @param {number} tenantId
* @param {IVendorEditDTO} vendorDTO
* @param {number} tenantId
* @param {IVendorEditDTO} vendorDTO
*/
public async editVendor(tenantId: number, vendorId: number, vendorDTO: IVendorEditDTO) {
public async editVendor(
tenantId: number,
vendorId: number,
vendorDTO: IVendorEditDTO
) {
const contactDTO = this.vendorToContactDTO(vendorDTO);
const vendor = await this.contactService.editContact(tenantId, vendorId, contactDTO, 'vendor');
const vendor = await this.contactService.editContact(
tenantId,
vendorId,
contactDTO,
'vendor'
);
await this.eventDispatcher.dispatch(events.vendors.onEdited);
@@ -84,17 +102,21 @@ export default class VendorsService {
/**
* Retrieve the given vendor details by id or throw not found.
* @param {number} tenantId
* @param {number} customerId
* @param {number} tenantId
* @param {number} customerId
*/
private getVendorByIdOrThrowError(tenantId: number, customerId: number) {
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'vendor');
return this.contactService.getContactByIdOrThrowError(
tenantId,
customerId,
'vendor'
);
}
/**
* Deletes the given vendor from the storage.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} tenantId
* @param {number} vendorId
* @return {Promise<void>}
*/
public async deleteVendor(tenantId: number, vendorId: number) {
@@ -103,17 +125,23 @@ export default class VendorsService {
await this.getVendorByIdOrThrowError(tenantId, vendorId);
await this.vendorHasNoBillsOrThrowError(tenantId, vendorId);
this.logger.info('[vendor] trying to delete vendor.', { tenantId, vendorId });
this.logger.info('[vendor] trying to delete vendor.', {
tenantId,
vendorId,
});
await Contact.query().findById(vendorId).delete();
await this.eventDispatcher.dispatch(events.vendors.onDeleted, { tenantId, vendorId });
await this.eventDispatcher.dispatch(events.vendors.onDeleted, {
tenantId,
vendorId,
});
this.logger.info('[vendor] deleted successfully.', { tenantId, vendorId });
}
/**
* Retrieve the given vendor details.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} tenantId
* @param {number} vendorId
*/
public async getVendor(tenantId: number, vendorId: number) {
return this.contactService.getContact(tenantId, vendorId, 'vendor');
@@ -121,26 +149,26 @@ export default class VendorsService {
/**
* Writes vendor opening balance journal entries.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} openingBalance
* @param {number} tenantId
* @param {number} vendorId
* @param {number} openingBalance
* @return {Promise<void>}
*/
public async writeVendorOpeningBalanceJournal(
tenantId: number,
vendorId: number,
openingBalance: number,
openingBalance: number
) {
const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal);
this.logger.info('[vendor] writing opening balance journal entries.', { tenantId, vendorId });
await journalCommands.vendorOpeningBalance(vendorId, openingBalance)
await Promise.all([
journal.saveBalance(),
journal.saveEntries(),
]);
this.logger.info('[vendor] writing opening balance journal entries.', {
tenantId,
vendorId,
});
await journalCommands.vendorOpeningBalance(vendorId, openingBalance);
await Promise.all([journal.saveBalance(), journal.saveEntries()]);
}
/**
@@ -149,28 +177,43 @@ export default class VendorsService {
* @param {number} vendorId -
* @return {Promise<void>}
*/
public async revertOpeningBalanceEntries(tenantId: number, vendorId: number|number[]) {
public async revertOpeningBalanceEntries(
tenantId: number,
vendorId: number | number[]
) {
const id = Array.isArray(vendorId) ? vendorId : [vendorId];
this.logger.info('[customer] trying to revert opening balance journal entries.', { tenantId, customerId });
this.logger.info(
'[customer] trying to revert opening balance journal entries.',
{ tenantId, customerId }
);
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId, id, 'vendor',
tenantId,
id,
'vendor'
);
}
/**
* Retrieve the given vendors or throw error if one of them not found.
* @param {numebr} tenantId
* @param {numebr} tenantId
* @param {number[]} vendorsIds
*/
private getVendorsOrThrowErrorNotFound(tenantId: number, vendorsIds: number[]) {
return this.contactService.getContactsOrThrowErrorNotFound(tenantId, vendorsIds, 'vendor');
private getVendorsOrThrowErrorNotFound(
tenantId: number,
vendorsIds: number[]
) {
return this.contactService.getContactsOrThrowErrorNotFound(
tenantId,
vendorsIds,
'vendor'
);
}
/**
* Deletes the given vendors from the storage.
* @param {number} tenantId
* @param {number[]} vendorsIds
* @param {number} tenantId
* @param {number[]} vendorsIds
* @return {Promise<void>}
*/
public async deleteBulkVendors(
@@ -183,38 +226,53 @@ export default class VendorsService {
await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds);
await Contact.query().whereIn('id', vendorsIds).delete();
await this.eventDispatcher.dispatch(events.vendors.onBulkDeleted, { tenantId, vendorsIds });
await this.eventDispatcher.dispatch(events.vendors.onBulkDeleted, {
tenantId,
vendorsIds,
});
this.logger.info('[vendor] bulk deleted successfully.', { tenantId, vendorsIds });
this.logger.info('[vendor] bulk deleted successfully.', {
tenantId,
vendorsIds,
});
}
/**
* Validates the vendor has no associated bills or throw service error.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} tenantId
* @param {number} vendorId
*/
private async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) {
private async vendorHasNoBillsOrThrowError(
tenantId: number,
vendorId: number
) {
const { billRepository } = this.tenancy.repositories(tenantId);
// Retrieve the bill that associated to the given vendor id.
const bills = await billRepository.find({ vendor_id: vendorId });
if (bills.length > 0) {
throw new ServiceError('vendor_has_bills')
throw new ServiceError('vendor_has_bills');
}
}
/**
* Throws error in case one of vendors have associated bills.
* @param {number} tenantId
* @param {number[]} customersIds
* @param {number} tenantId
* @param {number[]} customersIds
* @throws {ServiceError}
*/
private async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) {
private async vendorsHaveNoBillsOrThrowError(
tenantId: number,
vendorsIds: number[]
) {
const { billRepository } = this.tenancy.repositories(tenantId);
// Retrieves bills that assocaited to the given vendors.
const vendorsBills = await billRepository.findWhereIn('vendor_id', vendorsIds);
const vendorsBills = await billRepository.findWhereIn(
'vendor_id',
vendorsIds
);
const billsVendorsIds = vendorsBills.map((bill) => bill.vendorId);
// The difference between the vendors ids and bills vendors ids.
@@ -233,18 +291,24 @@ export default class VendorsService {
public async getVendorsList(
tenantId: number,
vendorsFilter: IVendorsFilter
): Promise<{ vendors: IVendor[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { Contact } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Contact, vendorsFilter);
const { results, pagination } = await Contact.query().onBuild((builder) => {
builder.modify('vendor');
dynamicFilter.buildQuery()(builder);
}).pagination(
vendorsFilter.page - 1,
vendorsFilter.pageSize,
): Promise<{
vendors: IVendor[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { Vendor } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
Vendor,
vendorsFilter
);
const { results, pagination } = await Vendor.query()
.onBuild((builder) => {
dynamicFilter.buildQuery()(builder);
})
.pagination(vendorsFilter.page - 1, vendorsFilter.pageSize);
return {
vendors: results,
pagination,
@@ -254,28 +318,33 @@ export default class VendorsService {
/**
* Changes the opeing balance of the given vendor.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} openingBalance
* @param {Date|string} openingBalanceAt
* @param {number} tenantId
* @param {number} vendorId
* @param {number} openingBalance
* @param {Date|string} openingBalanceAt
*/
public async changeOpeningBalance(
tenantId: number,
vendorId: number,
openingBalance: number,
openingBalanceAt: Date|string,
openingBalanceAt: Date | string
): Promise<void> {
await this.contactService.changeOpeningBalance(
tenantId,
vendorId,
'vendor',
openingBalance,
openingBalanceAt,
openingBalanceAt
);
// Triggers `onOpeingBalanceChanged` event.
await this.eventDispatcher.dispatch(events.vendors.onOpeningBalanceChanged, {
tenantId, vendorId, openingBalance, openingBalanceAt
});
await this.eventDispatcher.dispatch(
events.vendors.onOpeningBalanceChanged,
{
tenantId,
vendorId,
openingBalance,
openingBalanceAt,
}
);
}
}

View File

@@ -3,7 +3,7 @@ import {
ICurrencyEditDTO,
ICurrencyDTO,
ICurrenciesService,
ICurrency
ICurrency,
} from 'interfaces';
import {
EventDispatcher,
@@ -14,7 +14,7 @@ import TenancyService from 'services/Tenancy/TenancyService';
const ERRORS = {
CURRENCY_NOT_FOUND: 'currency_not_found',
CURRENCY_CODE_EXISTS: 'currency_code_exists'
CURRENCY_CODE_EXISTS: 'currency_code_exists',
};
@Service()
@@ -30,40 +30,62 @@ export default class CurrenciesService implements ICurrenciesService {
/**
* Retrieve currency by given currency code or throw not found error.
* @param {number} tenantId
* @param {string} currencyCode
* @param {number} currencyId
* @param {number} tenantId
* @param {string} currencyCode
* @param {number} currencyId
*/
private async validateCurrencyCodeUniquiness(tenantId: number, currencyCode: string, currencyId?: number) {
private async validateCurrencyCodeUniquiness(
tenantId: number,
currencyCode: string,
currencyId?: number
) {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] trying to validate currency code existance.', { tenantId, currencyCode });
this.logger.info(
'[currencies] trying to validate currency code existance.',
{ tenantId, currencyCode }
);
const foundCurrency = await Currency.query().onBuild((query) => {
query.findOne('currency_code', currencyCode);
if (currencyId) {
query.whereNot('id', currencyId)
query.whereNot('id', currencyId);
}
});
if (foundCurrency) {
this.logger.info('[currencies] the currency code already exists.', { tenantId, currencyCode });
this.logger.info('[currencies] the currency code already exists.', {
tenantId,
currencyCode,
});
throw new ServiceError(ERRORS.CURRENCY_CODE_EXISTS);
}
}
/**
* Retrieve currency by the given currency code or throw service error.
* @param {number} tenantId
* @param {string} currencyCode
* @param {number} tenantId
* @param {string} currencyCode
*/
private async getCurrencyByCodeOrThrowError(tenantId: number, currencyCode: string) {
private async getCurrencyByCodeOrThrowError(
tenantId: number,
currencyCode: string
) {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] trying to validate currency code existance.', { tenantId, currencyCode });
const foundCurrency = await Currency.query().findOne('currency_code', currencyCode);
this.logger.info(
'[currencies] trying to validate currency code existance.',
{ tenantId, currencyCode }
);
const foundCurrency = await Currency.query().findOne(
'currency_code',
currencyCode
);
if (!foundCurrency) {
this.logger.info('[currencies] the given currency code not exists.', { tenantId, currencyCode });
this.logger.info('[currencies] the given currency code not exists.', {
tenantId,
currencyCode,
});
throw new ServiceError(ERRORS.CURRENCY_NOT_FOUND);
}
return foundCurrency;
@@ -71,17 +93,26 @@ export default class CurrenciesService implements ICurrenciesService {
/**
* Retrieve currency by given id or throw not found error.
* @param {number} tenantId
* @param {number} currencyId
* @param {number} tenantId
* @param {number} currencyId
*/
private async getCurrencyIdOrThrowError(tenantId: number, currencyId: number) {
private async getCurrencyIdOrThrowError(
tenantId: number,
currencyId: number
) {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] trying to validate currency by id existance.', { tenantId, currencyId });
this.logger.info(
'[currencies] trying to validate currency by id existance.',
{ tenantId, currencyId }
);
const foundCurrency = await Currency.query().findOne('id', currencyId);
if (!foundCurrency) {
this.logger.info('[currencies] the currency code not found.', { tenantId, currencyId });
this.logger.info('[currencies] the currency code not found.', {
tenantId,
currencyId,
});
throw new ServiceError(ERRORS.CURRENCY_NOT_FOUND);
}
return foundCurrency;
@@ -89,56 +120,87 @@ export default class CurrenciesService implements ICurrenciesService {
/**
* Creates a new currency.
* @param {number} tenantId
* @param {ICurrencyDTO} currencyDTO
* @param {number} tenantId
* @param {ICurrencyDTO} currencyDTO
*/
public async newCurrency(tenantId: number, currencyDTO: ICurrencyDTO) {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] try to insert new currency.', { tenantId, currencyDTO });
this.logger.info('[currencies] try to insert new currency.', {
tenantId,
currencyDTO,
});
await this.validateCurrencyCodeUniquiness(tenantId, currencyDTO.currencyCode);
await this.validateCurrencyCodeUniquiness(
tenantId,
currencyDTO.currencyCode
);
await Currency.query().insert({ ...currencyDTO });
this.logger.info('[currencies] the currency inserted successfully.', { tenantId, currencyDTO });
this.logger.info('[currencies] the currency inserted successfully.', {
tenantId,
currencyDTO,
});
}
/**
* Edit details of the given currency.
* @param {number} tenantId
* @param {number} currencyId
* @param {ICurrencyDTO} currencyDTO
* @param {number} tenantId
* @param {number} currencyId
* @param {ICurrencyDTO} currencyDTO
*/
public async editCurrency(tenantId: number, currencyId: number, currencyDTO: ICurrencyEditDTO): Promise<ICurrency> {
public async editCurrency(
tenantId: number,
currencyId: number,
currencyDTO: ICurrencyEditDTO
): Promise<ICurrency> {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] try to edit currency.', { tenantId, currencyId, currencyDTO });
this.logger.info('[currencies] try to edit currency.', {
tenantId,
currencyId,
currencyDTO,
});
await this.getCurrencyIdOrThrowError(tenantId, currencyId);
const currency = await Currency.query().patchAndFetchById(currencyId, { ...currencyDTO });
this.logger.info('[currencies] the currency edited successfully.', { tenantId, currencyDTO });
const currency = await Currency.query().patchAndFetchById(currencyId, {
...currencyDTO,
});
this.logger.info('[currencies] the currency edited successfully.', {
tenantId,
currencyDTO,
});
return currency;
}
/**
* Delete the given currency code.
* @param {number} tenantId
* @param {string} currencyCode
* @param {number} tenantId
* @param {string} currencyCode
* @return {Promise<}
*/
public async deleteCurrency(tenantId: number, currencyCode: string): Promise<void> {
public async deleteCurrency(
tenantId: number,
currencyCode: string
): Promise<void> {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] trying to delete the given currency.', { tenantId, currencyCode });
this.logger.info('[currencies] trying to delete the given currency.', {
tenantId,
currencyCode,
});
await this.getCurrencyByCodeOrThrowError(tenantId, currencyCode);
await Currency.query().where('currency_code', currencyCode).delete();
this.logger.info('[currencies] the currency deleted successfully.', { tenantId, currencyCode });
this.logger.info('[currencies] the currency deleted successfully.', {
tenantId,
currencyCode,
});
}
/**
* Listing currencies.
* @param {number} tenantId
* @param {number} tenantId
* @return {Promise<ICurrency[]>}
*/
public async listCurrencies(tenantId: number): Promise<ICurrency[]> {
@@ -149,4 +211,4 @@ export default class CurrenciesService implements ICurrenciesService {
});
return currencies;
}
}
}

View File

@@ -1,29 +1,41 @@
import { IInviteUserInput, ISystemUser } from "interfaces";
import Mail from "lib/Mail";
import { Service } from "typedi";
import { ISystemUser } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import Mail from 'lib/Mail';
import { Service, Container } from 'typedi';
import config from 'config';
@Service()
export default class InviteUsersMailMessages {
/**
* Sends invite mail to the given email.
* @param user
* @param invite
* @param user
* @param invite
*/
async sendInviteMail(user: ISystemUser, invite) {
async sendInviteMail(tenantId: number, fromUser: ISystemUser, invite: any) {
const { protocol, hostname } = config;
const tenancyService = Container.get(TenancyService);
// Retrieve tenant's settings
const settings = tenancyService.settings(tenantId);
// Retreive tenant orgnaization name.
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const mail = new Mail()
.setSubject(`${user.fullName} has invited you to join a Bigcapital`)
.setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`)
.setView('mail/UserInvite.html')
.setTo(invite.email)
.setData({
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'),
acceptUrl: `${protocol}://${hostname}/invite/accept/${invite.token}`,
fullName: `${fromUser.firstName} ${fromUser.lastName}`,
firstName: fromUser.firstName,
lastName: fromUser.lastName,
email: fromUser.email,
organizationName,
});
await mail.send();
Logger.log('info', 'User has been sent invite user email successfuly.');
}
}
}

View File

@@ -42,23 +42,27 @@ export default class InviteUserService {
token: string,
inviteUserInput: IInviteUserInput
): Promise<void> {
const { systemUserRepository } = this.sysRepositories;
// Retrieve the invite token or throw not found error.
const inviteToken = await this.getInviteOrThrowError(token);
// Validates the user phone number.
await this.validateUserPhoneNumber(inviteUserInput);
this.logger.info('[aceept_invite] trying to hash the user password.');
const hashedPassword = await hashPassword(inviteUserInput.password);
this.logger.info('[accept_invite] trying to update user details.');
const { systemUserRepository } = this.sysRepositories;
const user = await systemUserRepository.findOneByEmail(inviteToken.email);
const user = await systemUserRepository.getByEmail(inviteToken.email);
const updateUserOper = systemUserRepository.edit(user.id, {
// Sets the invited user details after invite accepting.
const updateUserOper = systemUserRepository.update({
...inviteUserInput,
active: 1,
invite_accepted_at: moment().format('YYYY-MM-DD'),
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
password: hashedPassword,
});
}, { id: user.id });
this.logger.info('[accept_invite] trying to delete the given token.');
const deleteInviteTokenOper = Invite.query()
@@ -70,7 +74,6 @@ export default class InviteUserService {
updateUserOper,
deleteInviteTokenOper,
]);
// Triggers `onUserAcceptInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
inviteToken,
@@ -94,6 +97,9 @@ export default class InviteUserService {
invite: IInvite,
user: ISystemUser
}> {
const { systemUserRepository } = this.sysRepositories;
// Throw error in case user email exists.
await this.throwErrorIfUserEmailExists(email);
this.logger.info('[send_invite] trying to store invite token.');
@@ -106,16 +112,14 @@ export default class InviteUserService {
this.logger.info(
'[send_invite] trying to store user with email and tenant.'
);
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, {
invite,
invite, authorizedUser, tenantId
});
return { invite, user };
}
@@ -155,7 +159,7 @@ export default class InviteUserService {
email: string
): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.getByEmail(email);
const foundUser = await systemUserRepository.findOneByEmail(email);
if (foundUser) {
throw new ServiceError('email_already_invited');
@@ -187,7 +191,7 @@ export default class InviteUserService {
inviteUserInput: IInviteUserInput
): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.getByPhoneNumber(
const foundUser = await systemUserRepository.findOneByPhoneNumber(
inviteUserInput.phoneNumber
);

View File

@@ -157,7 +157,7 @@ export default class MediaService implements IMediaService {
const mediaIds = Array.isArray(mediaId) ? mediaId : [mediaId];
const tenant = await tenantRepository.getById(tenantId);
const tenant = await tenantRepository.findOneById(tenantId);
const media = await this.getMediaByIdsOrThrowError(tenantId, mediaIds);
const tenantPath = `${publicPath}${tenant.organizationId}`;
@@ -192,7 +192,7 @@ export default class MediaService implements IMediaService {
this.logger.info('[media] trying to upload media.', { tenantId });
const tenant = await tenantRepository.getById(tenantId);
const tenant = await tenantRepository.findOneById(tenantId);
const fileName = `${attachment.md5}.png`;
// Validate the attachment.

View File

@@ -106,7 +106,7 @@ export default class OrganizationService {
});
const { tenantRepository } = this.sysRepositories;
const tenant = await tenantRepository.getById(user.tenantId);
const tenant = await tenantRepository.findOneById(user.tenantId);
return [tenant];
}
@@ -150,7 +150,8 @@ export default class OrganizationService {
*/
private async getTenantByOrgIdOrThrowError(organizationId: string) {
const { tenantRepository } = this.sysRepositories;
const tenant = await tenantRepository.getByOrgId(organizationId);
const tenant = await tenantRepository.findOne({ organizationId });
this.throwIfTenantNotExists(tenant);
return tenant;

View File

@@ -28,14 +28,13 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
SALE_INVOICE_ALREADY_DELIVERED: 'SALE_INVOICE_ALREADY_DELIVERED',
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE'
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
};
/**
@@ -72,13 +71,20 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
saleEstimatesService: SaleEstimateService;
/**
*
*
* Validate whether sale invoice number unqiue on the storage.
*/
async validateInvoiceNumberUnique(tenantId: number, invoiceNumber: string, notInvoiceId?: number) {
async validateInvoiceNumberUnique(
tenantId: number,
invoiceNumber: string,
notInvoiceId?: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
this.logger.info('[sale_invoice] validating sale invoice number existance.', { tenantId, invoiceNumber });
this.logger.info(
'[sale_invoice] validating sale invoice number existance.',
{ tenantId, invoiceNumber }
);
const saleInvoice = await SaleInvoice.query()
.findOne('invoice_no', invoiceNumber)
.onBuild((builder) => {
@@ -88,8 +94,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
});
if (saleInvoice) {
this.logger.info('[sale_invoice] sale invoice number not unique.', { tenantId, invoiceNumber });
throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE)
this.logger.info('[sale_invoice] sale invoice number not unique.', {
tenantId,
invoiceNumber,
});
throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE);
}
}
@@ -116,73 +125,91 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
*/
transformDTOToModel(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO|ISaleInvoiceEditDTO,
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
oldSaleInvoice?: ISaleInvoice
): ISaleInvoice {
const { ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
return {
...formatDateFields(
omit(saleInvoiceDTO, ['delivered']),
['invoiceDate', 'dueDate']
),
...formatDateFields(omit(saleInvoiceDTO, ['delivered', 'entries']), [
'invoiceDate',
'dueDate',
]),
// Avoid rewrite the deliver date in edit mode when already published.
...(saleInvoiceDTO.delivered && (!oldSaleInvoice?.deliveredAt)) && ({
deliveredAt: moment().toMySqlDateTime(),
}),
...(saleInvoiceDTO.delivered &&
!oldSaleInvoice?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
balance,
paymentAmount: 0,
}
entries: saleInvoiceObj.entries.map((entry) => ({
reference_type: 'SaleInvoice',
...omit(entry, ['amount', 'id']),
})),
};
}
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
* @async
* @param {number} tenantId =
* @param {ISaleInvoice} saleInvoiceDTO -
* @param {number} tenantId =
* @param {ISaleInvoice} saleInvoiceDTO -
* @return {ISaleInvoice}
*/
public async createSaleInvoice(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const invLotNumber = 1;
// Transform DTO object to model object.
const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
saleInvoiceDTO.customerId
);
// Validate sale invoice number uniquiness.
if (saleInvoiceDTO.invoiceNo) {
await this.validateInvoiceNumberUnique(tenantId, saleInvoiceDTO.invoiceNo);
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceDTO.invoiceNo
);
}
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries);
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleInvoiceDTO.entries
);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleInvoiceDTO.entries
);
this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
const saleInvoice = await SaleInvoice.query()
.insertGraphAndFetch({
...omit(saleInvoiceObj, ['entries']),
entries: saleInvoiceObj.entries.map((entry) => ({
reference_type: 'SaleInvoice',
...omit(entry, ['amount', 'id']),
}))
});
const saleInvoice = await saleInvoiceRepository.upsertGraph({
...saleInvoiceObj,
});
await this.eventDispatcher.dispatch(events.saleInvoice.onCreated, {
tenantId, saleInvoice, saleInvoiceId: saleInvoice.id,
tenantId,
saleInvoice,
saleInvoiceId: saleInvoice.id,
});
this.logger.info('[sale_invoice] successfully inserted.', {
tenantId,
saleInvoice,
});
this.logger.info('[sale_invoice] successfully inserted.', { tenantId, saleInvoice });
return saleInvoice;
}
@@ -190,50 +217,85 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
/**
* Edit the given sale invoice.
* @async
* @param {number} tenantId -
* @param {number} tenantId -
* @param {Number} saleInvoiceId -
* @param {ISaleInvoice} saleInvoice -
*/
public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any): Promise<ISaleInvoice> {
public async editSaleInvoice(
tenantId: number,
saleInvoiceId: number,
saleInvoiceDTO: any
): Promise<ISaleInvoice> {
const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Transform DTO object to model object.
const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO, oldSaleInvoice);
const saleInvoiceObj = this.transformDTOToModel(
tenantId,
saleInvoiceDTO,
oldSaleInvoice
);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
saleInvoiceDTO.customerId
);
// Validate sale invoice number uniquiness.
if (saleInvoiceDTO.invoiceNo) {
await this.validateInvoiceNumberUnique(tenantId, saleInvoiceDTO.invoiceNo, saleInvoiceId);
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceDTO.invoiceNo,
saleInvoiceId
);
}
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries);
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleInvoiceDTO.entries
);
// Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleInvoiceDTO.entries
);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(tenantId, saleInvoiceId, 'SaleInvoice', saleInvoiceDTO.entries);
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
saleInvoiceId,
'SaleInvoice',
saleInvoiceDTO.entries
);
this.logger.info('[sale_invoice] trying to update sale invoice.');
const saleInvoice: ISaleInvoice = await SaleInvoice.query()
.upsertGraphAndFetch({
const saleInvoice: ISaleInvoice = await SaleInvoice.query().upsertGraphAndFetch(
{
id: saleInvoiceId,
...omit(saleInvoiceObj, ['entries', 'invLotNumber']),
entries: saleInvoiceObj.entries.map((entry) => ({
reference_type: 'SaleInvoice',
...omit(entry, ['amount']),
}))
});
})),
}
);
// Triggers `onSaleInvoiceEdited` event.
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, {
saleInvoice, oldSaleInvoice, tenantId, saleInvoiceId,
saleInvoice,
oldSaleInvoice,
tenantId,
saleInvoiceId,
});
return saleInvoice;
}
@@ -246,21 +308,27 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
*/
public async deliverSaleInvoice(
tenantId: number,
saleInvoiceId: number,
saleInvoiceId: number
): Promise<void> {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Retrieve details of the given sale invoice id.
const saleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
const saleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Throws error in case the sale invoice already published.
if (saleInvoice.isDelivered) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED);
}
// Record the delivered at on the storage.
await saleInvoiceRepository.update({
deliveredAt: moment().toMySqlDateTime()
}, { id: saleInvoiceId });
await saleInvoiceRepository.update(
{
deliveredAt: moment().toMySqlDateTime(),
},
{ id: saleInvoiceId }
);
}
/**
@@ -269,15 +337,21 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @async
* @param {Number} saleInvoiceId - The given sale invoice id.
*/
public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number): Promise<void> {
public async deleteSaleInvoice(
tenantId: number,
saleInvoiceId: number
): Promise<void> {
const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Unlink the converted sale estimates from the given sale invoice.
await this.saleEstimatesService.unlinkConvertedEstimateFromInvoice(
tenantId,
saleInvoiceId,
saleInvoiceId
);
this.logger.info('[sale_invoice] delete sale invoice with entries.');
@@ -288,7 +362,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
.delete();
await this.eventDispatcher.dispatch(events.saleInvoice.onDeleted, {
tenantId, oldSaleInvoice,
tenantId,
oldSaleInvoice,
});
}
@@ -303,44 +378,48 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
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',]),
lotNumber: saleInvoice.invLotNumber,
transactionType: 'SaleInvoice',
transactionId: saleInvoiceId,
direction: 'OUT',
date: saleInvoice.invoice_date,
entryId: entry.id,
}));
const inventortyTransactions = saleInvoice.entries.map((entry) => ({
...pick(entry, ['item_id', 'quantity', 'rate']),
lotNumber: saleInvoice.invLotNumber,
transactionType: 'SaleInvoice',
transactionId: saleInvoiceId,
direction: 'OUT',
date: saleInvoice.invoice_date,
entryId: entry.id,
}));
return this.inventoryService.recordInventoryTransactions(
tenantId, inventortyTransactions, override,
tenantId,
inventortyTransactions,
override
);
}
/**
* Deletes the inventory transactions.
* @param {string} transactionType
* @param {number} transactionId
* @param {string} transactionType
* @param {number} transactionId
*/
private async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
private 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) {
switch (trans.direction) {
case 'OUT':
if (trans.inventoryTransactionId) {
const revertRemaining = InventoryTransaction.query()
.where('id', trans.inventoryTransactionId)
.where('direction', 'OUT')
.increment('remaining', trans.quanitity);
opers.push(revertRemaining);
}
break;
@@ -361,16 +440,19 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
/**
* Retrieve sale invoice with associated entries.
* @async
* @param {Number} saleInvoiceId
* @param {Number} saleInvoiceId
*/
public async getSaleInvoice(tenantId: number, saleInvoiceId: number): Promise<ISaleInvoice> {
public async getSaleInvoice(
tenantId: number,
saleInvoiceId: number
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('entries')
.withGraphFetched('customer');
if (!saleInvoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
@@ -378,9 +460,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
}
/**
* Schedules compute sale invoice items cost based on each item
* Schedules compute sale invoice items cost based on each item
* cost method.
* @param {ISaleInvoice} saleInvoice
* @param {ISaleInvoice} saleInvoice
* @return {Promise}
*/
async scheduleComputeInvoiceItemsCost(
@@ -396,15 +478,20 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
const inventoryItemsIds = chain(saleInvoice.entries)
.filter((entry: IItemEntry) => entry.item.type === 'inventory')
.map((entry: IItemEntry) => entry.itemId)
.uniq().value();
.uniq()
.value();
if (inventoryItemsIds.length === 0) {
await this.writeNonInventoryInvoiceJournals(tenantId, saleInvoice, override);
await this.writeNonInventoryInvoiceJournals(
tenantId,
saleInvoice,
override
);
} else {
await this.scheduleComputeItemsCost(
tenantId,
inventoryItemsIds,
saleInvoice.invoice_date,
saleInvoice.invoice_date
);
}
}
@@ -436,40 +523,43 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
await Promise.all([
journal.deleteEntries(),
journal.saveEntries(),
journal.saveBalance(),
journal.saveBalance(),
]);
}
/**
* Retrieve sales invoices filterable and paginated list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async salesInvoicesList(
tenantId: number,
salesInvoicesFilter: ISalesInvoicesFilter
): Promise<{
salesInvoices: ISaleInvoice[],
pagination: IPaginationMeta,
filterMeta: IFilterMeta
salesInvoices: ISaleInvoice[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleInvoice, salesInvoicesFilter);
this.logger.info('[sale_invoice] try to get sales invoices list.', { tenantId, salesInvoicesFilter });
const {
results,
pagination,
} = await SaleInvoice.query().onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
}).pagination(
salesInvoicesFilter.page - 1,
salesInvoicesFilter.pageSize,
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
SaleInvoice,
salesInvoicesFilter
);
this.logger.info('[sale_invoice] try to get sales invoices list.', {
tenantId,
salesInvoicesFilter,
});
const { results, pagination } = await SaleInvoice.query()
.onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
})
.pagination(salesInvoicesFilter.page - 1, salesInvoicesFilter.pageSize);
return {
salesInvoices: results,
pagination,
@@ -479,12 +569,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
/**
* Retrieve due sales invoices.
* @param {number} tenantId
* @param {number} customerId
* @param {number} tenantId
* @param {number} customerId
*/
public async getPayableInvoices(
tenantId: number,
customerId?: number,
customerId?: number
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);

View File

@@ -41,7 +41,7 @@ export default class SubscriptionService {
const { tenantRepository } = this.sysRepositories;
const plan = await Plan.query().findOne('slug', planSlug);
const tenant = await tenantRepository.getById(tenantId);
const tenant = await tenantRepository.findOneById(tenantId);
const paymentViaLicense = new LicensePaymentMethod();
const paymentContext = new PaymentContext(paymentViaLicense);

View File

@@ -45,7 +45,7 @@ export default class TenantsManagerService implements ITenantManager{
*/
public async createTenant(): Promise<ITenant> {
const { tenantRepository } = this.sysRepositories;
const tenant = await tenantRepository.newTenantWithUniqueOrgId();
const tenant = await tenantRepository.createWithUniqueOrgId();
return tenant;
}

View File

@@ -1,9 +1,9 @@
import { Inject, Service } from "typedi";
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 systemRepositories from "loaders/systemRepositories";
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 {
@@ -18,47 +18,64 @@ export default class UsersService {
/**
* Creates a new user.
* @param {number} tenantId
* @param {number} userId
* @param {IUserDTO} userDTO
* @param {number} tenantId
* @param {number} userId
* @param {IUserDTO} userDTO
* @return {Promise<ISystemUser>}
*/
async editUser(tenantId: number, userId: number, userDTO: ISystemUserDTO): Promise<ISystemUser> {
async editUser(
tenantId: number,
userId: number,
userDTO: ISystemUserDTO
): Promise<ISystemUser> {
const { systemUserRepository } = this.repositories;
const isEmailExists = await systemUserRepository.isEmailExists(userDTO.email, userId);
const isPhoneNumberExists = await systemUserRepository.isPhoneNumberExists(userDTO.phoneNumber, userId);
const userByEmail = await systemUserRepository.findOne({
email: userDTO.email,
id: userId,
});
const userByPhoneNumber = await systemUserRepository.findOne({
phoneNumber: userDTO.phoneNumber,
id: userId
});
const serviceErrors: ServiceError[] = [];
if (isEmailExists) {
if (userByEmail) {
serviceErrors.push(new ServiceError('email_already_exists'));
}
if (isPhoneNumberExists) {
if (userByPhoneNumber) {
serviceErrors.push(new ServiceError('phone_number_already_exist'));
}
if (serviceErrors.length > 0) {
throw new ServiceErrors(serviceErrors);
}
const updateSystemUser = await SystemUser.query()
.where('id', userId)
.update({ ...userDTO });
const updateSystemUser = await systemUserRepository
.update({ ...userDTO, }, { id: userId });
return updateSystemUser;
}
/**
* Validate user existance throw error in case user was not found.,
* @param {number} tenantId -
* @param {number} tenantId -
* @param {number} userId -
* @returns {ISystemUser}
*/
async getUserOrThrowError(tenantId: number, userId: number): Promise<ISystemUser> {
async getUserOrThrowError(
tenantId: number,
userId: number
): Promise<ISystemUser> {
const { systemUserRepository } = this.repositories;
const user = await systemUserRepository.getByIdAndTenant(userId, tenantId);
const user = await systemUserRepository.findOneByIdAndTenant(
userId,
tenantId
);
if (!user) {
this.logger.info('[users] the given user not found.', { tenantId, userId });
this.logger.info('[users] the given user not found.', {
tenantId,
userId,
});
throw new ServiceError('user_not_found');
}
return user;
@@ -66,25 +83,35 @@ export default class UsersService {
/**
* Deletes the given user id.
* @param {number} tenantId
* @param {number} userId
* @param {number} tenantId
* @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 });
this.logger.info('[users] trying to delete the given user.', {
tenantId,
userId,
});
await systemUserRepository.deleteById(userId);
this.logger.info('[users] the given user deleted successfully.', { tenantId, userId });
this.logger.info('[users] the given user deleted successfully.', {
tenantId,
userId,
});
}
/**
* Activate the given user id.
* @param {number} tenantId
* @param {number} userId
* @param {number} tenantId
* @param {number} userId
*/
async activateUser(tenantId: number, userId: number, authorizedUser: ISystemUser): Promise<void> {
async activateUser(
tenantId: number,
userId: number,
authorizedUser: ISystemUser
): Promise<void> {
this.throwErrorIfUserIdSameAuthorizedUser(userId, authorizedUser);
const { systemUserRepository } = this.repositories;
@@ -96,11 +123,15 @@ export default class UsersService {
/**
* Inactivate the given user id.
* @param {number} tenantId
* @param {number} userId
* @param {number} tenantId
* @param {number} userId
* @return {Promise<void>}
*/
async inactivateUser(tenantId: number, userId: number, authorizedUser: ISystemUser): Promise<void> {
async inactivateUser(
tenantId: number,
userId: number,
authorizedUser: ISystemUser
): Promise<void> {
this.throwErrorIfUserIdSameAuthorizedUser(userId, authorizedUser);
const { systemUserRepository } = this.repositories;
@@ -112,8 +143,8 @@ export default class UsersService {
/**
* Retrieve users list based on the given filter.
* @param {number} tenantId
* @param {object} filter
* @param {number} tenantId
* @param {object} filter
*/
async getList(tenantId: number) {
const users = await SystemUser.query()
@@ -134,7 +165,7 @@ export default class UsersService {
/**
* Throws service error in case the user was already active.
* @param {ISystemUser} user
* @param {ISystemUser} user
* @throws {ServiceError}
*/
throwErrorIfUserActive(user: ISystemUser) {
@@ -145,7 +176,7 @@ export default class UsersService {
/**
* Throws service error in case the user was already inactive.
* @param {ISystemUser} user
* @param {ISystemUser} user
* @throws {ServiceError}
*/
throwErrorIfUserInactive(user: ISystemUser) {
@@ -155,13 +186,16 @@ export default class UsersService {
}
/**
* Throw service error in case the given user same the authorized user.
* @param {number} userId
* @param {ISystemUser} authorizedUser
* Throw service error in case the given user same the authorized user.
* @param {number} userId
* @param {ISystemUser} authorizedUser
*/
throwErrorIfUserIdSameAuthorizedUser(userId: number, authorizedUser: ISystemUser) {
throwErrorIfUserIdSameAuthorizedUser(
userId: number,
authorizedUser: ISystemUser
) {
if (userId === authorizedUser.id) {
throw new ServiceError('user_same_the_authorized_user');
}
}
}
}