add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
import { IAccountTransaction } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { transaction } from 'objection';
export default class AccountTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'date',
'formattedDate',
'transactionType',
'transactionId',
'transactionTypeFormatted',
'credit',
'debit',
'formattedCredit',
'formattedDebit',
'fcCredit',
'fcDebit',
'formattedFcCredit',
'formattedFcDebit',
];
};
/**
* Exclude all attributes of the model.
* @returns {Array<string>}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Retrieves the formatted date.
* @returns {string}
*/
public formattedDate(transaction: IAccountTransaction) {
return this.formatDate(transaction.date);
}
/**
* Retrieves the formatted transaction type.
* @returns {string}
*/
public transactionTypeFormatted(transaction: IAccountTransaction) {
return transaction.referenceTypeFormatted;
}
/**
* Retrieves the tranasction type.
* @returns {string}
*/
public transactionType(transaction: IAccountTransaction) {
return transaction.referenceType;
}
/**
* Retrieves the transaction id.
* @returns {number}
*/
public transactionId(transaction: IAccountTransaction) {
return transaction.referenceId;
}
/**
* Retrieves the credit amount.
* @returns {string}
*/
protected formattedCredit(transaction: IAccountTransaction) {
return this.formatMoney(transaction.credit, {
excerptZero: true,
});
}
/**
* Retrieves the credit amount.
* @returns {string}
*/
protected formattedDebit(transaction: IAccountTransaction) {
return this.formatMoney(transaction.debit, {
excerptZero: true,
});
}
/**
* Retrieves the foreign credit amount.
* @returns {number}
*/
protected fcCredit(transaction: IAccountTransaction) {
return transaction.credit * transaction.exchangeRate;
}
/**
* Retrieves the foreign debit amount.
* @returns {number}
*/
protected fcDebit(transaction: IAccountTransaction) {
return transaction.debit * transaction.exchangeRate;
}
/**
* Retrieves the formatted foreign credit amount.
* @returns {string}
*/
protected formattedFcCredit(transaction: IAccountTransaction) {
return this.formatMoney(this.fcDebit(transaction), {
currencyCode: transaction.currencyCode,
excerptZero: true,
});
}
/**
* Retrieves the formatted foreign debit amount.
* @returns {string}
*/
protected formattedFcDebit(transaction: IAccountTransaction) {
return this.formatMoney(this.fcCredit(transaction), {
currencyCode: transaction.currencyCode,
excerptZero: true,
});
}
}

View File

@@ -0,0 +1,24 @@
import { IAccount } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class AccountTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['formattedAmount'];
};
/**
* Retrieve formatted account amount.
* @param {IAccount} invoice
* @returns {string}
*/
protected formattedAmount = (account: IAccount): string => {
return formatNumber(account.amount, {
currencyCode: account.currencyCode,
});
};
}

View File

