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,38 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import { InventoryTransactionsWarehouses } from './AcountsTransactionsWarehouses';
import { IBranchesActivatedPayload } from '@/interfaces';
@Service()
export class AccountsTransactionsWarehousesSubscribe {
@Inject()
accountsTransactionsWarehouses: InventoryTransactionsWarehouses;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.branch.onActivated,
this.updateGLTransactionsToPrimaryBranchOnActivated
);
return bus;
};
/**
* Updates all GL transactions to primary branch once
* the multi-branches activated.
* @param {IBranchesActivatedPayload}
*/
private updateGLTransactionsToPrimaryBranchOnActivated = async ({
tenantId,
primaryBranch,
trx,
}: IBranchesActivatedPayload) => {
await this.accountsTransactionsWarehouses.updateTransactionsWithWarehouse(
tenantId,
primaryBranch.id,
trx
);
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class InventoryTransactionsWarehouses {
@Inject()
tenancy: HasTenancyService;
/**
* Updates all accounts transctions with the priamry branch.
* @param tenantId
* @param primaryBranchId
*/
public updateTransactionsWithWarehouse = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { AccountTransaction } = await this.tenancy.models(tenantId);
await AccountTransaction.query(trx).update({
branchId: primaryBranchId,
});
};
}

View File

@@ -0,0 +1,71 @@
import moment from 'moment';
import { castArray, sumBy, toArray } from 'lodash';
import { IBill, ISystemUser, IAccount } from '@/interfaces';
import JournalPoster from './JournalPoster';
import JournalEntry from './JournalEntry';
import { IExpense, IExpenseCategory } from '@/interfaces';
import { increment } from 'utils';
export default class JournalCommands {
journal: JournalPoster;
models: any;
repositories: any;
/**
* Constructor method.
* @param {JournalPoster} journal -
*/
constructor(journal: JournalPoster) {
this.journal = journal;
this.repositories = this.journal.repositories;
this.models = this.journal.models;
}
/**
* Reverts the jouranl entries.
* @param {number|number[]} referenceId - Reference id.
* @param {string} referenceType - Reference type.
*/
async revertJournalEntries(
referenceId: number | number[],
referenceType: string | string[]
) {
const { AccountTransaction } = this.models;
const transactions = await AccountTransaction.query()
.whereIn('reference_type', castArray(referenceType))
.whereIn('reference_id', castArray(referenceId))
.withGraphFetched('account');
this.journal.fromTransactions(transactions);
this.journal.removeEntries();
}
/**
* Reverts the sale invoice cost journal entries.
* @param {Date|string} startingDate
* @return {Promise<void>}
*/
async revertInventoryCostJournalEntries(
startingDate: Date | string
): Promise<void> {
const { AccountTransaction } = this.models;
this.journal.fromTransactions(transactions);
this.journal.removeEntries();
}
/**
* Reverts sale invoice the income journal entries.
* @param {number} saleInvoiceId
*/
async revertInvoiceIncomeEntries(saleInvoiceId: number) {
const { transactionsRepository } = this.repositories;
const transactions = await transactionsRepository.journal({
referenceType: ['SaleInvoice'],
referenceId: [saleInvoiceId],
});
this.journal.fromTransactions(transactions);
this.journal.removeEntries();
}
}

View File

@@ -0,0 +1,74 @@
import async from 'async';
export default class JournalContacts {
saveContactBalanceQueue: any;
contactsBalanceTable: {
[key: number]: { credit: number; debit: number };
} = {};
constructor(journal) {
this.journal = journal;
this.saveContactBalanceQueue = async.queue(
this.saveContactBalanceChangeTask.bind(this),
10
);
}
/**
* Sets the contact balance change.
*/
private getContactsBalanceChanges(entry) {
if (!entry.contactId) {
return;
}
const change = {
debit: entry.debit,
credit: entry.credit,
};
if (!this.contactsBalanceTable[entry.contactId]) {
this.contactsBalanceTable[entry.contactId] = { credit: 0, debit: 0 };
}
if (change.credit) {
this.contactsBalanceTable[entry.contactId].credit += change.credit;
}
if (change.debit) {
this.contactsBalanceTable[entry.contactId].debit += change.debit;
}
}
/**
* Save contacts balance change.
*/
saveContactsBalance() {
const balanceChanges = Object.entries(
this.contactsBalanceTable
).map(([contactId, { credit, debit }]) => ({ contactId, credit, debit }));
return this.saveContactBalanceQueue.pushAsync(balanceChanges);
}
/**
* Saves contact balance change task.
* @param {number} contactId - Contact id.
* @param {number} credit - Credit amount.
* @param {number} debit - Debit amount.
*/
async saveContactBalanceChangeTask({ contactId, credit, debit }, callback) {
const { contactRepository } = this.repositories;
const contact = await contactRepository.findOneById(contactId);
let balanceChange = 0;
if (contact.contactNormal === 'credit') {
balanceChange += credit - debit;
} else {
balanceChange += debit - credit;
}
// Contact change balance.
await contactRepository.changeNumber(
{ id: contactId },
'balance',
balanceChange
);
callback();
}
}

View File

@@ -0,0 +1,10 @@
export default class JournalEntry {
constructor(entry) {
const defaults = {
credit: 0,
debit: 0,
};
this.entry = { ...defaults, ...entry };
}
}

View File

@@ -0,0 +1,17 @@
import moment from 'moment';
import { IJournalPoster } from '@/interfaces';
export default class JournalFinancial {
journal: IJournalPoster;
accountsDepGraph: any;
/**
* Journal poster.
* @param {IJournalPoster} journal
*/
constructor(journal: IJournalPoster) {
this.journal = journal;
this.accountsDepGraph = this.journal.accountsDepGraph;
}
}

View File