@@ -0,0 +1,139 @@
import { Service, Inject } from 'typedi';
import {
IAccount,
IAccountCreateDTO,
IAccountEditDTO,
IAccountsFilter,
IAccountsTransactionsFilter,
IGetAccountTransactionPOJO,
} from '@/interfaces';
import { CreateAccount } from './CreateAccount';
import { DeleteAccount } from './DeleteAccount';
import { EditAccount } from './EditAccount';
import { ActivateAccount } from './ActivateAccount';
import { GetAccounts } from './GetAccounts';
import { GetAccount } from './GetAccount';
import { GetAccountTransactions } from './GetAccountTransactions';
@Service()
export class AccountsApplication {
@Inject()
private createAccountService: CreateAccount;
@Inject()
private deleteAccountService: DeleteAccount;
@Inject()
private editAccountService: EditAccount;
@Inject()
private activateAccountService: ActivateAccount;
@Inject()
private getAccountsService: GetAccounts;
@Inject()
private getAccountService: GetAccount;
@Inject()
private getAccountTransactionsService: GetAccountTransactions;
/**
* Creates a new account.
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
* @returns {Promise<IAccount>}
*/
public createAccount = (
tenantId: number,
accountDTO: IAccountCreateDTO
): Promise<IAccount> => {
return this.createAccountService.createAccount(tenantId, accountDTO);
};
/**
* Deletes the given account.
* @param {number} tenantId
* @param {number} accountId
* @returns {Promise<void>}
*/
public deleteAccount = (tenantId: number, accountId: number) => {
return this.deleteAccountService.deleteAccount(tenantId, accountId);
};
/**
* Edits the given account.
* @param {number} tenantId
* @param {number} accountId
* @param {IAccountEditDTO} accountDTO
* @returns
*/
public editAccount = (
tenantId: number,
accountId: number,
accountDTO: IAccountEditDTO
) => {
return this.editAccountService.editAccount(tenantId, accountId, accountDTO);
};
/**
* Activate the given account.
* @param {number} tenantId -
* @param {number} accountId -
*/
public activateAccount = (tenantId: number, accountId: number) => {
return this.activateAccountService.activateAccount(
tenantId,
accountId,
true
);
};
/**
* Inactivate the given account.
* @param {number} tenantId -
* @param {number} accountId -
*/
public inactivateAccount = (tenantId: number, accountId: number) => {
return this.activateAccountService.activateAccount(
tenantId,
accountId,
false
);
};
/**
* Retrieves the account details.
* @param {number} tenantId
* @param {number} accountId
* @returns {Promise<IAccount>}
*/
public getAccount = (tenantId: number, accountId: number) => {
return this.getAccountService.getAccount(tenantId, accountId);
};
/**
* Retrieves the accounts list.
* @param {number} tenantId
* @param {IAccountsFilter} filterDTO
* @returns
*/
public getAccounts = (tenantId: number, filterDTO: IAccountsFilter) => {
return this.getAccountsService.getAccountsList(tenantId, filterDTO);
};
/**
* Retrieves the given account transactions.
* @param {number} tenantId
* @param {IAccountsTransactionsFilter} filter
* @returns {Promise<IGetAccountTransactionPOJO[]>}
*/
public getAccountsTransactions = (
tenantId: number,
filter: IAccountsTransactionsFilter
): Promise<IGetAccountTransactionPOJO[]> => {
return this.getAccountTransactionsService.getAccountsTransactions(
tenantId,
filter
);
};
}

View File

@@ -0,0 +1,21 @@
import { Inject, Service } from 'typedi';
import { IAccountsTypesService, IAccountType } from '@/interfaces';
import AccountTypesUtils from '@/lib/AccountTypes';
import I18nService from '@/services/I18n/I18nService';
@Service()
export default class AccountsTypesService implements IAccountsTypesService {
@Inject()
i18nService: I18nService;
/**
* Retrieve all accounts types.
* @param {number} tenantId -
* @return {IAccountType}
*/
public getAccountsTypes(tenantId: number): IAccountType[] {
const accountTypes = AccountTypesUtils.getList();
return this.i18nService.i18nMapper(accountTypes, ['label'], tenantId);
}
}

View File

@@ -0,0 +1,64 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import TenancyService from '@/services/Tenancy/TenancyService';
import { IAccountEventActivatedPayload } from '@/interfaces';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { CommandAccountValidators } from './CommandAccountValidators';
@Service()
export class ActivateAccount {
@Inject()
private tenancy: TenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validator: CommandAccountValidators;
/**
* Activates/Inactivates the given account.
* @param {number} tenantId
* @param {number} accountId
* @param {boolean} activate
*/
public activateAccount = async (
tenantId: number,
accountId: number,
activate?: boolean
) => {
const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given account or throw not found error.
const oldAccount = await Account.query()
.findById(accountId)
.throwIfNotFound();
// Get all children accounts.
const accountsGraph = await accountRepository.getDependencyGraph();
const dependenciesAccounts = accountsGraph.dependenciesOf(accountId);
const patchAccountsIds = [...dependenciesAccounts, accountId];
// Activate account and associated transactions under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Activate and inactivate the given accounts ids.
activate
? await accountRepository.activateByIds(patchAccountsIds, trx)
: await accountRepository.inactivateByIds(patchAccountsIds, trx);
// Triggers `onAccountActivated` event.
this.eventPublisher.emitAsync(events.accounts.onActivated, {
tenantId,
accountId,
trx,
} as IAccountEventActivatedPayload);
});
};
}