@@ -0,0 +1,759 @@
import { omit, get, chain } from 'lodash';
import moment from 'moment';
import { Container } from 'typedi';
import async from 'async';
import JournalEntry from '@/services/Accounting/JournalEntry';
import TenancyService from '@/services/Tenancy/TenancyService';
import {
IJournalEntry,
IJournalPoster,
IAccountChange,
IAccountsChange,
TEntryType,
} from '@/interfaces';
import Knex from 'knex';
const CONTACTS_CONFIG = [
{
accountBySlug: 'accounts-receivable',
contactService: 'customer',
assignRequired: true,
},
{
accountBySlug: 'accounts-payable',
contactService: 'vendor',
assignRequired: true,
},
];
export default class JournalPoster implements IJournalPoster {
tenantId: number;
tenancy: TenancyService;
logger: any;
models: any;
repositories: any;
deletedEntriesIds: number[] = [];
entries: IJournalEntry[] = [];
balancesChange: IAccountsChange = {};
accountsDepGraph: IAccountsChange;
accountsBalanceTable: { [key: number]: number } = {};
contactsBalanceTable: {
[key: number]: { credit: number; debit: number }[];
} = {};
saveContactBalanceQueue: any;
/**
* Journal poster constructor.
* @param {number} tenantId -
*/
constructor(tenantId: number, accountsGraph?: any, trx?: Knex.Transaction) {
this.initTenancy();
this.tenantId = tenantId;
this.models = this.tenancy.models(tenantId);
this.repositories = this.tenancy.repositories(tenantId);
if (accountsGraph) {
this.accountsDepGraph = accountsGraph;
}
this.trx = trx;
this.saveContactBalanceQueue = async.queue(
this.saveContactBalanceChangeTask.bind(this),
10
);
}
/**
* Initial tenancy.
* @private
*/
private initTenancy() {
try {
this.tenancy = Container.get(TenancyService);
this.logger = Container.get('logger');
} catch (exception) {
throw new Error('Should execute this class inside tenancy area.');
}
}
/**
* Async initialize acccounts dependency graph.
* @private
* @returns {Promise<void>}
*/
public async initAccountsDepGraph(): Promise<void> {
const { accountRepository } = this.repositories;
if (!this.accountsDepGraph) {
const accountsDepGraph = await accountRepository.getDependencyGraph();
this.accountsDepGraph = accountsDepGraph;
}
}
/**
* Detarmines the ledger is empty.
*/
public isEmpty() {
return this.entries.length === 0;
}
/**
* Writes the credit entry for the given account.
* @param {IJournalEntry} entry -
*/
public credit(entryModel: IJournalEntry): void {
if (entryModel instanceof JournalEntry === false) {
throw new Error('The entry is not instance of JournalEntry.');
}
this.entries.push(entryModel.entry);
this.setAccountBalanceChange(entryModel.entry);
this.setContactBalanceChange(entryModel.entry);
}
/**
* Writes the debit entry for the given account.
* @param {JournalEntry} entry -
*/
public debit(entryModel: IJournalEntry): void {
if (entryModel instanceof JournalEntry === false) {
throw new Error('The entry is not instance of JournalEntry.');
}
this.entries.push(entryModel.entry);
this.setAccountBalanceChange(entryModel.entry);
this.setContactBalanceChange(entryModel.entry);
}
/**
* Sets the contact balance change.
*/
private setContactBalanceChange(entry) {
if (!entry.contactId) {
return;
}
const change = {
debit: entry.debit || 0,
credit: entry.credit || 0,
account: entry.account,
};
if (!this.contactsBalanceTable[entry.contactId]) {
this.contactsBalanceTable[entry.contactId] = [];
}
this.contactsBalanceTable[entry.contactId].push(change);
}
/**
* Save contacts balance change.
*/
async saveContactsBalance() {
await this.initAccountsDepGraph();
const balanceChanges = Object.entries(this.contactsBalanceTable).map(
([contactId, entries]) => ({
contactId,
entries: entries.filter((entry) => {
const account = this.accountsDepGraph.getNodeData(entry.account);
return (
account &&
CONTACTS_CONFIG.some((config) => {
return config.accountBySlug === account.slug;
})
);
}),
})
);
const balanceEntries = chain(balanceChanges)
.map((change) =>
change.entries.map((entry) => ({
...entry,
contactId: change.contactId,
}))
)
.flatten()
.value();
return this.saveContactBalanceQueue.pushAsync(balanceEntries);
}
/**
* Saves contact balance change task.
* @param {number} contactId - Contact id.
* @param {number} credit - Credit amount.
* @param {number} debit - Debit amount.
*/
async saveContactBalanceChangeTask({ contactId, credit, debit }) {
const { contactRepository } = this.repositories;
const contact = await contactRepository.findOneById(contactId);
let balanceChange = 0;
if (contact.contactNormal === 'credit') {
balanceChange += credit - debit;
} else {
balanceChange += debit - credit;
}
// Contact change balance.
await contactRepository.changeNumber(
{ id: contactId },
'balance',
balanceChange,
this.trx
);
}
/**
* Sets account balance change.
* @param {JournalEntry} entry
* @param {String} type
*/
private setAccountBalanceChange(entry: IJournalEntry): void {
const accountChange: IAccountChange = {
debit: entry.debit,
credit: entry.credit,
};
this._setAccountBalanceChange(entry.account, accountChange);
}
/**
* Sets account balance change.
* @private
* @param {number} accountId -
* @param {IAccountChange} accountChange
*/
private _setAccountBalanceChange(
accountId: number,
accountChange: IAccountChange
) {
this.balancesChange = this.accountBalanceChangeReducer(
this.balancesChange,
accountId,
accountChange
);
}
/**
* Accounts balance change reducer.
* @param {IAccountsChange} balancesChange
* @param {number} accountId
* @param {IAccountChange} accountChange
* @return {IAccountChange}
*/
private accountBalanceChangeReducer(
balancesChange: IAccountsChange,
accountId: number,
accountChange: IAccountChange
) {
const change = { ...balancesChange };
if (!change[accountId]) {
change[accountId] = { credit: 0, debit: 0 };
}
if (accountChange.credit) {
change[accountId].credit += accountChange.credit;
}
if (accountChange.debit) {
change[accountId].debit += accountChange.debit;
}
return change;
}
/**
* Converts balance changes to array.
* @private
* @param {IAccountsChange} accountsChange -
* @return {Promise<{ account: number, change: number }>}
*/
private async convertBalanceChangesToArr(
accountsChange: IAccountsChange
): Promise<{ account: number; change: number }[]> {
const mappedList: { account: number; change: number }[] = [];
const accountsIds: number[] = Object.keys(accountsChange).map((id) =>
parseInt(id, 10)
);
await Promise.all(
accountsIds.map(async (account: number) => {
const accountChange = accountsChange[account];
const accountNode = this.accountsDepGraph.getNodeData(account);
const normal = accountNode.accountNormal;
let change = 0;
if (accountChange.credit) {
change +=
normal === 'credit'
? accountChange.credit
: -1 * accountChange.credit;
}
if (accountChange.debit) {
change +=
normal === 'debit' ? accountChange.debit : -1 * accountChange.debit;
}
mappedList.push({ account, change });
})
);
return mappedList;
}
/**
* Saves the balance change of journal entries.
* @returns {Promise<void>}
*/
public async saveBalance() {
await this.initAccountsDepGraph();
const { Account } = this.models;
const accountsChange = this.balanceChangeWithDepends(this.balancesChange);
const balancesList = await this.convertBalanceChangesToArr(accountsChange);
const balancesAccounts = balancesList.map((b) => b.account);
// Ensure the accounts has atleast zero in amount.
await Account.query(this.trx)
.where('amount', null)
.whereIn('id', balancesAccounts)
.patch({ amount: 0 });
const balanceUpdateOpers: Promise<void>[] = [];
balancesList.forEach((balance: { account: number; change: number }) => {
const method: string = balance.change < 0 ? 'decrement' : 'increment';
this.logger.info(
'[journal_poster] increment/decrement account balance.',
{
balance,
tenantId: this.tenantId,
}
);
const query = Account.query(this.trx)
[method]('amount', Math.abs(balance.change))
.where('id', balance.account);
balanceUpdateOpers.push(query);
});
await Promise.all(balanceUpdateOpers);
this.resetAccountsBalanceChange();
}
/**
* Changes all accounts that dependencies of changed accounts.
* @param {IAccountsChange} accountsChange
* @returns {IAccountsChange}
*/
private balanceChangeWithDepends(
accountsChange: IAccountsChange
): IAccountsChange {
const accountsIds = Object.keys(accountsChange);
let changes: IAccountsChange = {};
accountsIds.forEach((accountId) => {
const accountChange = accountsChange[accountId];
const depAccountsIds = this.accountsDepGraph.dependantsOf(accountId);
[accountId, ...depAccountsIds].forEach((account) => {
changes = this.accountBalanceChangeReducer(
changes,
account,
accountChange
);
});
});
return changes;
}
/**
* Resets accounts balance change.
* @private
*/
private resetAccountsBalanceChange() {
this.balancesChange = {};
}
/**
* Saves the stacked journal entries to the storage.
* @returns {Promise<void>}
*/
public async saveEntries() {
const { transactionsRepository } = this.repositories;
const saveOperations: Promise<void>[] = [];
this.logger.info('[journal] trying to insert accounts transactions.');
this.entries.forEach((entry) => {
const oper = transactionsRepository.create(
{
accountId: entry.account,
...omit(entry, ['account']),
},
this.trx
);
saveOperations.push(oper);
});
await Promise.all(saveOperations);
}
/**
* Reverses the stacked journal entries.
*/
public reverseEntries() {
const reverseEntries: IJournalEntry[] = [];
this.entries.forEach((entry) => {
const reverseEntry = { ...entry };
if (entry.credit) {
reverseEntry.debit = entry.credit;
}
if (entry.debit) {
reverseEntry.credit = entry.debit;
}
reverseEntries.push(reverseEntry);
});
this.entries = reverseEntries;
}
/**
* Removes all stored entries or by the given in ids.
* @param {Array} ids -
*/
removeEntries(ids: number[] = []) {
const targetIds = ids.length <= 0 ? this.entries.map((e) => e.id) : ids;
const removeEntries = this.entries.filter(
(e) => targetIds.indexOf(e.id) !== -1
);
this.entries = this.entries.filter((e) => targetIds.indexOf(e.id) === -1);
removeEntries.forEach((entry) => {
entry.credit = -1 * entry.credit;
entry.debit = -1 * entry.debit;
this.setAccountBalanceChange(entry);
this.setContactBalanceChange(entry);
});
this.deletedEntriesIds.push(...removeEntries.map((entry) => entry.id));
}
/**
* Delete all the stacked entries.
* @return {Promise<void>}
*/
public async deleteEntries() {
const { transactionsRepository } = this.repositories;
if (this.deletedEntriesIds.length > 0) {
await transactionsRepository.deleteWhereIdIn(
this.deletedEntriesIds,
this.trx
);
}
}
/**
* Load fetched accounts journal entries.
* @param {IJournalEntry[]} entries -
*/
fromTransactions(transactions) {
transactions.forEach((transaction) => {
this.entries.push({
...transaction,
referenceTypeFormatted: transaction.referenceTypeFormatted,
account: transaction.accountId,
accountNormal: get(transaction, 'account.accountNormal'),
});
});
}
/**
* Calculates the entries balance change.
* @public
*/
public calculateEntriesBalanceChange() {
this.entries.forEach((entry) => {
if (entry.credit) {
this.setAccountBalanceChange(entry, 'credit');
}
if (entry.debit) {
this.setAccountBalanceChange(entry, 'debit');
}
});
}
static fromTransactions(entries, ...args: [number, ...any]) {
const journal = new this(...args);
journal.fromTransactions(entries);
return journal;
}
/**
* Retrieve the closing balance for the given account and closing date.
* @param {Number} accountId -
* @param {Date} closingDate -
* @param {string} dataType? -
* @return {number}
*/
getClosingBalance(
accountId: number,
closingDate: Date | string,
dateType: string = 'day'
): number {
let closingBalance = 0;
const momentClosingDate = moment(closingDate);
this.entries.forEach((entry) => {
// Can not continue if not before or event same closing date.
if (
(!momentClosingDate.isAfter(entry.date, dateType) &&
!momentClosingDate.isSame(entry.date, dateType)) ||
(entry.account !== accountId && accountId)
) {
return;
}
if (entry.accountNormal === 'credit') {
closingBalance += entry.credit ? entry.credit : -1 * entry.debit;
} else if (entry.accountNormal === 'debit') {
closingBalance += entry.debit ? entry.debit : -1 * entry.credit;
}
});
return closingBalance;
}
/**
* Retrieve the given account balance with dependencies accounts.
* @param {Number} accountId -
* @param {Date} closingDate -
* @param {String} dateType -
* @return {Number}
*/
getAccountBalance(
accountId: number,
closingDate: Date | string,
dateType: string
) {
const accountNode = this.accountsDepGraph.getNodeData(accountId);
const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId);
const depAccounts = depAccountsIds.map((id) =>
this.accountsDepGraph.getNodeData(id)
);
let balance: number = 0;
[...depAccounts, accountNode].forEach((account) => {
const closingBalance = this.getClosingBalance(
account.id,
closingDate,
dateType
);
this.accountsBalanceTable[account.id] = closingBalance;
balance += this.accountsBalanceTable[account.id];
});
return balance;
}
/**
* Retrieve the credit/debit sumation for the given account and date.
* @param {Number} account -
* @param {Date|String} closingDate -
*/
getTrialBalance(accountId, closingDate) {
const momentClosingDate = moment(closingDate);
const result = {
credit: 0,
debit: 0,
balance: 0,
};
this.entries.forEach((entry) => {
if (
(!momentClosingDate.isAfter(entry.date, 'day') &&
!momentClosingDate.isSame(entry.date, 'day')) ||
(entry.account !== accountId && accountId)
) {
return;
}
result.credit += entry.credit;
result.debit += entry.debit;
if (entry.accountNormal === 'credit') {
result.balance += entry.credit - entry.debit;
} else if (entry.accountNormal === 'debit') {
result.balance += entry.debit - entry.credit;
}
});
return result;
}
/**
* Retrieve trial balance of the given account with depends.
* @param {Number} accountId
* @param {Date} closingDate
* @param {String} dateType
* @return {Number}
*/
getTrialBalanceWithDepands(
accountId: number,
closingDate: Date,
dateType: string
) {
const accountNode = this.accountsDepGraph.getNodeData(accountId);
const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId);
const depAccounts = depAccountsIds.map((id) =>
this.accountsDepGraph.getNodeData(id)
);
const trialBalance = { credit: 0, debit: 0, balance: 0 };
[...depAccounts, accountNode].forEach((account) => {
const _trialBalance = this.getTrialBalance(
account.id,
closingDate,
dateType
);
trialBalance.credit += _trialBalance.credit;
trialBalance.debit += _trialBalance.debit;
trialBalance.balance += _trialBalance.balance;
});
return trialBalance;
}
getContactTrialBalance(
accountId: number,
contactId: number,
contactType: string,
closingDate?: Date | string,
openingDate?: Date | string
) {
const momentClosingDate = moment(closingDate);
const momentOpeningDate = moment(openingDate);
const trial = {
credit: 0,
debit: 0,
balance: 0,
};
this.entries.forEach((entry) => {
if (
(closingDate &&
!momentClosingDate.isAfter(entry.date, 'day') &&
!momentClosingDate.isSame(entry.date, 'day')) ||
(openingDate &&
!momentOpeningDate.isBefore(entry.date, 'day') &&
!momentOpeningDate.isSame(entry.date)) ||
(accountId && entry.account !== accountId) ||
(contactId && entry.contactId !== contactId) ||
entry.contactType !== contactType
) {
return;
}
if (entry.credit) {
trial.balance -= entry.credit;
trial.credit += entry.credit;
}
if (entry.debit) {
trial.balance += entry.debit;
trial.debit += entry.debit;
}
});
return trial;
}
/**
* Retrieve total balnace of the given customer/vendor contact.
* @param {Number} accountId
* @param {Number} contactId
* @param {String} contactType
* @param {Date} closingDate
*/
getContactBalance(
accountId: number,
contactId: number,
contactType: string,
closingDate: Date,
openingDate: Date
) {
const momentClosingDate = moment(closingDate);
let balance = 0;
this.entries.forEach((entry) => {
if (
(closingDate &&
!momentClosingDate.isAfter(entry.date, 'day') &&
!momentClosingDate.isSame(entry.date, 'day')) ||
(entry.account !== accountId && accountId) ||
(contactId && entry.contactId !== contactId) ||
entry.contactType !== contactType
) {
return;
}
if (entry.credit) {
balance -= entry.credit;
}
if (entry.debit) {
balance += entry.debit;
}
});
return balance;
}
getAccountEntries(accountId: number) {
return this.entries.filter((entry) => entry.account === accountId);
}
/**
* Retrieve account entries with depents accounts.
* @param {number} accountId -
*/
getAccountEntriesWithDepents(accountId: number) {
const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId);
const accountsIds = [accountId, ...depAccountsIds];
return this.entries.filter(
(entry) => accountsIds.indexOf(entry.account) !== -1
);
}
/**
* Retrieve total balnace of the given customer/vendor contact.
* @param {Number} accountId
* @param {Number} contactId
* @param {String} contactType
* @param {Date} closingDate
*/
getEntriesBalance(entries) {
let balance = 0;
entries.forEach((entry) => {
if (entry.credit) {
balance -= entry.credit;
}
if (entry.debit) {
balance += entry.debit;
}
});
return balance;
}
getContactEntries(contactId: number, openingDate: Date, closingDate?: Date) {
const momentClosingDate = moment(closingDate);
const momentOpeningDate = moment(openingDate);
return this.entries.filter((entry) => {
if (
(closingDate &&
!momentClosingDate.isAfter(entry.date, 'day') &&
!momentClosingDate.isSame(entry.date, 'day')) ||
(openingDate &&
!momentOpeningDate.isBefore(entry.date, 'day') &&
!momentOpeningDate.isSame(entry.date)) ||
entry.contactId === contactId
) {
return true;
}
return false;
});
}
}

View File

@@ -0,0 +1,249 @@
import moment from 'moment';
import { defaultTo, uniqBy } from 'lodash';
import { IAccountTransaction, ILedger, ILedgerEntry } from '@/interfaces';
export default class Ledger implements ILedger {
readonly entries: ILedgerEntry[];
/**
* Constructor method.
* @param {ILedgerEntry[]} entries
*/
constructor(entries: ILedgerEntry[]) {
this.entries = entries;
}
/**
* Filters the ledegr entries.
* @param callback
* @returns {ILedger}
*/
public filter(callback): ILedger {
const entries = this.entries.filter(callback);
return new Ledger(entries);
}
/**
* Retrieve the all entries of the ledger.
* @return {ILedgerEntry[]}
*/
public getEntries(): ILedgerEntry[] {
return this.entries;
}
/**
* Filters entries by th given contact id and returns a new ledger.
* @param {number} contactId
* @returns {ILedger}
*/
public whereContactId(contactId: number): ILedger {
return this.filter((entry) => entry.contactId === contactId);
}
/**
* Filters entries by the given account id and returns a new ledger.
* @param {number} accountId
* @returns {ILedger}
*/
public whereAccountId(accountId: number): ILedger {
return this.filter((entry) => entry.accountId === accountId);
}
/**
* Filters entries that before or same the given date and returns a new ledger.
* @param {Date|string} fromDate
* @returns {ILedger}
*/
public whereFromDate(fromDate: Date | string): ILedger {
const fromDateParsed = moment(fromDate);
return this.filter(
(entry) =>
fromDateParsed.isBefore(entry.date) || fromDateParsed.isSame(entry.date)
);
}
/**
* Filters ledger entries that after the given date and retruns a new ledger.
* @param {Date|string} toDate
* @returns {ILedger}
*/
public whereToDate(toDate: Date | string): ILedger {
const toDateParsed = moment(toDate);
return this.filter(
(entry) =>
toDateParsed.isAfter(entry.date) || toDateParsed.isSame(entry.date)
);
}
/**
* Filters the ledget entries by the given currency code.
* @param {string} currencyCode -
* @returns {ILedger}
*/
public whereCurrencyCode(currencyCode: string): ILedger {
return this.filter((entry) => entry.currencyCode === currencyCode);
}
/**
* Filters the ledger entries by the given branch id.
* @param {number} branchId
* @returns {ILedger}
*/
public whereBranch(branchId: number): ILedger {
return this.filter((entry) => entry.branchId === branchId);
}
/**
*
* @param {number} projectId
* @returns {ILedger}
*/
public whereProject(projectId: number): ILedger {
return this.filter((entry) => entry.projectId === projectId);
}
/**
* Filters the ledger entries by the given item id.
* @param {number} itemId
* @returns {ILedger}
*/
public whereItem(itemId: number): ILedger {
return this.filter((entry) => entry.itemId === itemId);
}
/**
* Retrieve the closing balance of the entries.
* @returns {number}
*/
public getClosingBalance(): number {
let closingBalance = 0;
this.entries.forEach((entry) => {
if (entry.accountNormal === 'credit') {
closingBalance += entry.credit - entry.debit;
} else if (entry.accountNormal === 'debit') {
closingBalance += entry.debit - entry.credit;
}
});
return closingBalance;
}
/**
* Retrieve the closing balance of the entries.
* @returns {number}
*/
public getForeignClosingBalance(): number {
let closingBalance = 0;
this.entries.forEach((entry) => {
const exchangeRate = entry.exchangeRate || 1;
if (entry.accountNormal === 'credit') {
closingBalance += (entry.credit - entry.debit) / exchangeRate;
} else if (entry.accountNormal === 'debit') {
closingBalance += (entry.debit - entry.credit) / exchangeRate;
}
});
return closingBalance;
}
/**
* Detarmines whether the ledger has no entries.
* @returns {boolean}
*/
public isEmpty(): boolean {
return this.entries.length === 0;
}
/**
* Retrieves the accounts ids of the entries uniquely.
* @returns {number[]}
*/
public getAccountsIds = (): number[] => {
return uniqBy(this.entries, 'accountId').map(
(e: ILedgerEntry) => e.accountId
);
};
/**
* Retrieves the contacts ids of the entries uniquely.
* @returns {number[]}
*/
public getContactsIds = (): number[] => {
return uniqBy(this.entries, 'contactId')
.filter((e: ILedgerEntry) => e.contactId)
.map((e: ILedgerEntry) => e.contactId);
};
/**
* Reverses the ledger entries.
* @returns {Ledger}
*/
public reverse = (): Ledger => {
const newEntries = this.entries.map((e) => {
const credit = e.debit;
const debit = e.credit;
return { ...e, credit, debit };
});
return new Ledger(newEntries);
};
// ---------------------------------
// # STATIC METHODS.
// ----------------------------------
/**
* Mappes the account transactions to ledger entries.
* @param {IAccountTransaction[]} entries
* @returns {ILedgerEntry[]}
*/
static mappingTransactions(entries: IAccountTransaction[]): ILedgerEntry[] {
return entries.map(this.mapTransaction);
}
/**
* Mappes the account transaction to ledger entry.
* @param {IAccountTransaction} entry
* @returns {ILedgerEntry}
*/
static mapTransaction(entry: IAccountTransaction): ILedgerEntry {
return {
credit: defaultTo(entry.credit, 0),
debit: defaultTo(entry.debit, 0),
exchangeRate: entry.exchangeRate,
currencyCode: entry.currencyCode,
accountNormal: entry.account.accountNormal,
accountId: entry.accountId,
contactId: entry.contactId,
date: entry.date,
transactionId: entry.referenceId,
transactionType: entry.referenceType,
transactionNumber: entry.transactionNumber,
referenceNumber: entry.referenceNumber,
index: entry.index,
indexGroup: entry.indexGroup,
entryId: entry.id,
branchId: entry.branchId,
projectId: entry.projectId,
};
}
/**
* Mappes the account transactions to ledger entries.
* @param {IAccountTransaction[]} transactions
* @returns {ILedger}
*/
static fromTransactions(transactions: IAccountTransaction[]): Ledger {
const entries = Ledger.mappingTransactions(transactions);
return new Ledger(entries);
}
}

View File

@@ -0,0 +1,103 @@
import { Service, Inject } from 'typedi';
import async from 'async';
import { Knex } from 'knex';
import { ILedger, ISaleContactsBalanceQueuePayload } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models';
@Service()
export class LedgerContactsBalanceStorage {
@Inject()
private tenancy: HasTenancyService;
/**
*
* @param {number} tenantId -
* @param {ILedger} ledger -
* @param {Knex.Transaction} trx -
* @returns {Promise<void>}
*/
public saveContactsBalance = async (
tenantId: number,
ledger: ILedger,
trx?: Knex.Transaction
): Promise<void> => {
// Save contact balance queue.
const saveContactsBalanceQueue = async.queue(
this.saveContactBalanceTask,
10
);
// Retrieves the effected contacts ids.
const effectedContactsIds = ledger.getContactsIds();
effectedContactsIds.forEach((contactId: number) => {
saveContactsBalanceQueue.push({ tenantId, contactId, ledger, trx });
});
if (effectedContactsIds.length > 0) await saveContactsBalanceQueue.drain();
};
/**
*
* @param {ISaleContactsBalanceQueuePayload} task
* @returns {Promise<void>}
*/
private saveContactBalanceTask = async (
task: ISaleContactsBalanceQueuePayload
) => {
const { tenantId, contactId, ledger, trx } = task;
await this.saveContactBalance(tenantId, ledger, contactId, trx);
};
/**
*
* @param {number} tenantId
* @param {ILedger} ledger
* @param {number} contactId
* @returns {Promise<void>}
*/
private saveContactBalance = async (
tenantId: number,
ledger: ILedger,
contactId: number,
trx?: Knex.Transaction
): Promise<void> => {
const { Contact } = this.tenancy.models(tenantId);
const contact = await Contact.query().findById(contactId);
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Detarmines whether the contact has foreign currency.
const isForeignContact = contact.currencyCode !== tenantMeta.baseCurrency;
// Filters the ledger base on the given contact id.
const contactLedger = ledger.whereContactId(contactId);
const closingBalance = isForeignContact
? contactLedger
.whereCurrencyCode(contact.currencyCode)
.getForeignClosingBalance()
: contactLedger.getClosingBalance();
await this.changeContactBalance(tenantId, contactId, closingBalance, trx);
};
/**
*
* @param {number} tenantId
* @param {number} contactId
* @param {number} change
* @returns
*/
private changeContactBalance = (
tenantId: number,
contactId: number,
change: number,
trx?: Knex.Transaction
) => {
const { Contact } = this.tenancy.models(tenantId);
return Contact.changeAmount({ id: contactId }, 'balance', change, trx);
};
}

View File