View File

@@ -0,0 +1,211 @@
import { Inject, Service } from 'typedi';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { IAccountDTO, IAccount, IAccountCreateDTO } from '@/interfaces';
import AccountTypesUtils from '@/lib/AccountTypes';
import { ERRORS } from './constants';
@Service()
export class CommandAccountValidators {
@Inject()
private tenancy: TenancyService;
/**
* Throws error if the account was prefined.
* @param {IAccount} account
*/
public throwErrorIfAccountPredefined(account: IAccount) {
if (account.predefined) {
throw new ServiceError(ERRORS.ACCOUNT_PREDEFINED);
}
}
/**
* 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
*/
public async isAccountTypeChangedOrThrowError(
oldAccount: IAccount | IAccountDTO,
newAccount: IAccount | IAccountDTO
) {
if (oldAccount.accountType !== newAccount.accountType) {
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE);
}
}
/**
* Retrieve account type or throws service error.
* @param {number} tenantId -
* @param {number} accountTypeId -
* @return {IAccountType}
*/
public getAccountTypeOrThrowError(accountTypeKey: string) {
const accountType = AccountTypesUtils.getType(accountTypeKey);
if (!accountType) {
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_FOUND);
}
return accountType;
}
/**
* Retrieve parent account or throw service error.
* @param {number} tenantId
* @param {number} accountId
* @param {number} notAccountId
*/
public async getParentAccountOrThrowError(
tenantId: number,
accountId: number,
notAccountId?: number
) {
const { Account } = this.tenancy.models(tenantId);
const parentAccount = await Account.query()
.findById(accountId)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
});
if (!parentAccount) {
throw new ServiceError(ERRORS.PARENT_ACCOUNT_NOT_FOUND);
}
return parentAccount;
}
/**
* Throws error if the account type was not unique on the storage.
* @param {number} tenantId
* @param {string} accountCode
* @param {number} notAccountId
*/
public async isAccountCodeUniqueOrThrowError(
tenantId: number,
accountCode: string,
notAccountId?: number
) {
const { Account } = this.tenancy.models(tenantId);
const account = await Account.query()
.where('code', accountCode)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
});
if (account.length > 0) {
throw new ServiceError(ERRORS.ACCOUNT_CODE_NOT_UNIQUE);
}
}
/**
* Validates the account name uniquiness.
* @param {number} tenantId
* @param {string} accountName
* @param {number} notAccountId - Ignore the account id.
*/
public async validateAccountNameUniquiness(
tenantId: number,
accountName: string,
notAccountId?: number
) {
const { Account } = this.tenancy.models(tenantId);
const foundAccount = await Account.query()
.findOne('name', accountName)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
});
if (foundAccount) {
throw new ServiceError(ERRORS.ACCOUNT_NAME_NOT_UNIQUE);
}
}
/**
* Validates the given account type supports multi-currency.
* @param {IAccountDTO} accountDTO -
*/
public validateAccountTypeSupportCurrency = (
accountDTO: IAccountCreateDTO,
baseCurrency: string
) => {
// Can't continue to validate the type has multi-currency feature
// if the given currency equals the base currency or not assigned.
if (accountDTO.currencyCode === baseCurrency || !accountDTO.currencyCode) {
return;
}
const meta = AccountTypesUtils.getType(accountDTO.accountType);
// Throw error if the account type does not support multi-currency.
if (!meta?.multiCurrency) {
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY);
}
};
/**
* Validates the account DTO currency code whether equals the currency code of
* parent account.
* @param {IAccountCreateDTO} accountDTO
* @param {IAccount} parentAccount
* @param {string} baseCurrency -
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
*/
public validateCurrentSameParentAccount = (
accountDTO: IAccountCreateDTO,
parentAccount: IAccount,
baseCurrency: string,
) => {
// If the account DTO currency not assigned and the parent account has no base currency.
if (
!accountDTO.currencyCode &&
parentAccount.currencyCode !== baseCurrency
) {
throw new ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT);
}
// If the account DTO is assigned and not equals the currency code of parent account.
if (
accountDTO.currencyCode &&
parentAccount.currencyCode !== accountDTO.currencyCode
) {
throw new ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT);
}
};
/**
* Throws service error if parent account has different type.
* @param {IAccountDTO} accountDTO
* @param {IAccount} parentAccount
*/
public throwErrorIfParentHasDiffType(
accountDTO: IAccountDTO,
parentAccount: IAccount
) {
if (accountDTO.accountType !== parentAccount.accountType) {
throw new ServiceError(ERRORS.PARENT_ACCOUNT_HAS_DIFFERENT_TYPE);
}
}
/**
* Retrieve account of throw service error in case account not found.
* @param {number} tenantId
* @param {number} accountId
* @return {IAccount}
*/
public async getAccountOrThrowError(tenantId: number, accountId: number) {
const { accountRepository } = this.tenancy.repositories(tenantId);
const account = await accountRepository.findOneById(accountId);
if (!account) {
throw new ServiceError(ERRORS.ACCOUNT_NOT_FOUND);
}
return account;
}
}