@@ -0,0 +1,87 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import async from 'async';
import {
ILedgerEntry,
ISaveLedgerEntryQueuePayload,
ILedger,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { transformLedgerEntryToTransaction } from './utils';
@Service()
export class LedgerEntriesStorage {
@Inject()
tenancy: HasTenancyService;
/**
* Saves entries of the given ledger.
* @param {number} tenantId
* @param {ILedger} ledger
* @param {Knex.Transaction} knex
* @returns {Promise<void>}
*/
public saveEntries = async (
tenantId: number,
ledger: ILedger,
trx?: Knex.Transaction
) => {
const saveEntryQueue = async.queue(this.saveEntryTask, 10);
const entries = ledger.getEntries();
entries.forEach((entry) => {
saveEntryQueue.push({ tenantId, entry, trx });
});
if (entries.length > 0) await saveEntryQueue.drain();
};
/**
* Deletes the ledger entries.
* @param {number} tenantId
* @param {ILedger} ledger
* @param {Knex.Transaction} trx
*/
public deleteEntries = async (
tenantId: number,
ledger: ILedger,
trx?: Knex.Transaction
) => {
const { AccountTransaction } = this.tenancy.models(tenantId);
const entriesIds = ledger
.getEntries()
.filter((e) => e.entryId)
.map((e) => e.entryId);
await AccountTransaction.query(trx).whereIn('id', entriesIds).delete();
};
/**
* Saves the ledger entry to the account transactions repository.
* @param {number} tenantId
* @param {ILedgerEntry} entry
* @returns {Promise<void>}
*/
private saveEntry = async (
tenantId: number,
entry: ILedgerEntry,
trx?: Knex.Transaction
): Promise<void> => {
const { AccountTransaction } = this.tenancy.models(tenantId);
const transaction = transformLedgerEntryToTransaction(entry);
await AccountTransaction.query(trx).insert(transaction);
};
/**
* Save the ledger entry to the transactions repository async task.
* @param {ISaveLedgerEntryQueuePayload} task
* @returns {Promise<void>}
*/
private saveEntryTask = async (
task: ISaveLedgerEntryQueuePayload
): Promise<void> => {
const { entry, tenantId, trx } = task;
await this.saveEntry(tenantId, entry, trx);
};
}

View File

@@ -0,0 +1,61 @@
import { Inject } from 'typedi';
import { castArray } from 'lodash';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import LedgerStorageService from './LedgerStorageService';
import Ledger from './Ledger';
import { Knex } from 'knex';
export class LedgerRevert {
@Inject()
private tenancy: HasTenancyService;
@Inject()
ledgerStorage: LedgerStorageService;
/**
* Reverts the jouranl entries.
* @param {number|number[]} referenceId - Reference id.
* @param {string} referenceType - Reference type.
*/
public getTransactionsByReference = async (
tenantId: number,
referenceId: number | number[],
referenceType: string | string[]
) => {
const { AccountTransaction } = this.tenancy.models(tenantId);
const transactions = await AccountTransaction.query()
.whereIn('reference_type', castArray(referenceType))
.whereIn('reference_id', castArray(referenceId))
.withGraphFetched('account');
return transactions;
};
/**
*
* @param tenantId
* @param referenceId
* @param referenceType
* @param trx
*/
public revertGLEntries = async (
tenantId: number,
referenceId: number | number[],
referenceType: string | string[],
trx?: Knex.Transaction
) => {
//
const transactions = await this.getTransactionsByReference(
tenantId,
referenceId,
referenceType
);
// Creates a new ledger from transaction and reverse the entries.
const ledger = Ledger.fromTransactions(transactions);
const reversedLedger = ledger.reverse();
//
await this.ledgerStorage.commit(tenantId, reversedLedger, trx);
};
}

View File

@@ -0,0 +1,98 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import { ILedger } from '@/interfaces';
import { LedgerContactsBalanceStorage } from './LedgerContactStorage';
import { LedegrAccountsStorage } from './LedgetAccountStorage';
import { LedgerEntriesStorage } from './LedgerEntriesStorage';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import Ledger from './Ledger';
@Service()
export default class LedgerStorageService {
@Inject()
private ledgerEntriesService: LedgerEntriesStorage;
@Inject()
private ledgerContactsBalance: LedgerContactsBalanceStorage;
@Inject()
private ledgerAccountsBalance: LedegrAccountsStorage;
@Inject()
private tenancy: HasTenancyService;
/**
* Commit the ledger to the storage layer as one unit-of-work.
* @param {number} tenantId
* @param {ILedger} ledger
* @returns {Promise<void>}
*/
public commit = async (
tenantId: number,
ledger: ILedger,
trx?: Knex.Transaction
): Promise<void> => {
const tasks = [
// Saves the ledger entries.
this.ledgerEntriesService.saveEntries(tenantId, ledger, trx),
// Mutates the assocaited accounts balances.
this.ledgerAccountsBalance.saveAccountsBalance(tenantId, ledger, trx),
// Mutates the associated contacts balances.
this.ledgerContactsBalance.saveContactsBalance(tenantId, ledger, trx),
];
await Promise.all(tasks);
};
/**
* Deletes the given ledger and revert balances.
* @param {number} tenantId
* @param {ILedger} ledger
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public delete = async (
tenantId: number,
ledger: ILedger,
trx?: Knex.Transaction
) => {
const tasks = [
// Deletes the ledger entries.
this.ledgerEntriesService.deleteEntries(tenantId, ledger, trx),
// Mutates the assocaited accounts balances.
this.ledgerAccountsBalance.saveAccountsBalance(tenantId, ledger, trx),
// Mutates the associated contacts balances.
this.ledgerContactsBalance.saveContactsBalance(tenantId, ledger, trx),
];
await Promise.all(tasks);
};
/**
* @param tenantId
* @param referenceId
* @param referenceType
* @param trx
*/
public deleteByReference = async (
tenantId: number,
referenceId: number | number[],
referenceType: string | string[],
trx?: Knex.Transaction
) => {
const { transactionsRepository } = this.tenancy.repositories(tenantId);
// Retrieves the transactions of the given reference.
const transactions =
await transactionsRepository.getTransactionsByReference(
referenceId,
referenceType
);
// Creates a new ledger from transaction and reverse the entries.
const reversedLedger = Ledger.fromTransactions(transactions).reverse();
// Deletes and reverts the balances.
await this.delete(tenantId, reversedLedger, trx);
};
}

View File

@@ -0,0 +1,155 @@
import { Service, Inject } from 'typedi';
import async from 'async';
import { Knex } from 'knex';
import { uniq } from 'lodash';
import { ILedger, ISaveAccountsBalanceQueuePayload } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models';
@Service()
export class LedegrAccountsStorage {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve depepants ids of the give accounts ids.
* @param {number[]} accountsIds
* @param depGraph
* @returns {number[]}
*/
private getDependantsAccountsIds = (
accountsIds: number[],
depGraph
): number[] => {
const depAccountsIds = [];
accountsIds.forEach((accountId: number) => {
const depAccountIds = depGraph.dependantsOf(accountId);
depAccountsIds.push(accountId, ...depAccountIds);
});
return uniq(depAccountsIds);
};
/**
*
* @param {number} tenantId
* @param {number[]} accountsIds
* @returns {number[]}
*/
private findDependantsAccountsIds = async (
tenantId: number,
accountsIds: number[],
trx?: Knex.Transaction
): Promise<number[]> => {
const { accountRepository } = this.tenancy.repositories(tenantId);
const accountsGraph = await accountRepository.getDependencyGraph(null, trx);
return this.getDependantsAccountsIds(accountsIds, accountsGraph);
};
/**
* Atomic mutation for accounts balances.
* @param {number} tenantId
* @param {ILedger} ledger
* @param {Knex.Transaction} trx -
* @returns {Promise<void>}
*/
public saveAccountsBalance = async (
tenantId: number,
ledger: ILedger,
trx?: Knex.Transaction
): Promise<void> => {
// Initiate a new queue for accounts balance mutation.
const saveAccountsBalanceQueue = async.queue(
this.saveAccountBalanceTask,
10
);
const effectedAccountsIds = ledger.getAccountsIds();
const dependAccountsIds = await this.findDependantsAccountsIds(
tenantId,
effectedAccountsIds,
trx
);
dependAccountsIds.forEach((accountId: number) => {
saveAccountsBalanceQueue.push({ tenantId, ledger, accountId, trx });
});
if (dependAccountsIds.length > 0) {
await saveAccountsBalanceQueue.drain();
}
};
/**
* Async task mutates the given account balance.
* @param {ISaveAccountsBalanceQueuePayload} task
* @returns {Promise<void>}
*/
private saveAccountBalanceTask = async (
task: ISaveAccountsBalanceQueuePayload
): Promise<void> => {
const { tenantId, ledger, accountId, trx } = task;
await this.saveAccountBalanceFromLedger(tenantId, ledger, accountId, trx);
};
/**
* Saves specific account balance from the given ledger.
* @param {number} tenantId
* @param {ILedger} ledger
* @param {number} accountId
* @param {Knex.Transaction} trx -
* @returns {Promise<void>}
*/
private saveAccountBalanceFromLedger = async (
tenantId: number,
ledger: ILedger,
accountId: number,
trx?: Knex.Transaction
): Promise<void> => {
const { Account } = this.tenancy.models(tenantId);
const account = await Account.query(trx).findById(accountId);
// Filters the ledger entries by the current acount.
const accountLedger = ledger.whereAccountId(accountId);
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Detarmines whether the account has foreign currency.
const isAccountForeign = account.currencyCode !== tenantMeta.baseCurrency;
// Calculates the closing foreign balance by the given currency if account was has
// foreign currency otherwise get closing balance.
const closingBalance = isAccountForeign
? accountLedger
.whereCurrencyCode(account.currencyCode)
.getForeignClosingBalance()
: accountLedger.getClosingBalance();
await this.saveAccountBalance(tenantId, accountId, closingBalance, trx);
};
/**
* Saves the account balance.
* @param {number} tenantId
* @param {number} accountId
* @param {number} change
* @param {Knex.Transaction} trx -
* @returns {Promise<void>}
*/
private saveAccountBalance = async (
tenantId: number,
accountId: number,
change: number,
trx?: Knex.Transaction
) => {
const { Account } = this.tenancy.models(tenantId);
// Ensure the account has atleast zero in amount.
await Account.query(trx)
.findById(accountId)
.whereNull('amount')
.patch({ amount: 0 });
await Account.changeAmount({ id: accountId }, 'amount', change, trx);
};
}

View File

@@ -0,0 +1,34 @@
import { IAccountTransaction, ILedgerEntry } from '@/interfaces';
export const transformLedgerEntryToTransaction = (
entry: ILedgerEntry
): IAccountTransaction => {
return {
date: entry.date,
credit: entry.credit,
debit: entry.debit,
currencyCode: entry.currencyCode,
exchangeRate: entry.exchangeRate,
accountId: entry.accountId,
contactId: entry.contactId,
referenceType: entry.transactionType,
referenceId: entry.transactionId,
transactionNumber: entry.transactionNumber,
referenceNumber: entry.referenceNumber,
index: entry.index,
indexGroup: entry.indexGroup,
branchId: entry.branchId,
userId: entry.userId,
itemId: entry.itemId,
projectId: entry.projectId,
costable: entry.costable,
};
};

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

View File

@@ -0,0 +1,9 @@
export class AccountsReceivableRepository {
findOrCreateAccount = (currencyCode?: string) => {
};
}

View File

@@ -0,0 +1,15 @@
import { Service, Inject } from 'typedi';
import { ISystemUser } from '@/interfaces';
@Service()
export default class AuthenticatedAccount {
/**
*
* @param {number} tenantId
* @param {ISystemUser} authorizedUser
* @returns
*/
getAccount = async (tenantId: number, authorizedUser: ISystemUser) => {
return authorizedUser;
};
}

View File

@@ -0,0 +1,73 @@
import { Service } from 'typedi';
import { ISystemUser } from '@/interfaces';
import config from '@/config';
import Mail from '@/lib/Mail';
@Service()
export default class AuthenticationMailMesssages {
/**
* Sends welcome message.
* @param {ISystemUser} user - The system user.
* @param {string} organizationName -
* @return {Promise<void>}
*/
async sendWelcomeMessage(
user: ISystemUser,
organizationId: string
): Promise<void> {
const root = __dirname + '/../../../views/images/bigcapital.png';
const mail = new Mail()
.setView('mail/Welcome.html')
.setSubject('Welcome to Bigcapital')
.setTo(user.email)
.setAttachments([
{
filename: 'bigcapital.png',
path: root,
cid: 'bigcapital_logo',
},
])
.setData({
firstName: user.firstName,
organizationId,
successPhoneNumber: config.customerSuccess.phoneNumber,
successEmail: config.customerSuccess.email,
});
await mail.send();
}
/**
* Sends reset password message.
* @param {ISystemUser} user - The system user.
* @param {string} token - Reset password token.
* @return {Promise<void>}
*/
async sendResetPasswordMessage(
user: ISystemUser,
token: string
): Promise<void> {
const root = __dirname + '/../../../views/images/bigcapital.png';
const mail = new Mail()
.setSubject('Bigcapital - Password Reset')
.setView('mail/ResetPassword.html')
.setTo(user.email)
.setAttachments([
{
filename: 'bigcapital.png',
path: root,
cid: 'bigcapital_logo',
},
])
.setData({
resetPasswordUrl: `${config.baseURL}/auth/reset_password/${token}`,
first_name: user.firstName,
last_name: user.lastName,
contact_us_email: config.contactUsMail,
});
await mail.send();
}
}

View File

@@ -0,0 +1,19 @@
import { Service, Inject } from 'typedi';
import { ISystemUser, ITenant } from '@/interfaces';
@Service()
export default class AuthenticationSMSMessages {
@Inject('SMSClient')
smsClient: any;
/**
* Sends welcome sms message.
* @param {ITenant} tenant
* @param {ISystemUser} user
*/
sendWelcomeMessage(tenant: ITenant, user: ISystemUser) {
const message: string = `Hi ${user.firstName}, Welcome to Bigcapital, You've joined the new workspace, if you need any help please don't hesitate to contact us.`;
return this.smsClient.sendMessage(user.phoneNumber, message);
}
}

View File

@@ -0,0 +1,49 @@
import { RateLimiterClusterMasterPM2, RateLimiterMemory, RateLimiterRedis, RateLimiterRes } from 'rate-limiter-flexible';
export default class RateLimiter {
rateLimiter: RateLimiterRedis;
/**
* Rate limiter redis constructor.
* @param {RateLimiterRedis} rateLimiter
*/
constructor(rateLimiter: RateLimiterMemory) {
this.rateLimiter = rateLimiter;
}
/**
*
* @return {boolean}
*/
public attempt(key: string, pointsToConsume = 1): Promise<RateLimiterRes> {
return this.rateLimiter.consume(key, pointsToConsume);
}
/**
* Increment the counter for a given key for a given decay time.
* @param {string} key -
*/
public hit(
key: string | number,
points: number,
secDuration: number,
): Promise<RateLimiterRes> {
return this.rateLimiter.penalty(key, points, secDuration);
}
/**
* Retrieve the rate limiter response of the given key.
* @param {string} key
*/
public get(key: string): Promise<RateLimiterRes> {
return this.rateLimiter.get(key);
}
/**
* Resets the rate limiter of the given key.
* @param key
*/
public reset(key: string): Promise<boolean> {
return this.rateLimiter.delete(key);
}
}

View File

@@ -0,0 +1,322 @@
import { Service, Inject, Container } from 'typedi';
import JWT from 'jsonwebtoken';
import uniqid from 'uniqid';
import { omit, cloneDeep } from 'lodash';
import moment from 'moment';
import { PasswordReset, Tenant } from '@/system/models';
import {
IRegisterDTO,
ITenant,
ISystemUser,
IPasswordReset,
IAuthenticationService,
} from '@/interfaces';
import { hashPassword } from 'utils';
import { ServiceError, ServiceErrors } from '@/exceptions';
import config from '@/config';
import events from '@/subscribers/events';
import AuthenticationMailMessages from '@/services/Authentication/AuthenticationMailMessages';
import TenantsManager from '@/services/Tenancy/TenantsManager';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
const ERRORS = {
INVALID_DETAILS: 'INVALID_DETAILS',
USER_INACTIVE: 'USER_INACTIVE',
EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND',
TOKEN_INVALID: 'TOKEN_INVALID',
USER_NOT_FOUND: 'USER_NOT_FOUND',
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
EMAIL_EXISTS: 'EMAIL_EXISTS',
};
@Service()
export default class AuthenticationService implements IAuthenticationService {
@Inject('logger')
logger: any;
@Inject()
eventPublisher: EventPublisher;
@Inject()
mailMessages: AuthenticationMailMessages;
@Inject('repositories')
sysRepositories: any;
@Inject()
tenantsManager: TenantsManager;
/**
* Signin and generates JWT token.
* @throws {ServiceError}
* @param {string} emailOrPhone - Email or phone number.
* @param {string} password - Password.
* @return {Promise<{user: IUser, token: string}>}
*/
public async signIn(
emailOrPhone: string,
password: string
): Promise<{
user: ISystemUser;
token: string;
tenant: ITenant;
}> {
this.logger.info('[login] Someone trying to login.', {
emailOrPhone,
password,
});
const { systemUserRepository } = this.sysRepositories;
const loginThrottler = Container.get('rateLimiter.login');
// Finds the user of the given email or phone number.
const user = await systemUserRepository.findByCrediential(emailOrPhone);
if (!user) {
// Hits the loging throttler to the given crediential.
await loginThrottler.hit(emailOrPhone);
this.logger.info('[login] invalid data');
throw new ServiceError(ERRORS.INVALID_DETAILS);
}
this.logger.info('[login] check password validation.', {
emailOrPhone,
password,
});
if (!user.verifyPassword(password)) {
// Hits the loging throttler to the given crediential.
await loginThrottler.hit(emailOrPhone);
throw new ServiceError(ERRORS.INVALID_DETAILS);
}
if (!user.active) {
this.logger.info('[login] user inactive.', { userId: user.id });
throw new ServiceError(ERRORS.USER_INACTIVE);
}
this.logger.info('[login] generating JWT token.', { userId: user.id });
const token = this.generateToken(user);
this.logger.info('[login] updating user last login at.', {
userId: user.id,
});
await systemUserRepository.patchLastLoginAt(user.id);
this.logger.info('[login] Logging success.', { user, token });
// Triggers `onLogin` event.
await this.eventPublisher.emitAsync(events.auth.login, {
emailOrPhone,
password,
user,
});
const tenant = await Tenant.query().findById(user.tenantId).withGraphFetched('metadata');
// Keep the user object immutable.
const outputUser = cloneDeep(user);
// Remove password property from user object.
Reflect.deleteProperty(outputUser, 'password');
return { user: outputUser, token, tenant };
}
/**
* Validates email and phone number uniqiness on the storage.
* @throws {ServiceErrors}
* @param {IRegisterDTO} registerDTO - Register data object.
*/
private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) {
const { systemUserRepository } = this.sysRepositories;
const isEmailExists = await systemUserRepository.findOneByEmail(
registerDTO.email
);
const isPhoneExists = await systemUserRepository.findOneByPhoneNumber(
registerDTO.phoneNumber
);
const errorReasons: ServiceError[] = [];
if (isPhoneExists) {
this.logger.info('[register] phone number exists on the storage.');
errorReasons.push(new ServiceError(ERRORS.PHONE_NUMBER_EXISTS));
}
if (isEmailExists) {
this.logger.info('[register] email exists on the storage.');
errorReasons.push(new ServiceError(ERRORS.EMAIL_EXISTS));
}
if (errorReasons.length > 0) {
throw new ServiceErrors(errorReasons);
}
}
/**
* Registers a new tenant with user from user input.
* @throws {ServiceErrors}
* @param {IUserDTO} user
*/
public async register(registerDTO: IRegisterDTO): Promise<ISystemUser> {
this.logger.info('[register] Someone trying to register.');
await this.validateEmailAndPhoneUniqiness(registerDTO);
this.logger.info('[register] Creating a new tenant organization.');
const tenant = await this.newTenantOrganization();
this.logger.info('[register] Trying hashing the password.');
const hashedPassword = await hashPassword(registerDTO.password);
const { systemUserRepository } = this.sysRepositories;
const registeredUser = await systemUserRepository.create({
...omit(registerDTO, 'country'),
active: true,
password: hashedPassword,
tenantId: tenant.id,
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
});
// Triggers `onRegister` event.
await this.eventPublisher.emitAsync(events.auth.register, {
registerDTO,
tenant,
user: registeredUser,
});
return registeredUser;
}
/**
* Generates and insert new tenant organization id.
* @async
* @return {Promise<ITenant>}
*/
private async newTenantOrganization(): Promise<ITenant> {
return this.tenantsManager.createTenant();
}
/**
* Validate the given email existance on the storage.
* @throws {ServiceError}
* @param {string} email - email address.
*/
private async validateEmailExistance(email: string): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
const userByEmail = await systemUserRepository.findOneByEmail(email);
if (!userByEmail) {
this.logger.info('[send_reset_password] The given email not found.');
throw new ServiceError(ERRORS.EMAIL_NOT_FOUND);
}
return userByEmail;
}
/**
* Generates and retrieve password reset token for the given user email.
* @param {string} email
* @return {<Promise<IPasswordReset>}
*/
public async sendResetPassword(email: string): Promise<IPasswordReset> {
this.logger.info('[send_reset_password] Trying to send reset password.');
const user = await this.validateEmailExistance(email);
// Delete all stored tokens of reset password that associate to the give email.
this.logger.info(
'[send_reset_password] trying to delete all tokens by email.'
);
this.deletePasswordResetToken(email);
const token: string = uniqid();
this.logger.info('[send_reset_password] insert the generated token.');
const passwordReset = await PasswordReset.query().insert({ email, token });
// Triggers `onSendResetPassword` event.
await this.eventPublisher.emitAsync(events.auth.sendResetPassword, {
user,
token,
});
return passwordReset;
}
/**
* Resets a user password from given token.
* @param {string} token - Password reset token.
* @param {string} password - New Password.
* @return {Promise<void>}
*/
public async resetPassword(token: string, password: string): Promise<void> {
const { systemUserRepository } = this.sysRepositories;
// Finds the password reset token.
const tokenModel: IPasswordReset = await PasswordReset.query().findOne(
'token',
token
);
// In case the password reset token not found throw token invalid error..
if (!tokenModel) {
this.logger.info('[reset_password] token invalid.');
throw new ServiceError(ERRORS.TOKEN_INVALID);
}
// Different between tokne creation datetime and current time.
if (
moment().diff(tokenModel.createdAt, 'seconds') >
config.resetPasswordSeconds
) {
this.logger.info('[reset_password] token expired.');
// Deletes the expired token by expired token email.
await this.deletePasswordResetToken(tokenModel.email);
throw new ServiceError(ERRORS.TOKEN_EXPIRED);
}
const user = await systemUserRepository.findOneByEmail(tokenModel.email);
if (!user) {
throw new ServiceError(ERRORS.USER_NOT_FOUND);
}
const hashedPassword = await hashPassword(password);
this.logger.info('[reset_password] saving a new hashed password.');
await systemUserRepository.update(
{ password: hashedPassword },
{ id: user.id }
);
// Deletes the used token.
await this.deletePasswordResetToken(tokenModel.email);
// Triggers `onResetPassword` event.
await this.eventPublisher.emitAsync(events.auth.resetPassword, {
user,
token,
password,
});
this.logger.info('[reset_password] reset password success.');
}
/**
* Deletes the password reset token by the given email.
* @param {string} email
* @returns {Promise}
*/
private async deletePasswordResetToken(email: string) {
this.logger.info('[reset_password] trying to delete all tokens by email.');
return PasswordReset.query().where('email', email).delete();
}
/**
* Generates JWT token for the given user.
* @param {ISystemUser} user
* @return {string} token
*/
generateToken(user: ISystemUser): string {
const today = new Date();
const exp = new Date(today);
exp.setDate(today.getDate() + 60);
this.logger.silly(`Sign JWT for userId: ${user.id}`);
return JWT.sign(
{
id: user.id, // We are gonna use this in the middleware 'isAuth'
exp: exp.getTime() / 1000,
},
config.jwtSecret
);
}
}

View File

@@ -0,0 +1,90 @@
import { Service, Inject } from 'typedi';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import {
IBranchesActivatedPayload,
IBranchesActivatePayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import { CreateBranch } from './CreateBranch';
import { BranchesSettings } from './BranchesSettings';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class ActivateBranches {
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private createBranch: CreateBranch;
@Inject()
private branchesSettings: BranchesSettings;
/**
* Throws service error if multi-branches feature is already activated.
* @param {boolean} isActivated
*/
private throwIfMultiBranchesActivated = (isActivated: boolean) => {
if (isActivated) {
throw new ServiceError(ERRORS.MUTLI_BRANCHES_ALREADY_ACTIVATED);
}
};
/**
* Creates a new initial branch.
* @param {number} tenantId
*/
private createInitialBranch = (tenantId: number) => {
const { __ } = this.tenancy.i18n(tenantId);
return this.createBranch.createBranch(tenantId, {
name: __('branches.head_branch'),
code: '10001',
primary: true,
});
};
/**
* Activate multi-branches feature.
* @param {number} tenantId
* @returns {Promise<void>}
*/
public activateBranches = (tenantId: number): Promise<void> => {
const isActivated = this.branchesSettings.isMultiBranchesActive(tenantId);
// Throw error if mutli-branches is already activated.
this.throwIfMultiBranchesActivated(isActivated);
// Activate multi-branches under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBranchActivate` branch.
await this.eventPublisher.emitAsync(events.branch.onActivate, {
tenantId,
trx,
} as IBranchesActivatePayload);
// Create a new branch as primary branch.
const primaryBranch = await this.createInitialBranch(tenantId);
// Mark the mutli-branches is activated.
await this.branchesSettings.markMultiBranchesAsActivated(tenantId);
// Triggers `onBranchActivated` branch.
await this.eventPublisher.emitAsync(events.branch.onActivated, {
tenantId,
primaryBranch,
trx,
} as IBranchesActivatedPayload);
});
};
}