View File

@@ -0,0 +1,140 @@
import { Inject, Service } from 'typedi';
import { kebabCase } from 'lodash';
import { Knex } from 'knex';
import TenancyService from '@/services/Tenancy/TenancyService';
import {
IAccount,
IAccountEventCreatedPayload,
IAccountEventCreatingPayload,
IAccountCreateDTO,
} from '@/interfaces';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { TenantMetadata } from '@/system/models';
import { CommandAccountValidators } from './CommandAccountValidators';
@Service()
export class CreateAccount {
@Inject()
private tenancy: TenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validator: CommandAccountValidators;
/**
* Authorize the account creation.
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
*/
private authorize = async (
tenantId: number,
accountDTO: IAccountCreateDTO,
baseCurrency: string
) => {
// Validate account name uniquiness.
await this.validator.validateAccountNameUniquiness(
tenantId,
accountDTO.name
);
// Validate the account code uniquiness.
if (accountDTO.code) {
await this.validator.isAccountCodeUniqueOrThrowError(
tenantId,
accountDTO.code
);
}
// Retrieve the account type meta or throw service error if not found.
this.validator.getAccountTypeOrThrowError(accountDTO.accountType);
// Ingore the parent account validation if not presented.
if (accountDTO.parentAccountId) {
const parentAccount = await this.validator.getParentAccountOrThrowError(
tenantId,
accountDTO.parentAccountId
);
this.validator.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
// Inherit active status from parent account.
accountDTO.active = parentAccount.active;
// Validate should currency code be the same currency of parent account.
this.validator.validateCurrentSameParentAccount(
accountDTO,
parentAccount,
baseCurrency
);
}
// Validates the given account type supports the multi-currency.
this.validator.validateAccountTypeSupportCurrency(accountDTO, baseCurrency);
};
/**
* Transformes the create account DTO to input model.
* @param {IAccountCreateDTO} createAccountDTO
*/
private transformDTOToModel = (
createAccountDTO: IAccountCreateDTO,
baseCurrency: string
) => {
return {
...createAccountDTO,
slug: kebabCase(createAccountDTO.name),
currencyCode: createAccountDTO.currencyCode || baseCurrency,
};
};
/**
* Creates a new account on the storage.
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
* @returns {Promise<IAccount>}
*/
public createAccount = async (
tenantId: number,
accountDTO: IAccountCreateDTO
): Promise<IAccount> => {
const { Account } = this.tenancy.models(tenantId);
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Authorize the account creation.
await this.authorize(tenantId, accountDTO, tenantMeta.baseCurrency);
// Transformes the DTO to model.
const accountInputModel = this.transformDTOToModel(
accountDTO,
tenantMeta.baseCurrency
);
// Creates a new account with associated transactions under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onAccountCreating` event.
await this.eventPublisher.emitAsync(events.accounts.onCreating, {
tenantId,
accountDTO,
trx,
} as IAccountEventCreatingPayload);
// Inserts account to the storage.
const account = await Account.query(trx).insertAndFetch({
...accountInputModel,
});
// Triggers `onAccountCreated` event.
await this.eventPublisher.emitAsync(events.accounts.onCreated, {
tenantId,
account,
accountId: account.id,
trx,
} as IAccountEventCreatedPayload);
return account;
});
};
}

View File

@@ -0,0 +1,107 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { IAccountEventDeletedPayload, IAccount } from '@/interfaces';
import events from '@/subscribers/events';
import { CommandAccountValidators } from './CommandAccountValidators';
import { ERRORS } from './constants';
@Service()
export class DeleteAccount {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validator: CommandAccountValidators;
/**
* Authorize account delete.
* @param {number} tenantId - Tenant id.
* @param {number} accountId - Account id.
*/
private authorize = async (
tenantId: number,
accountId: number,
oldAccount: IAccount
) => {
// Throw error if the account was predefined.
this.validator.throwErrorIfAccountPredefined(oldAccount);
};
/**
* Unlink the given parent account with children accounts.
* @param {number} tenantId -
* @param {number|number[]} parentAccountId -
*/
private async unassociateChildrenAccountsFromParent(
tenantId: number,
parentAccountId: number | number[],
trx?: Knex.Transaction
) {
const { Account } = this.tenancy.models(tenantId);
const accountsIds = Array.isArray(parentAccountId)
? parentAccountId
: [parentAccountId];
await Account.query(trx)
.whereIn('parent_account_id', accountsIds)
.patch({ parent_account_id: null });
}
/**
* Deletes the account from the storage.
* @param {number} tenantId
* @param {number} accountId
*/
public deleteAccount = async (
tenantId: number,
accountId: number
): Promise<void> => {
const { Account } = this.tenancy.models(tenantId);
// Retrieve account or not found service error.
const oldAccount = await Account.query()
.findById(accountId)
.throwIfNotFound()
.queryAndThrowIfHasRelations({
type: ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS,
});
// Authorize before delete account.
await this.authorize(tenantId, accountId, oldAccount);
// Deletes the account and assocaited transactions under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onAccountDelete` event.
await this.eventPublisher.emitAsync(events.accounts.onDelete, {
trx,
oldAccount,
tenantId,
} as IAccountEventDeletedPayload);
// Unlink the parent account from children accounts.
await this.unassociateChildrenAccountsFromParent(
tenantId,
accountId,
trx
);
// Deletes account by the given id.
await Account.query(trx).findById(accountId).delete();
// Triggers `onAccountDeleted` event.
await this.eventPublisher.emitAsync(events.accounts.onDeleted, {
tenantId,
accountId,
oldAccount,
trx,
} as IAccountEventDeletedPayload);
});
};
}

View File