View File

@@ -0,0 +1,35 @@
import { Request, Response, NextFunction } from 'express';
import { ServiceError } from '@/exceptions';
/**
* Handles branches integration service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
export function BranchIntegrationErrorsMiddleware(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'WAREHOUSE_ID_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'WAREHOUSE_ID_NOT_FOUND', code: 5000 }],
});
}
if (error.errorType === 'BRANCH_ID_REQUIRED') {
return res.boom.badRequest(null, {
errors: [{ type: 'BRANCH_ID_REQUIRED', code: 5100 }],
});
}
if (error.errorType === 'BRANCH_ID_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'BRANCH_ID_NOT_FOUND', code: 5300 }],
});
}
}
next(error);
}

View File

@@ -0,0 +1,52 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
@Service()
export class BranchValidator {
@Inject()
tenancy: HasTenancyService;
public validateBranchNotOnlyWarehouse = async (
tenantId: number,
branchId: number
) => {
const { Branch } = this.tenancy.models(tenantId);
const warehouses = await Branch.query().whereNot('id', branchId);
if (warehouses.length === 0) {
throw new ServiceError(ERRORS.COULD_NOT_DELETE_ONLY_BRANCH);
}
};
/**
* Validates the given branch whether is unique.
* @param {number} tenantId
* @param {string} code
* @param {number} exceptBranchId
*/
public validateBranchCodeUnique = async (
tenantId: number,
code: string,
exceptBranchId?: number
): Promise<void> => {
const { Branch } = this.tenancy.models(tenantId);
const branch = await Branch.query()
.onBuild((query) => {
query.select(['id']);
query.where('code', code);
if (exceptBranchId) {
query.whereNot('id', exceptBranchId);
}
})
.first();
if (branch) {
throw new ServiceError(ERRORS.BRANCH_CODE_NOT_UNIQUE);
}
};
}

View File

@@ -0,0 +1,112 @@
import { IBranch, ICreateBranchDTO, IEditBranchDTO } from '@/interfaces';
import { Service, Inject } from 'typedi';
import { ActivateBranches } from './ActivateBranches';
import { CreateBranch } from './CreateBranch';
import { DeleteBranch } from './DeleteBranch';
import { EditBranch } from './EditBranch';
import { GetBranch } from './GetBranch';
import { GetBranches } from './GetBranches';
import { MarkBranchAsPrimary } from './MarkBranchAsPrimary';
@Service()
export class BranchesApplication {
@Inject()
private deleteBranchService: DeleteBranch;
@Inject()
private createBranchService: CreateBranch;
@Inject()
private getBranchService: GetBranch;
@Inject()
private editBranchService: EditBranch;
@Inject()
private getBranchesService: GetBranches;
@Inject()
private activateBranchesService: ActivateBranches;
@Inject()
private markBranchAsPrimaryService: MarkBranchAsPrimary;
/**
* Retrieves branches list.
* @param {number} tenantId
* @returns {IBranch}
*/
public getBranches = (tenantId: number): Promise<IBranch[]> => {
return this.getBranchesService.getBranches(tenantId);
};
/**
* Retrieves the given branch details.
* @param {number} tenantId - Tenant id.
* @param {number} branchId - Branch id.
* @returns {Promise<IBranch>}
*/
public getBranch = (tenantId: number, branchId: number): Promise<IBranch> => {
return this.getBranchService.getBranch(tenantId, branchId);
};
/**
* Creates a new branch.
* @param {number} tenantId -
* @param {ICreateBranchDTO} createBranchDTO
* @returns {Promise<IBranch>}
*/
public createBranch = (
tenantId: number,
createBranchDTO: ICreateBranchDTO
): Promise<IBranch> => {
return this.createBranchService.createBranch(tenantId, createBranchDTO);
};
/**
* Edits the given branch.
* @param {number} tenantId - Tenant id.
* @param {number} branchId - Branch id.
* @param {IEditBranchDTO} editBranchDTO - Edit branch DTO.
* @returns {Promise<IBranch>}
*/
public editBranch = (
tenantId: number,
branchId: number,
editBranchDTO: IEditBranchDTO
): Promise<IBranch> => {
return this.editBranchService.editBranch(tenantId, branchId, editBranchDTO);
};
/**
* Deletes the given branch.
* @param {number} tenantId - Tenant id.
* @param {number} branchId - Branch id.
* @returns {Promise<void>}
*/
public deleteBranch = (tenantId: number, branchId: number): Promise<void> => {
return this.deleteBranchService.deleteBranch(tenantId, branchId);
};
/**
* Activates the given branches.
* @param {number} tenantId - Tenant id.
* @returns {Promise<void>}
*/
public activateBranches = (tenantId: number): Promise<void> => {
return this.activateBranchesService.activateBranches(tenantId);
};
/**
* Marks the given branch as primary.
* @param {number} tenantId
* @param {number} branchId
* @returns {Promise<IBranch>}
*/
public markBranchAsPrimary = async (
tenantId: number,
branchId: number
): Promise<IBranch> => {
return this.markBranchAsPrimaryService.markAsPrimary(tenantId, branchId);
};
}

View File

@@ -0,0 +1,29 @@
import { Service, Inject } from 'typedi';
import { Features } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class BranchesSettings {
@Inject()
private tenancy: HasTenancyService;
/**
* Marks multi-branches as activated.
* @param {number} tenantId -
*/
public markMultiBranchesAsActivated = (tenantId: number) => {
const settings = this.tenancy.settings(tenantId);
settings.set({ group: 'features', key: Features.BRANCHES, value: 1 });
};
/**
* Retrieves whether multi-branches is active.
* @param {number} tenantId
*/
public isMultiBranchesActive = (tenantId: number) => {
const settings = this.tenancy.settings(tenantId);
return settings.get({ group: 'features', key: Features.BRANCHES });
};
}

View File

@@ -0,0 +1,30 @@
import { Inject } from "typedi";
import { ServiceError } from "exceptions";
import HasTenancyService from "services/Tenancy/TenancyService";
import { ERRORS } from "./constants";
export class CURDBranch {
@Inject()
tenancy: HasTenancyService;
/**
*
* @param branch
*/
throwIfBranchNotFound = (branch) => {
if (!branch) {
throw new ServiceError(ERRORS.BRANCH_NOT_FOUND);
}
}
getBranchOrThrowNotFound = async (tenantId: number, branchId: number) => {
const { Branch } = this.tenancy.models(tenantId);
const foundBranch = await Branch.query().findById(branchId);
if (!foundBranch) {
throw new ServiceError(ERRORS.BRANCH_NOT_FOUND);
}
return foundBranch;
}
}

View File

@@ -0,0 +1,64 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
IBranch,
IBranchCreatedPayload,
IBranchCreatePayload,
ICreateBranchDTO,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { BranchValidator } from './BranchValidate';
@Service()
export class CreateBranch {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private validator: BranchValidator;
/**
* Creates a new branch.
* @param {number} tenantId
* @param {ICreateBranchDTO} createBranchDTO
* @returns {Promise<IBranch>}
*/
public createBranch = (
tenantId: number,
createBranchDTO: ICreateBranchDTO
): Promise<IBranch> => {
const { Branch } = this.tenancy.models(tenantId);
// Creates a new branch under unit-of-work.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBranchCreate` event.
await this.eventPublisher.emitAsync(events.warehouse.onEdit, {
tenantId,
createBranchDTO,
trx,
} as IBranchCreatePayload);
const branch = await Branch.query().insertAndFetch({
...createBranchDTO,
});
// Triggers `onBranchCreated` event.
await this.eventPublisher.emitAsync(events.warehouse.onEdited, {
tenantId,
createBranchDTO,
branch,
trx,
} as IBranchCreatedPayload);
return branch;
});
};
}

View File

@@ -0,0 +1,76 @@
import { Service, Inject } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import { IBranchDeletedPayload, IBranchDeletePayload } from '@/interfaces';
import { CURDBranch } from './CRUDBranch';
import { BranchValidator } from './BranchValidate';
import { ERRORS } from './constants';
@Service()
export class DeleteBranch extends CURDBranch {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private validator: BranchValidator;
/**
* Validates the branch deleteing.
* @param {number} tenantId
* @param {number} branchId
* @returns {Promise<void>}
*/
private authorize = async (tenantId: number, branchId: number) => {
await this.validator.validateBranchNotOnlyWarehouse(tenantId, branchId);
};
/**
* Deletes branch.
* @param {number} tenantId
* @param {number} branchId
* @returns {Promise<void>}
*/
public deleteBranch = async (
tenantId: number,
branchId: number
): Promise<void> => {
const { Branch } = this.tenancy.models(tenantId);
// Retrieves the old branch or throw not found service error.
const oldBranch = await Branch.query()
.findById(branchId)
.throwIfNotFound()
.queryAndThrowIfHasRelations({
type: ERRORS.BRANCH_HAS_ASSOCIATED_TRANSACTIONS,
});
// Authorize the branch before deleting.
await this.authorize(tenantId, branchId);
// Deletes branch under unit-of-work.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBranchCreate` event.
await this.eventPublisher.emitAsync(events.warehouse.onEdit, {
tenantId,
oldBranch,
trx,
} as IBranchDeletePayload);
await Branch.query().findById(branchId).delete();
// Triggers `onBranchCreate` event.
await this.eventPublisher.emitAsync(events.warehouse.onEdited, {
tenantId,
oldBranch,
trx,
} as IBranchDeletedPayload);
});
};
}

View File

@@ -0,0 +1,65 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
IBranchEditedPayload,
IBranchEditPayload,
IEditBranchDTO,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { CURDBranch } from './CRUDBranch';
import events from '@/subscribers/events';
@Service()
export class EditBranch extends CURDBranch {
@Inject()
tenancy: HasTenancyService;
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
/**
* Edits branch.
* @param {number} tenantId
* @param {number} branchId
* @param editBranchDTO
*/
public editBranch = async (
tenantId: number,
branchId: number,
editBranchDTO: IEditBranchDTO
) => {
const { Branch } = this.tenancy.models(tenantId);
// Retrieves the old branch or throw not found service error.
const oldBranch = await this.getBranchOrThrowNotFound(tenantId, branchId);
// Deletes branch under unit-of-work.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBranchEdit` event.
await this.eventPublisher.emitAsync(events.warehouse.onEdit, {
tenantId,
oldBranch,
trx,
} as IBranchEditPayload);
// Edits the branch on the storage.
const branch = await Branch.query().patchAndFetchById(branchId, {
...editBranchDTO,
});
// Triggers `onBranchEdited` event.
await this.eventPublisher.emitAsync(events.warehouse.onEdited, {
tenantId,
oldBranch,
branch,
trx,
} as IBranchEditedPayload);
return branch;
});
};
}

View File

@@ -0,0 +1,50 @@
import {
CreditNoteActivateBranchesSubscriber,
PaymentReceiveActivateBranchesSubscriber,
SaleEstimatesActivateBranchesSubscriber,
SaleInvoicesActivateBranchesSubscriber,
PaymentMadeActivateBranchesSubscriber,
SaleReceiptsActivateBranchesSubscriber,
} from './Subscribers/Activate';
import {
BillBranchValidateSubscriber,
VendorCreditBranchValidateSubscriber,
PaymentMadeBranchValidateSubscriber,
SaleEstimateBranchValidateSubscriber,
CreditNoteBranchValidateSubscriber,
ExpenseBranchValidateSubscriber,
SaleReceiptBranchValidateSubscriber,
ManualJournalBranchValidateSubscriber,
PaymentReceiveBranchValidateSubscriber,
CreditNoteRefundBranchValidateSubscriber,
CashflowBranchDTOValidatorSubscriber,
VendorCreditRefundBranchValidateSubscriber,
InvoiceBranchValidateSubscriber,
ContactBranchValidateSubscriber,
InventoryAdjustmentBranchValidateSubscriber
} from './Subscribers/Validators';
export default () => [
BillBranchValidateSubscriber,
CreditNoteBranchValidateSubscriber,
ExpenseBranchValidateSubscriber,
PaymentMadeBranchValidateSubscriber,
SaleReceiptBranchValidateSubscriber,
VendorCreditBranchValidateSubscriber,
SaleEstimateBranchValidateSubscriber,
ManualJournalBranchValidateSubscriber,
PaymentReceiveBranchValidateSubscriber,
CreditNoteRefundBranchValidateSubscriber,
VendorCreditRefundBranchValidateSubscriber,
CreditNoteActivateBranchesSubscriber,
PaymentReceiveActivateBranchesSubscriber,
SaleEstimatesActivateBranchesSubscriber,
SaleInvoicesActivateBranchesSubscriber,
PaymentMadeActivateBranchesSubscriber,
SaleReceiptsActivateBranchesSubscriber,
CashflowBranchDTOValidatorSubscriber,
InvoiceBranchValidateSubscriber,
ContactBranchValidateSubscriber,
InventoryAdjustmentBranchValidateSubscriber
];

View File

@@ -0,0 +1,26 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Service, Inject } from 'typedi';
import { CURDBranch } from './CRUDBranch';
@Service()
export class GetBranch extends CURDBranch{
@Inject()
tenancy: HasTenancyService;
/**
*
* @param {number} tenantId
* @param {number} branchId
* @returns
*/
public getBranch = async (tenantId: number, branchId: number) => {
const { Branch } = this.tenancy.models(tenantId);
const branch = await Branch.query().findById(branchId);
// Throw not found service error if the branch not found.
this.throwIfBranchNotFound(branch);
return branch;
};
}

View File

@@ -0,0 +1,22 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Service, Inject } from 'typedi';
@Service()
export class GetBranches {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieves branches list.
* @param {number} tenantId
* @param {number} branchId
* @returns
*/
public getBranches = async (tenantId: number) => {
const { Branch } = this.tenancy.models(tenantId);
const branches = await Branch.query().orderBy('name', 'DESC');
return branches;
};
}

View File

@@ -0,0 +1,35 @@
import { Service, Inject } from 'typedi';
import { omit } from 'lodash';
import { BranchesSettings } from '../BranchesSettings';
@Service()
export class BranchTransactionDTOTransform {
@Inject()
branchesSettings: BranchesSettings;
/**
* Excludes DTO branch id when mutli-warehouses feature is inactive.
* @param {number} tenantId
* @returns {any}
*/
private excludeDTOBranchIdWhenInactive = <T extends { branchId?: number }>(
tenantId: number,
DTO: T
): Omit<T, 'branchId'> | T => {
const isActive = this.branchesSettings.isMultiBranchesActive(tenantId);
return !isActive ? omit(DTO, ['branchId']) : DTO;
};
/**
* Transformes the input DTO for branches feature.
* @param {number} tenantId -
* @param {T} DTO -
* @returns {Omit<T, 'branchId'> | T}
*/
public transformDTO =
<T extends { branchId?: number }>(tenantId: number) =>
(DTO: T): Omit<T, 'branchId'> | T => {
return this.excludeDTOBranchIdWhenInactive<T>(tenantId, DTO);
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class CashflowTransactionsActivateBranches {
@Inject()
private tenancy: HasTenancyService;
/**
* Updates all cashflow transactions with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updateCashflowTransactionsWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { CashflowTransaction } = this.tenancy.models(tenantId);
// Updates the cashflow transactions with primary branch.
await CashflowTransaction.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class ExpensesActivateBranches {
@Inject()
private tenancy: HasTenancyService;
/**
* Updates all expenses transactions with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updateExpensesWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { Expense } = this.tenancy.models(tenantId);
// Updates the expenses with primary branch.
await Expense.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class ManualJournalsActivateBranches {
@Inject()
private tenancy: HasTenancyService;
/**
* Updates all manual journals transactions with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updateManualJournalsWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { ManualJournal } = this.tenancy.models(tenantId);
// Updates the manual journal with primary branch.
await ManualJournal.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,32 @@
import { omit } from 'lodash';
import { Inject, Service } from 'typedi';
import { IManualJournal } from '@/interfaces';
import { BranchesSettings } from '../../BranchesSettings';
@Service()
export class ManualJournalBranchesDTOTransformer {
@Inject()
branchesSettings: BranchesSettings;
private excludeDTOBranchIdWhenInactive = (
tenantId: number,
DTO: IManualJournal
): IManualJournal => {
const isActive = this.branchesSettings.isMultiBranchesActive(tenantId);
if (isActive) return DTO;
return {
...DTO,
entries: DTO.entries.map((e) => omit(e, ['branchId'])),
};
};
/**
*
*/
public transformDTO =
(tenantId: number) =>
(DTO: IManualJournal): IManualJournal => {
return this.excludeDTOBranchIdWhenInactive(tenantId, DTO);
};
}

View File

@@ -0,0 +1,23 @@
import { Service, Inject } from 'typedi';
import { ServiceError } from '@/exceptions';
import { IManualJournalDTO, IManualJournalEntryDTO } from '@/interfaces';
import { ERRORS } from './constants';
@Service()
export class ManualJournalBranchesValidator {
/**
* Validates the DTO entries should have branch id.
* @param {IManualJournalDTO} manualJournalDTO
*/
public validateEntriesHasBranchId = async (
manualJournalDTO: IManualJournalDTO
) => {
const hasNoIdEntries = manualJournalDTO.entries.filter(
(entry: IManualJournalEntryDTO) =>
!entry.branchId && !manualJournalDTO.branchId
);
if (hasNoIdEntries.length > 0) {
throw new ServiceError(ERRORS.MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID);
}
};
}

View File

@@ -0,0 +1,4 @@
export const ERRORS = {
MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID:
'MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID',
};

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class BillActivateBranches {
@Inject()
private tenancy: HasTenancyService;
/**
* Updates all bills transactions with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updateBillsWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { Bill } = this.tenancy.models(tenantId);
// Updates the sale invoice with primary branch.
await Bill.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class BillPaymentsActivateBranches {
@Inject()
tenancy: HasTenancyService;
/**
* Updates all bills payments transcations with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updateBillPaymentsWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { BillPayment } = this.tenancy.models(tenantId);
// Updates the bill payments with primary branch.
await BillPayment.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class VendorCreditActivateBranches {
@Inject()
tenancy: HasTenancyService;
/**
* Updates all vendor credits transcations with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updateVendorCreditsWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { VendorCredit } = this.tenancy.models(tenantId);
// Updates the vendors credits with primary branch.
await VendorCredit.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class CreditNoteActivateBranches {
@Inject()
private tenancy: HasTenancyService;
/**
* Updates all creidt notes transactions with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updateCreditsWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { CreditNote } = this.tenancy.models(tenantId);
// Updates the sale invoice with primary branch.
await CreditNote.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class PaymentReceiveActivateBranches {
@Inject()
tenancy: HasTenancyService;
/**
* Updates all creidt notes transactions with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updatePaymentsWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { PaymentReceive } = this.tenancy.models(tenantId);
// Updates the sale invoice with primary branch.
await PaymentReceive.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class SaleEstimateActivateBranches {
@Inject()
tenancy: HasTenancyService;
/**
* Updates all sale estimates transactions with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updateEstimatesWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { PaymentReceive } = this.tenancy.models(tenantId);
// Updates the sale invoice with primary branch.
await PaymentReceive.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class SaleInvoiceActivateBranches {
@Inject()
private tenancy: HasTenancyService;
/**
* Updates all sale invoices transactions with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updateInvoicesWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Updates the sale invoice with primary branch.
await SaleInvoice.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,26 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class SaleReceiptActivateBranches {
@Inject()
tenancy: HasTenancyService;
/**
* Updates all sale receipts transactions with the primary branch.
* @param {number} tenantId
* @param {number} primaryBranchId
* @returns {Promise<void>}
*/
public updateReceiptsWithBranch = async (
tenantId: number,
primaryBranchId: number,
trx?: Knex.Transaction
) => {
const { SaleReceipt } = this.tenancy.models(tenantId);
// Updates the sale receipt with primary branch.
await SaleReceipt.query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -0,0 +1,74 @@
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Service, Inject } from 'typedi';
import { BranchesSettings } from '../BranchesSettings';
import { ERRORS } from './constants';
@Service()
export class ValidateBranchExistance {
@Inject()
tenancy: HasTenancyService;
@Inject()
branchesSettings: BranchesSettings;
/**
* Validate transaction branch id when the feature is active.
* @param {number} tenantId
* @param {number} branchId
* @returns {Promise<void>}
*/
public validateTransactionBranchWhenActive = async (
tenantId: number,
branchId: number | null
) => {
const isActive = this.branchesSettings.isMultiBranchesActive(tenantId);
// Can't continue if the multi-warehouses feature is inactive.
if (!isActive) return;
return this.validateTransactionBranch(tenantId, branchId);
};
/**
* Validate transaction branch id existance.
* @param {number} tenantId
* @param {number} branchId
* @return {Promise<void>}
*/
public validateTransactionBranch = async (
tenantId: number,
branchId: number | null
) => {
//
this.validateBranchIdExistance(branchId);
//
await this.validateBranchExistance(tenantId, branchId);
};
/**
*
* @param branchId
*/
public validateBranchIdExistance = (branchId: number | null) => {
if (!branchId) {
throw new ServiceError(ERRORS.BRANCH_ID_REQUIRED);
}
};
/**
*
* @param tenantId
* @param branchId
*/
public validateBranchExistance = async (tenantId: number, branchId: number) => {
const { Branch } = this.tenancy.models(tenantId);
const branch = await Branch.query().findById(branchId);
if (!branch) {
throw new ServiceError(ERRORS.BRANCH_ID_NOT_FOUND);
}
};
}

View File

@@ -0,0 +1,6 @@
export const ERRORS = {
BRANCH_ID_REQUIRED: 'BRANCH_ID_REQUIRED',
BRANCH_ID_NOT_FOUND: 'BRANCH_ID_NOT_FOUND'
}

View File

@@ -0,0 +1,67 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { CURDBranch } from './CRUDBranch';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import {
IBranch,
IBranchMarkAsPrimaryPayload,
IBranchMarkedAsPrimaryPayload,
} from '@/interfaces';
@Service()
export class MarkBranchAsPrimary extends CURDBranch {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Marks the given branch as primary.
* @param {number} tenantId
* @param {number} branchId
* @returns {Promise<IBranch>}
*/
public markAsPrimary = async (
tenantId: number,
branchId: number
): Promise<IBranch> => {
const { Branch } = this.tenancy.models(tenantId);
// Retrieves the old branch or throw not found service error.
const oldBranch = await this.getBranchOrThrowNotFound(tenantId, branchId);
// Updates the branches under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBranchMarkPrimary` event.
await this.eventPublisher.emitAsync(events.branch.onMarkPrimary, {
tenantId,
oldBranch,
trx,
} as IBranchMarkAsPrimaryPayload);
// Updates all branches as not primary.
await Branch.query(trx).update({ primary: false });
// Updates the given branch as primary.
const markedBranch = await Branch.query(trx).patchAndFetchById(branchId, {
primary: true,
});
// Triggers `onBranchMarkedPrimary` event.
await this.eventPublisher.emitAsync(events.branch.onMarkedPrimary, {
tenantId,
markedBranch,
oldBranch,
trx,
} as IBranchMarkedAsPrimaryPayload);
return markedBranch;
});
};
}

View File

@@ -0,0 +1,38 @@
import { IBranchesActivatedPayload } from '@/interfaces';
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import { CashflowTransactionsActivateBranches } from '../../Integrations/Cashflow/CashflowActivateBranches';
@Service()
export class CreditNoteActivateBranchesSubscriber {
@Inject()
private cashflowActivateBranches: CashflowTransactionsActivateBranches;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.branch.onActivated,
this.updateCashflowWithBranchOnActivated
);
return bus;
}
/**
* Updates accounts transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
private updateCashflowWithBranchOnActivated = async ({
tenantId,
primaryBranch,
trx,
}: IBranchesActivatedPayload) => {
await this.cashflowActivateBranches.updateCashflowTransactionsWithBranch(
tenantId,
primaryBranch.id,
trx
);
};
}

View File

@@ -0,0 +1,38 @@
import { IBranchesActivatedPayload } from '@/interfaces';
import { Service, Inject } from 'typedi';
import { CreditNoteActivateBranches } from '../../Integrations/Sales/CreditNoteBranchesActivate';
import events from '@/subscribers/events';
@Service()
export class CreditNoteActivateBranchesSubscriber {
@Inject()
private creditNotesActivateBranches: CreditNoteActivateBranches;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.branch.onActivated,
this.updateCreditNoteWithBranchOnActivated
);
return bus;
}
/**
* Updates accounts transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
private updateCreditNoteWithBranchOnActivated = async ({
tenantId,
primaryBranch,
trx,
}: IBranchesActivatedPayload) => {
await this.creditNotesActivateBranches.updateCreditsWithBranch(
tenantId,
primaryBranch.id,
trx
);
};
}

View File

@@ -0,0 +1,38 @@
import { IBranchesActivatedPayload } from '@/interfaces';
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import { ExpensesActivateBranches } from '../../Integrations/Expense/ExpensesActivateBranches';
@Service()
export class ExpenseActivateBranchesSubscriber {
@Inject()
private expensesActivateBranches: ExpensesActivateBranches;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.branch.onActivated,
this.updateExpensesWithBranchOnActivated
);
return bus;
}
/**
* Updates accounts transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
private updateExpensesWithBranchOnActivated = async ({
tenantId,
primaryBranch,
trx,
}: IBranchesActivatedPayload) => {
await this.expensesActivateBranches.updateExpensesWithBranch(
tenantId,
primaryBranch.id,
trx
);
};
}

View File

@@ -0,0 +1,38 @@
import { IBranchesActivatedPayload } from '@/interfaces';
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import { BillPaymentsActivateBranches } from '../../Integrations/Purchases/PaymentMadeBranchesActivate';
@Service()
export class PaymentMadeActivateBranchesSubscriber {
@Inject()
private paymentsActivateBranches: BillPaymentsActivateBranches;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.branch.onActivated,
this.updatePaymentsWithBranchOnActivated
);
return bus;
}
/**
* Updates accounts transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
private updatePaymentsWithBranchOnActivated = async ({
tenantId,
primaryBranch,
trx,
}: IBranchesActivatedPayload) => {
await this.paymentsActivateBranches.updateBillPaymentsWithBranch(
tenantId,
primaryBranch.id,
trx
);
};
}

View File

@@ -0,0 +1,38 @@
import { IBranchesActivatedPayload } from '@/interfaces';
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import { PaymentReceiveActivateBranches } from '../../Integrations/Sales/PaymentReceiveBranchesActivate';
@Service()
export class PaymentReceiveActivateBranchesSubscriber {
@Inject()
private paymentsActivateBranches: PaymentReceiveActivateBranches;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.branch.onActivated,
this.updateCreditNoteWithBranchOnActivated
);
return bus;
}
/**
* Updates accounts transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
private updateCreditNoteWithBranchOnActivated = async ({
tenantId,
primaryBranch,
trx,
}: IBranchesActivatedPayload) => {
await this.paymentsActivateBranches.updatePaymentsWithBranch(
tenantId,
primaryBranch.id,
trx
);
};
}

View File

@@ -0,0 +1,38 @@
import { IBranchesActivatedPayload } from '@/interfaces';
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import { SaleEstimateActivateBranches } from '../../Integrations/Sales/SaleEstimatesBranchesActivate';
@Service()
export class SaleEstimatesActivateBranchesSubscriber {
@Inject()
private estimatesActivateBranches: SaleEstimateActivateBranches;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.branch.onActivated,
this.updateEstimatesWithBranchOnActivated
);
return bus;
}
/**
* Updates accounts transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
private updateEstimatesWithBranchOnActivated = async ({
tenantId,
primaryBranch,
trx,
}: IBranchesActivatedPayload) => {
await this.estimatesActivateBranches.updateEstimatesWithBranch(
tenantId,
primaryBranch.id,
trx
);
};
}

View File

@@ -0,0 +1,38 @@
import { IBranchesActivatedPayload } from '@/interfaces';
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import { SaleInvoiceActivateBranches } from '../../Integrations/Sales/SaleInvoiceBranchesActivate';
@Service()
export class SaleInvoicesActivateBranchesSubscriber {
@Inject()
private invoicesActivateBranches: SaleInvoiceActivateBranches;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.branch.onActivated,
this.updateInvoicesWithBranchOnActivated
);
return bus;
}
/**
* Updates accounts transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
private updateInvoicesWithBranchOnActivated = async ({
tenantId,
primaryBranch,
trx,
}: IBranchesActivatedPayload) => {
await this.invoicesActivateBranches.updateInvoicesWithBranch(
tenantId,
primaryBranch.id,
trx
);
};
}

View File

@@ -0,0 +1,38 @@
import { IBranchesActivatedPayload } from '@/interfaces';
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import { SaleReceiptActivateBranches } from '../../Integrations/Sales/SaleReceiptBranchesActivate';
@Service()
export class SaleReceiptsActivateBranchesSubscriber {
@Inject()
private receiptsActivateBranches: SaleReceiptActivateBranches;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.branch.onActivated,
this.updateReceiptsWithBranchOnActivated
);
return bus;
}
/**
* Updates accounts transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
private updateReceiptsWithBranchOnActivated = async ({
tenantId,
primaryBranch,
trx,
}: IBranchesActivatedPayload) => {
await this.receiptsActivateBranches.updateReceiptsWithBranch(
tenantId,
primaryBranch.id,
trx
);
};
}

View File

@@ -0,0 +1,8 @@
export * from './CashflowBranchesActviateSubscriber';
export * from './CreditNoteBranchesActivateSubscriber';
export * from './PaymentMadeBranchesActivateSubscriber';
export * from './PaymentReceiveBranchesActivateSubscriber';
export * from './SaleReceiptsBranchesActivateSubscriber';
export * from './SaleEstiamtesBranchesActivateSubscriber';
export * from './SaleInvoiceBranchesActivateSubscriber';
export * from './ExpenseBranchesActivateSubscriber';

View File

@@ -0,0 +1,53 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import { IBillCreatingPayload, IBillEditingPayload } from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class BillBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.bill.onCreating,
this.validateBranchExistanceOnBillCreating
);
bus.subscribe(
events.bill.onEditing,
this.validateBranchExistanceOnBillEditing
);
return bus;
};
/**
* Validate branch existance on estimate creating.
* @param {ISaleEstimateCreatedPayload} payload
*/
private validateBranchExistanceOnBillCreating = async ({
tenantId,
billDTO,
}: IBillCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
billDTO.branchId
);
};
/**
* Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload
*/
private validateBranchExistanceOnBillEditing = async ({
billDTO,
tenantId,
}: IBillEditingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
billDTO.branchId
);
};
}

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import { ICommandCashflowCreatingPayload } from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class CashflowBranchDTOValidatorSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.cashflow.onTransactionCreating,
this.validateBranchExistanceOnCashflowTransactionCreating
);
return bus;
};
/**
* Validate branch existance once cashflow transaction creating.
* @param {ICommandCashflowCreatingPayload} payload
*/
private validateBranchExistanceOnCashflowTransactionCreating = async ({
tenantId,
newTransactionDTO,
}: ICommandCashflowCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
newTransactionDTO.branchId
);
};
}