@@ -0,0 +1,116 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import TenancyService from '@/services/Tenancy/TenancyService';
import {
IAccountEventEditedPayload,
IAccountEditDTO,
IAccount,
} from '@/interfaces';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { CommandAccountValidators } from './CommandAccountValidators';
@Service()
export class EditAccount {
@Inject()
private tenancy: TenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
@Inject()
private validator: CommandAccountValidators;
/**
* Authorize the account editing.
* @param {number} tenantId
* @param {number} accountId
* @param {IAccountEditDTO} accountDTO
* @param {IAccount} oldAccount -
*/
private authorize = async (
tenantId: number,
accountId: number,
accountDTO: IAccountEditDTO,
oldAccount: IAccount
) => {
// Validate account name uniquiness.
await this.validator.validateAccountNameUniquiness(
tenantId,
accountDTO.name,
accountId
);
// Validate the account type should be not mutated.
await this.validator.isAccountTypeChangedOrThrowError(
oldAccount,
accountDTO
);
// Validate the account code not exists on the storage.
if (accountDTO.code && accountDTO.code !== oldAccount.code) {
await this.validator.isAccountCodeUniqueOrThrowError(
tenantId,
accountDTO.code,
oldAccount.id
);
}
// Retrieve the parent account of throw not found service error.
if (accountDTO.parentAccountId) {
const parentAccount = await this.validator.getParentAccountOrThrowError(
tenantId,
accountDTO.parentAccountId,
oldAccount.id
);
this.validator.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
}
};
/**
* Edits details of the given account.
* @param {number} tenantId
* @param {number} accountId
* @param {IAccountDTO} accountDTO
*/
public async editAccount(
tenantId: number,
accountId: number,
accountDTO: IAccountEditDTO
): Promise<IAccount> {
const { Account } = this.tenancy.models(tenantId);
// Retrieve the old account or throw not found service error.
const oldAccount = await Account.query()
.findById(accountId)
.throwIfNotFound();
// Authorize the account editing.
await this.authorize(tenantId, accountId, accountDTO, oldAccount);
// Edits account and associated transactions under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onAccountEditing` event.
await this.eventPublisher.emitAsync(events.accounts.onEditing, {
tenantId,
oldAccount,
accountDTO,
});
// Update the account on the storage.
const account = await Account.query(trx)
.findById(accountId)
.update({ ...accountDTO });
// Triggers `onAccountEdited` event.
await this.eventPublisher.emitAsync(events.accounts.onEdited, {
tenantId,
account,
oldAccount,
trx,
} as IAccountEventEditedPayload);
return account;
});
}
}

View File

@@ -0,0 +1,41 @@
import { Service, Inject } from 'typedi';
import I18nService from '@/services/I18n/I18nService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { AccountTransformer } from './AccountTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetAccount {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private i18nService: I18nService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the given account details.
* @param {number} tenantId
* @param {number} accountId
*/
public getAccount = async (tenantId: number, accountId: number) => {
const { Account } = this.tenancy.models(tenantId);
// Find the given account or throw not found error.
const account = await Account.query().findById(accountId).throwIfNotFound();
// Transformes the account model to POJO.
const transformed = await this.transformer.transform(
tenantId,
account,
new AccountTransformer()
);
return this.i18nService.i18nApply(
[['accountTypeLabel'], ['accountNormalFormatted']],
transformed,
tenantId
);
};
}

View File