View File

@@ -0,0 +1,104 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
ICustomerEventCreatingPayload,
ICustomerOpeningBalanceEditingPayload,
IVendorEventCreatingPayload,
IVendorOpeningBalanceEditingPayload,
} from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class ContactBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.customers.onCreating,
this.validateBranchExistanceOnCustomerCreating
);
bus.subscribe(
events.customers.onOpeningBalanceChanging,
this.validateBranchExistanceOnCustomerOpeningBalanceEditing
);
bus.subscribe(
events.vendors.onCreating,
this.validateBranchExistanceonVendorCreating
);
bus.subscribe(
events.vendors.onOpeningBalanceChanging,
this.validateBranchExistanceOnVendorOpeningBalanceEditing
);
return bus;
};
/**
* Validate branch existance on customer creating.
* @param {ICustomerEventCreatingPayload} payload
*/
private validateBranchExistanceOnCustomerCreating = async ({
tenantId,
customerDTO,
}: ICustomerEventCreatingPayload) => {
// Can't continue if the customer opening balance is zero.
if (!customerDTO.openingBalance) return;
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
customerDTO.openingBalanceBranchId
);
};
/**
* Validate branch existance once customer opening balance editing.
* @param {ICustomerOpeningBalanceEditingPayload} payload
*/
private validateBranchExistanceOnCustomerOpeningBalanceEditing = async ({
openingBalanceEditDTO,
tenantId,
}: ICustomerOpeningBalanceEditingPayload) => {
if (!openingBalanceEditDTO.openingBalance) return;
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
openingBalanceEditDTO.openingBalanceBranchId
);
};
/**
* Validates the branch existance on vendor creating.
* @param {IVendorEventCreatingPayload} payload -
*/
private validateBranchExistanceonVendorCreating = async ({
vendorDTO,
tenantId,
}: IVendorEventCreatingPayload) => {
// Can't continue if the customer opening balance is zero.
if (!vendorDTO.openingBalance) return;
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
vendorDTO.openingBalanceBranchId
);
};
/**
* Validate branch existance once the vendor opening balance editing.
* @param {IVendorOpeningBalanceEditingPayload}
*/
private validateBranchExistanceOnVendorOpeningBalanceEditing = async ({
tenantId,
openingBalanceEditDTO,
}: IVendorOpeningBalanceEditingPayload) => {
if (!openingBalanceEditDTO.openingBalance) return;
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
openingBalanceEditDTO.openingBalanceBranchId
);
};
}

View File

@@ -0,0 +1,56 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
ICreditNoteCreatingPayload,
ICreditNoteEditingPayload,
} from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class CreditNoteBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.creditNote.onCreating,
this.validateBranchExistanceOnCreditCreating
);
bus.subscribe(
events.creditNote.onEditing,
this.validateBranchExistanceOnCreditEditing
);
return bus;
};
/**
* Validate branch existance on estimate creating.
* @param {ICreditNoteCreatingPayload} payload
*/
private validateBranchExistanceOnCreditCreating = async ({
tenantId,
creditNoteDTO,
}: ICreditNoteCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
creditNoteDTO.branchId
);
};
/**
* Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload
*/
private validateBranchExistanceOnCreditEditing = async ({
creditNoteEditDTO,
tenantId,
}: ICreditNoteEditingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
creditNoteEditDTO.branchId
);
};
}

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import { IRefundCreditNoteCreatingPayload } from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class CreditNoteRefundBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.creditNote.onRefundCreating,
this.validateBranchExistanceOnCreditRefundCreating
);
return bus;
};
/**
* Validate branch existance on refund credit note creating.
* @param {ICreditNoteCreatingPayload} payload
*/
private validateBranchExistanceOnCreditRefundCreating = async ({
tenantId,
newCreditNoteDTO,
}: IRefundCreditNoteCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
newCreditNoteDTO.branchId
);
};
}

View File

@@ -0,0 +1,56 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
IExpenseCreatingPayload,
IExpenseEventEditingPayload,
} from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class ExpenseBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.expenses.onCreating,
this.validateBranchExistanceOnExpenseCreating
);
bus.subscribe(
events.expenses.onEditing,
this.validateBranchExistanceOnExpenseEditing
);
return bus;
};
/**
* Validate branch existance once expense transaction creating.
* @param {ISaleEstimateCreatedPayload} payload
*/
private validateBranchExistanceOnExpenseCreating = async ({
tenantId,
expenseDTO,
}: IExpenseCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
expenseDTO.branchId
);
};
/**
* Validate branch existance once expense transaction editing.
* @param {ISaleEstimateEditingPayload} payload
*/
private validateBranchExistanceOnExpenseEditing = async ({
expenseDTO,
tenantId,
}: IExpenseEventEditingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
expenseDTO.branchId
);
};
}

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import { IInventoryAdjustmentCreatingPayload } from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class InventoryAdjustmentBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.inventoryAdjustment.onQuickCreating,
this.validateBranchExistanceOnInventoryCreating
);
return bus;
};
/**
* Validate branch existance on invoice creating.
* @param {ISaleInvoiceCreatingPaylaod} payload
*/
private validateBranchExistanceOnInventoryCreating = async ({
tenantId,
quickAdjustmentDTO,
}: IInventoryAdjustmentCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
quickAdjustmentDTO.branchId
);
};
}

View File

@@ -0,0 +1,56 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
ISaleInvoiceCreatingPaylaod,
ISaleInvoiceEditingPayload,
} from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class InvoiceBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.saleInvoice.onCreating,
this.validateBranchExistanceOnInvoiceCreating
);
bus.subscribe(
events.saleInvoice.onEditing,
this.validateBranchExistanceOnInvoiceEditing
);
return bus;
};
/**
* Validate branch existance on invoice creating.
* @param {ISaleInvoiceCreatingPaylaod} payload
*/
private validateBranchExistanceOnInvoiceCreating = async ({
tenantId,
saleInvoiceDTO,
}: ISaleInvoiceCreatingPaylaod) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
saleInvoiceDTO.branchId
);
};
/**
* Validate branch existance once invoice editing.
* @param {ISaleInvoiceEditingPayload} payload
*/
private validateBranchExistanceOnInvoiceEditing = async ({
saleInvoiceDTO,
tenantId,
}: ISaleInvoiceEditingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
saleInvoiceDTO.branchId
);
};
}

View File

@@ -0,0 +1,76 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
Features,
IManualJournalCreatingPayload,
IManualJournalEditingPayload,
} from '@/interfaces';
import { ManualJournalBranchesValidator } from '../../Integrations/ManualJournals/ManualJournalsBranchesValidator';
import { FeaturesManager } from '@/services/Features/FeaturesManager';
@Service()
export class ManualJournalBranchValidateSubscriber {
@Inject()
private validateManualJournalBranch: ManualJournalBranchesValidator;
@Inject()
private featuresManager: FeaturesManager;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.manualJournals.onCreating,
this.validateBranchExistanceOnBillCreating
);
bus.subscribe(
events.manualJournals.onEditing,
this.validateBranchExistanceOnBillEditing
);
return bus;
};
/**
* Validate branch existance on estimate creating.
* @param {IManualJournalCreatingPayload} payload
*/
private validateBranchExistanceOnBillCreating = async ({
manualJournalDTO,
tenantId,
}: IManualJournalCreatingPayload) => {
// Detarmines whether the multi-branches is accessible by tenant.
const isAccessible = await this.featuresManager.accessible(
tenantId,
Features.BRANCHES
);
// Can't continue if the multi-branches feature is inactive.
if (!isAccessible) return;
// Validates the entries whether have branch id.
await this.validateManualJournalBranch.validateEntriesHasBranchId(
manualJournalDTO
);
};
/**
* Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload
*/
private validateBranchExistanceOnBillEditing = async ({
tenantId,
manualJournalDTO,
}: IManualJournalEditingPayload) => {
// Detarmines whether the multi-branches is accessible by tenant.
const isAccessible = await this.featuresManager.accessible(
tenantId,
Features.BRANCHES
);
// Can't continue if the multi-branches feature is inactive.
if (!isAccessible) return;
await this.validateManualJournalBranch.validateEntriesHasBranchId(
manualJournalDTO
);
};
}

View File

@@ -0,0 +1,56 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
IBillPaymentCreatingPayload,
IBillPaymentEditingPayload,
} from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class PaymentMadeBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.billPayment.onCreating,
this.validateBranchExistanceOnPaymentCreating
);
bus.subscribe(
events.billPayment.onEditing,
this.validateBranchExistanceOnPaymentEditing
);
return bus;
};
/**
* Validate branch existance on estimate creating.
* @param {ISaleEstimateCreatedPayload} payload
*/
private validateBranchExistanceOnPaymentCreating = async ({
tenantId,
billPaymentDTO,
}: IBillPaymentCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
billPaymentDTO.branchId
);
};
/**
* Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload
*/
private validateBranchExistanceOnPaymentEditing = async ({
billPaymentDTO,
tenantId,
}: IBillPaymentEditingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
billPaymentDTO.branchId
);
};
}

View File

@@ -0,0 +1,56 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
IPaymentReceiveCreatingPayload,
IPaymentReceiveEditingPayload,
} from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class PaymentReceiveBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.paymentReceive.onCreating,
this.validateBranchExistanceOnPaymentCreating
);
bus.subscribe(
events.paymentReceive.onEditing,
this.validateBranchExistanceOnPaymentEditing
);
return bus;
};
/**
* Validate branch existance on estimate creating.
* @param {IPaymentReceiveCreatingPayload} payload
*/
private validateBranchExistanceOnPaymentCreating = async ({
tenantId,
paymentReceiveDTO,
}: IPaymentReceiveCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
paymentReceiveDTO.branchId
);
};
/**
* Validate branch existance once estimate editing.
* @param {IPaymentReceiveEditingPayload} payload
*/
private validateBranchExistanceOnPaymentEditing = async ({
paymentReceiveDTO,
tenantId,
}: IPaymentReceiveEditingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
paymentReceiveDTO.branchId
);
};
}

View File

@@ -0,0 +1,56 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
ISaleEstimateCreatingPayload,
ISaleEstimateEditingPayload,
} from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class SaleEstimateBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.saleEstimate.onCreating,
this.validateBranchExistanceOnEstimateCreating
);
bus.subscribe(
events.saleEstimate.onEditing,
this.validateBranchExistanceOnEstimateEditing
);
return bus;
};
/**
* Validate branch existance on estimate creating.
* @param {ISaleEstimateCreatedPayload} payload
*/
private validateBranchExistanceOnEstimateCreating = async ({
tenantId,
estimateDTO,
}: ISaleEstimateCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
estimateDTO.branchId
);
};
/**
* Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload
*/
private validateBranchExistanceOnEstimateEditing = async ({
estimateDTO,
tenantId,
}: ISaleEstimateEditingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
estimateDTO.branchId
);
};
}

View File

@@ -0,0 +1,56 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
ISaleReceiptCreatingPayload,
ISaleReceiptEditingPayload,
} from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class SaleReceiptBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.saleReceipt.onCreating,
this.validateBranchExistanceOnInvoiceCreating
);
bus.subscribe(
events.saleReceipt.onEditing,
this.validateBranchExistanceOnInvoiceEditing
);
return bus;
};
/**
* Validate branch existance on estimate creating.
* @param {ISaleReceiptCreatingPayload} payload
*/
private validateBranchExistanceOnInvoiceCreating = async ({
tenantId,
saleReceiptDTO,
}: ISaleReceiptCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
saleReceiptDTO.branchId
);
};
/**
* Validate branch existance once estimate editing.
* @param {ISaleReceiptEditingPayload} payload
*/
private validateBranchExistanceOnInvoiceEditing = async ({
saleReceiptDTO,
tenantId,
}: ISaleReceiptEditingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
saleReceiptDTO.branchId
);
};
}

View File