@@ -0,0 +1,51 @@
import { Service, Inject } from 'typedi';
import {
IAccountsTransactionsFilter,
IAccountTransaction,
IGetAccountTransactionPOJO,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import AccountTransactionTransformer from './AccountTransactionTransformer';
@Service()
export class GetAccountTransactions {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the accounts transactions.
* @param {number} tenantId -
* @param {IAccountsTransactionsFilter} filter -
*/
public getAccountsTransactions = async (
tenantId: number,
filter: IAccountsTransactionsFilter
): Promise<IGetAccountTransactionPOJO[]> => {
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
// Retrieve the given account or throw not found error.
if (filter.accountId) {
await Account.query().findById(filter.accountId).throwIfNotFound();
}
const transactions = await AccountTransaction.query().onBuild((query) => {
query.orderBy('date', 'DESC');
if (filter.accountId) {
query.where('account_id', filter.accountId);
}
query.withGraphFetched('account');
query.withGraphFetched('contact');
query.limit(filter.limit || 50);
});
// Transform the account transaction.
return this.transformer.transform(
tenantId,
transactions,
new AccountTransactionTransformer()
);
};
}

View File

@@ -0,0 +1,66 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import { IAccountsFilter, IAccountResponse, IFilterMeta } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { AccountTransformer } from './AccountTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetAccounts {
@Inject()
private tenancy: TenancyService;
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Parsees accounts list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve accounts datatable list.
* @param {number} tenantId
* @param {IAccountsFilter} accountsFilter
* @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
*/
public getAccountsList = async (
tenantId: number,
filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
const { Account } = this.tenancy.models(tenantId);
// Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Account,
filter
);
// Retrieve accounts model based on the given query.
const accounts = await Account.query().onBuild((builder) => {
dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode);
});
// Retrievs the formatted accounts collection.
const transformedAccounts = await this.transformer.transform(
tenantId,
accounts,
new AccountTransformer()
);
return {
accounts: transformedAccounts,
filterMeta: dynamicList.getResponseMeta(),
};
};
}

View File

@@ -0,0 +1,22 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class MutateBaseCurrencyAccounts {
@Inject()
tenancy: HasTenancyService;
/**
* Mutates the all accounts or the organziation.
* @param {number} tenantId
* @param {string} currencyCode
*/
public mutateAllAccountsCurrency = async (
tenantId: number,
currencyCode: string
) => {
const { Account } = this.tenancy.models(tenantId);
await Account.query().update({ currencyCode });
};
}

View File

@@ -0,0 +1,77 @@
export const ERRORS = {
ACCOUNT_NOT_FOUND: 'account_not_found',
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',
ACCOUNT_PREDEFINED: 'account_predefined',
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
PREDEFINED_ACCOUNTS: 'predefined_accounts',
ACCOUNTS_HAVE_TRANSACTIONS: 'accounts_have_transactions',
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE:
'close_account_and_to_account_not_same_type',
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY: 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY',
ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT: 'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT',
};
// Default views columns.
export const DEFAULT_VIEW_COLUMNS = [
{ key: 'name', label: 'Account name' },
{ key: 'code', label: 'Account code' },
{ key: 'account_type_label', label: 'Account type' },
{ key: 'account_normal', label: 'Account normal' },
{ key: 'amount', label: 'Balance' },
{ key: 'currencyCode', label: 'Currency' },
];
// Accounts default views.
export const DEFAULT_VIEWS = [
{
name: 'Assets',
slug: 'assets',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'root_type', comparator: 'equals', value: 'asset' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Liabilities',
slug: 'liabilities',
rolesLogicExpression: '1',
roles: [
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'liability' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Equity',
slug: 'equity',
rolesLogicExpression: '1',
roles: [
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'equity' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Income',
slug: 'income',
rolesLogicExpression: '1',
roles: [
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'income' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Expenses',
slug: 'expenses',
rolesLogicExpression: '1',
roles: [
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'expense' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
];

View File

@@ -0,0 +1,34 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import { MutateBaseCurrencyAccounts } from '../MutateBaseCurrencyAccounts';
@Service()
export class MutateBaseCurrencyAccountsSubscriber {
@Inject()
public mutateBaseCurrencyAccounts: MutateBaseCurrencyAccounts;
/**
* Attaches the events with handles.
* @param bus
*/
attach(bus) {
bus.subscribe(
events.organization.baseCurrencyUpdated,
this.updateAccountsCurrencyOnBaseCurrencyMutated
);
}
/**
* Updates the all accounts currency once the base currency
* of the organization is mutated.
*/
private updateAccountsCurrencyOnBaseCurrencyMutated = async ({
tenantId,
organizationDTO,
}) => {
await this.mutateBaseCurrencyAccounts.mutateAllAccountsCurrency(
tenantId,
organizationDTO.baseCurrency
);
};
}