@@ -0,0 +1,56 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
IVendorCreditCreatingPayload,
IVendorCreditEditingPayload,
} from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class VendorCreditBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.vendorCredit.onCreating,
this.validateBranchExistanceOnCreditCreating
);
bus.subscribe(
events.vendorCredit.onEditing,
this.validateBranchExistanceOnCreditEditing
);
return bus;
};
/**
* Validate branch existance on estimate creating.
* @param {ISaleEstimateCreatedPayload} payload
*/
private validateBranchExistanceOnCreditCreating = async ({
tenantId,
vendorCreditCreateDTO,
}: IVendorCreditCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
vendorCreditCreateDTO.branchId
);
};
/**
* Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload
*/
private validateBranchExistanceOnCreditEditing = async ({
vendorCreditDTO,
tenantId,
}: IVendorCreditEditingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
vendorCreditDTO.branchId
);
};
}

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import { IRefundVendorCreditCreatingPayload } from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@Service()
export class VendorCreditRefundBranchValidateSubscriber {
@Inject()
private validateBranchExistance: ValidateBranchExistance;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.vendorCredit.onRefundCreating,
this.validateBranchExistanceOnCreditRefundCreating
);
return bus;
};
/**
* Validate branch existance on refund credit note creating.
* @param {IRefundVendorCreditCreatingPayload} payload
*/
private validateBranchExistanceOnCreditRefundCreating = async ({
tenantId,
refundVendorCreditDTO,
}: IRefundVendorCreditCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
refundVendorCreditDTO.branchId
);
};
}

View File

@@ -0,0 +1,15 @@
export * from './BillBranchSubscriber';
export * from './CashflowBranchDTOValidatorSubscriber';
export * from './CreditNoteBranchesSubscriber';
export * from './CreditNoteRefundBranchSubscriber';
export * from './ExpenseBranchSubscriber';
export * from './ManualJournalBranchSubscriber';
export * from './PaymentMadeBranchSubscriber';
export * from './PaymentReceiveBranchSubscriber';
export * from './SaleEstimateMultiBranchesSubscriber';
export * from './SaleReceiptBranchesSubscriber';
export * from './VendorCreditBranchSubscriber';
export * from './VendorCreditRefundBranchSubscriber';
export * from './InvoiceBranchValidatorSubscriber';
export * from './ContactOpeningBalanceBranchSubscriber';
export * from './InventoryAdjustmentBranchValidatorSubscriber';

View File

@@ -0,0 +1,7 @@
export const ERRORS = {
BRANCH_NOT_FOUND: 'BRANCH_NOT_FOUND',
MUTLI_BRANCHES_ALREADY_ACTIVATED: 'MUTLI_BRANCHES_ALREADY_ACTIVATED',
COULD_NOT_DELETE_ONLY_BRANCH: 'COULD_NOT_DELETE_ONLY_BRANCH',
BRANCH_CODE_NOT_UNIQUE: 'BRANCH_CODE_NOT_UNIQUE',
BRANCH_HAS_ASSOCIATED_TRANSACTIONS: 'BRANCH_HAS_ASSOCIATED_TRANSACTIONS'
};

View File

@@ -0,0 +1,49 @@
import NodeCache from 'node-cache';
export default class Cache {
cache: NodeCache;
constructor(config?: object) {
this.cache = new NodeCache({
useClones: false,
...config,
});
}
get(key: string, storeFunction: () => Promise<any>) {
const value = this.cache.get(key);
if (value) {
return Promise.resolve(value);
}
return storeFunction().then((result) => {
this.cache.set(key, result);
return result;
});
}
set(key: string, results: any) {
this.cache.set(key, results);
}
del(keys: string) {
this.cache.del(keys);
}
delStartWith(startStr = '') {
if (!startStr) {
return;
}
const keys = this.cache.keys();
for (const key of keys) {
if (key.indexOf(startStr) === 0) {
this.del(key);
}
}
}
flush() {
this.cache.flushAll();
}
}

View File

@@ -0,0 +1,40 @@
import { IAccount } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class CashflowAccountTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['formattedAmount'];
};
/**
* Exclude these attributes to sale invoice object.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return [
'predefined',
'index',
'accountRootType',
'accountTypeLabel',
'accountParentType',
'isBalanceSheetAccount',
'isPlSheet',
];
};
/**
* 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,30 @@
import { Service, Inject } from 'typedi';
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
@Service()
export default class CashflowDeleteAccount {
@Inject()
tenancy: HasTenancyService;
/**
* Validate the account has no associated cashflow transactions.
* @param {number} tenantId
* @param {number} accountId
*/
public validateAccountHasNoCashflowEntries = async (
tenantId: number,
accountId: number
) => {
const { CashflowTransactionLine } = this.tenancy.models(tenantId);
const associatedLines = await CashflowTransactionLine.query()
.where('creditAccountId', accountId)
.orWhere('cashflowAccountId', accountId);
if (associatedLines.length > 0) {
throw new ServiceError(ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS)
}
};
}

View File

@@ -0,0 +1,31 @@
import { Service, Inject } from 'typedi';
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
@Service()
export class CashflowTransactionAutoIncrement {
@Inject()
private autoIncrementOrdersService: AutoIncrementOrdersService;
/**
* Retrieve the next unique invoice number.
* @param {number} tenantId - Tenant id.
* @return {string}
*/
public getNextTransactionNumber = (tenantId: number): string => {
return this.autoIncrementOrdersService.getNextTransactionNumber(
tenantId,
'cashflow'
);
};
/**
* Increment the invoice next number.
* @param {number} tenantId -
*/
public incrementNextTransactionNumber = (tenantId: number) => {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
tenantId,
'cashflow'
);
};
}

View File

@@ -0,0 +1,171 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import * as R from 'ramda';
import {
ILedgerEntry,
ICashflowTransaction,
AccountNormal,
ICashflowTransactionLine,
} from '../../interfaces';
import {
transformCashflowTransactionType,
getCashflowAccountTransactionsTypes,
} from './utils';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import Ledger from '@/services/Accounting/Ledger';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class CashflowTransactionJournalEntries {
@Inject()
private ledgerStorage: LedgerStorageService;
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the common entry of cashflow transaction.
* @param {ICashflowTransaction} cashflowTransaction
* @returns {}
*/
private getCommonEntry = (cashflowTransaction: ICashflowTransaction) => {
const { entries, ...transaction } = cashflowTransaction;
return {
date: transaction.date,
currencyCode: transaction.currencyCode,
exchangeRate: transaction.exchangeRate,
transactionType: transformCashflowTransactionType(
transaction.transactionType
),
transactionId: transaction.id,
transactionNumber: transaction.transactionNumber,
referenceNo: transaction.referenceNo,
branchId: cashflowTransaction.branchId,
userId: cashflowTransaction.userId,
};
};
/**
* Retrieves the cashflow debit GL entry.
* @param {ICashflowTransaction} cashflowTransaction
* @param {ICashflowTransactionLine} entry
* @param {number} index
* @returns {ILedgerEntry}
*/
private getCashflowDebitGLEntry = (
cashflowTransaction: ICashflowTransaction
): ILedgerEntry => {
const commonEntry = this.getCommonEntry(cashflowTransaction);
return {
...commonEntry,
accountId: cashflowTransaction.cashflowAccountId,
credit: cashflowTransaction.isCashCredit
? cashflowTransaction.localAmount
: 0,
debit: cashflowTransaction.isCashDebit
? cashflowTransaction.localAmount
: 0,
accountNormal: AccountNormal.DEBIT,
index: 1,
};
};
/**
* Retrieves the cashflow credit GL entry.
* @param {ICashflowTransaction} cashflowTransaction
* @param {ICashflowTransactionLine} entry
* @param {number} index
* @returns {ILedgerEntry}
*/
private getCashflowCreditGLEntry = (
cashflowTransaction: ICashflowTransaction
): ILedgerEntry => {
const commonEntry = this.getCommonEntry(cashflowTransaction);
return {
...commonEntry,
credit: cashflowTransaction.isCashDebit
? cashflowTransaction.localAmount
: 0,
debit: cashflowTransaction.isCashCredit
? cashflowTransaction.localAmount
: 0,
accountId: cashflowTransaction.creditAccountId,
accountNormal: cashflowTransaction.creditAccount.accountNormal,
index: 2,
};
};
/**
* Retrieves the cashflow transaction GL entry.
* @param {ICashflowTransaction} cashflowTransaction
* @param {ICashflowTransactionLine} entry
* @param {number} index
* @returns
*/
private getJournalEntries = (
cashflowTransaction: ICashflowTransaction
): ILedgerEntry[] => {
const debitEntry = this.getCashflowDebitGLEntry(cashflowTransaction);
const creditEntry = this.getCashflowCreditGLEntry(cashflowTransaction);
return [debitEntry, creditEntry];
};
/**
* Retrieves the cashflow GL ledger.
* @param {ICashflowTransaction} cashflowTransaction
* @returns {Ledger}
*/
private getCashflowLedger = (cashflowTransaction: ICashflowTransaction) => {
const entries = this.getJournalEntries(cashflowTransaction);
return new Ledger(entries);
};
/**
* Write the journal entries of the given cashflow transaction.
* @param {number} tenantId
* @param {ICashflowTransaction} cashflowTransaction
*/
public writeJournalEntries = async (
tenantId: number,
cashflowTransactionId: number,
trx?: Knex.Transaction
): Promise<void> => {
const { CashflowTransaction } = this.tenancy.models(tenantId);
// Retrieves the cashflow transactions with associated entries.
const transaction = await CashflowTransaction.query(trx)
.findById(cashflowTransactionId)
.withGraphFetched('creditAccount');
// Retrieves the cashflow transaction ledger.
const ledger = this.getCashflowLedger(transaction);
await this.ledgerStorage.commit(tenantId, ledger, trx);
};
/**
* Delete the journal entries.
* @param {number} tenantId - Tenant id.
* @param {number} cashflowTransactionId - Cashflow transaction id.
*/
public revertJournalEntries = async (
tenantId: number,
cashflowTransactionId: number,
trx?: Knex.Transaction
): Promise<void> => {
const transactionTypes = getCashflowAccountTransactionsTypes();
await this.ledgerStorage.deleteByReference(
tenantId,
cashflowTransactionId,
transactionTypes,
trx
);
};
}

View File

@@ -0,0 +1,83 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import CashflowTransactionJournalEntries from './CashflowTransactionJournalEntries';
import {
ICommandCashflowCreatedPayload,
ICommandCashflowDeletedPayload,
} from '@/interfaces';
import { CashflowTransactionAutoIncrement } from './CashflowTransactionAutoIncrement';
@Service()
export default class CashflowTransactionSubscriber {
@Inject()
private cashflowTransactionEntries: CashflowTransactionJournalEntries;
@Inject()
private cashflowTransactionAutoIncrement: CashflowTransactionAutoIncrement;
/**
* Attaches events with handles.
*/
public attach(bus) {
bus.subscribe(
events.cashflow.onTransactionCreated,
this.writeJournalEntriesOnceTransactionCreated
);
bus.subscribe(
events.cashflow.onTransactionCreated,
this.incrementTransactionNumberOnceTransactionCreated
);
bus.subscribe(
events.cashflow.onTransactionDeleted,
this.revertGLEntriesOnceTransactionDeleted
);
return bus;
}
/**
* Writes the journal entries once the cashflow transaction create.
* @param {ICommandCashflowCreatedPayload} payload -
*/
private writeJournalEntriesOnceTransactionCreated = async ({
tenantId,
cashflowTransaction,
trx,
}: ICommandCashflowCreatedPayload) => {
// Can't write GL entries if the transaction not published yet.
if (!cashflowTransaction.isPublished) return;
await this.cashflowTransactionEntries.writeJournalEntries(
tenantId,
cashflowTransaction.id,
trx
);
};
/**
* Increment the cashflow transaction number once the transaction created.
* @param {ICommandCashflowCreatedPayload} payload -
*/
private incrementTransactionNumberOnceTransactionCreated = async ({
tenantId,
}: ICommandCashflowCreatedPayload) => {
this.cashflowTransactionAutoIncrement.incrementNextTransactionNumber(
tenantId
);
};
/**
* Deletes the GL entries once the cashflow transaction deleted.
* @param {ICommandCashflowDeletedPayload} payload -
*/
private revertGLEntriesOnceTransactionDeleted = async ({
tenantId,
cashflowTransactionId,
trx,
}: ICommandCashflowDeletedPayload) => {
await this.cashflowTransactionEntries.revertJournalEntries(
tenantId,
cashflowTransactionId,
trx
);
};
}

View File

@@ -0,0 +1,33 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class CashflowTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['formattedAmount', 'transactionTypeFormatted'];
};
/**
* Formatted amount.
* @param {} transaction
* @returns {string}
*/
protected formattedAmount = (transaction) => {
return formatNumber(transaction.amount, {
currencyCode: transaction.currencyCode,
excerptZero: true,
});
};
/**
* Formatted transaction type.
* @param transaction
* @returns {string}
*/
protected transactionTypeFormatted = (transaction) => {
return this.context.i18n.__(transaction.transactionTypeFormatted);
}
}

View File

@@ -0,0 +1,71 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class CashflowTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {string[]}
*/
protected includeAttributes = (): string[] => {
return ['deposit', 'withdrawal', 'formattedDeposit', 'formattedWithdrawal'];
};
/**
* Exclude these attributes.
* @returns {string[]}
*/
protected excludeAttributes = (): string[] => {
return [
'credit',
'debit',
'index',
'index_group',
'item_id',
'item_quantity',
'contact_type',
'contact_id',
];
};
/**
* Deposit amount attribute.
* @param transaction
* @returns
*/
protected deposit = (transaction) => {
return transaction.debit;
};
/**
* Withdrawal amount attribute.
* @param transaction
* @returns
*/
protected withdrawal = (transaction) => {
return transaction.credit;
};
/**
* Formatted withdrawal amount.
* @param transaction
* @returns
*/
protected formattedWithdrawal = (transaction) => {
return formatNumber(transaction.credit, {
currencyCode: transaction.currencyCode,
excerptZero: true,
});
};
/**
* Formatted deposit account.
* @param transaction
* @returns
*/
protected formattedDeposit = (transaction) => {
return formatNumber(transaction.debit, {
currencyCode: transaction.currencyCode,
excerptZero: true,
});
};
}

View File

@@ -0,0 +1,34 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import { IAccountEventDeletePayload } from '@/interfaces';
import CashflowDeleteAccount from './CashflowDeleteAccount';
@Service()
export default class CashflowWithAccountSubscriber {
@Inject()
cashflowDeleteAccount: CashflowDeleteAccount;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.accounts.onDelete,
this.validateAccountHasNoCashflowTransactionsOnDelete
);
};
/**
* Validate chart account has no associated cashflow transactions on delete.
* @param {IAccountEventDeletePayload} payload -
*/
private validateAccountHasNoCashflowTransactionsOnDelete = async ({
tenantId,
oldAccount,
}: IAccountEventDeletePayload) => {
await this.cashflowDeleteAccount.validateAccountHasNoCashflowEntries(
tenantId,
oldAccount.id
);
};
}

Some files were not shown because too many files have changed in this diff Show More