mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-05-12 05:44:56 +00:00
add server to monorepo.
This commit is contained in:
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
71
packages/server/src/services/Accounting/JournalCommands.ts
Normal file
71
packages/server/src/services/Accounting/JournalCommands.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
74
packages/server/src/services/Accounting/JournalContacts.ts
Normal file
74
packages/server/src/services/Accounting/JournalContacts.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
10
packages/server/src/services/Accounting/JournalEntry.ts
Normal file
10
packages/server/src/services/Accounting/JournalEntry.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
export default class JournalEntry {
|
||||
constructor(entry) {
|
||||
const defaults = {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
};
|
||||
this.entry = { ...defaults, ...entry };
|
||||
}
|
||||
}
|
||||
17
packages/server/src/services/Accounting/JournalFinancial.ts
Normal file
17
packages/server/src/services/Accounting/JournalFinancial.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
759
packages/server/src/services/Accounting/JournalPoster.ts
Normal file
759
packages/server/src/services/Accounting/JournalPoster.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
249
packages/server/src/services/Accounting/Ledger.ts
Normal file
249
packages/server/src/services/Accounting/Ledger.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
103
packages/server/src/services/Accounting/LedgerContactStorage.ts
Normal file
103
packages/server/src/services/Accounting/LedgerContactStorage.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
155
packages/server/src/services/Accounting/LedgetAccountStorage.ts
Normal file
155
packages/server/src/services/Accounting/LedgetAccountStorage.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
34
packages/server/src/services/Accounting/utils.ts
Normal file
34
packages/server/src/services/Accounting/utils.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
24
packages/server/src/services/Accounts/AccountTransform.ts
Normal file
24
packages/server/src/services/Accounts/AccountTransform.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
}
|
||||
139
packages/server/src/services/Accounts/AccountsApplication.ts
Normal file
139
packages/server/src/services/Accounts/AccountsApplication.ts
Normal 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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
64
packages/server/src/services/Accounts/ActivateAccount.ts
Normal file
64
packages/server/src/services/Accounts/ActivateAccount.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
140
packages/server/src/services/Accounts/CreateAccount.ts
Normal file
140
packages/server/src/services/Accounts/CreateAccount.ts
Normal 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;
|
||||
});
|
||||
};
|
||||
}
|
||||
107
packages/server/src/services/Accounts/DeleteAccount.ts
Normal file
107
packages/server/src/services/Accounts/DeleteAccount.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
}
|
||||
116
packages/server/src/services/Accounts/EditAccount.ts
Normal file
116
packages/server/src/services/Accounts/EditAccount.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
41
packages/server/src/services/Accounts/GetAccount.ts
Normal file
41
packages/server/src/services/Accounts/GetAccount.ts
Normal 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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
};
|
||||
}
|
||||
66
packages/server/src/services/Accounts/GetAccounts.ts
Normal file
66
packages/server/src/services/Accounts/GetAccounts.ts
Normal 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(),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
77
packages/server/src/services/Accounts/constants.ts
Normal file
77
packages/server/src/services/Accounts/constants.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
export class AccountsReceivableRepository {
|
||||
|
||||
|
||||
findOrCreateAccount = (currencyCode?: string) => {
|
||||
|
||||
};
|
||||
}
|
||||
15
packages/server/src/services/AuthenticatedAccount/index.ts
Normal file
15
packages/server/src/services/AuthenticatedAccount/index.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
49
packages/server/src/services/Authentication/RateLimiter.ts
Normal file
49
packages/server/src/services/Authentication/RateLimiter.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
322
packages/server/src/services/Authentication/index.ts
Normal file
322
packages/server/src/services/Authentication/index.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
90
packages/server/src/services/Branches/ActivateBranches.ts
Normal file
90
packages/server/src/services/Branches/ActivateBranches.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
52
packages/server/src/services/Branches/BranchValidate.ts
Normal file
52
packages/server/src/services/Branches/BranchValidate.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
112
packages/server/src/services/Branches/BranchesApplication.ts
Normal file
112
packages/server/src/services/Branches/BranchesApplication.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
29
packages/server/src/services/Branches/BranchesSettings.ts
Normal file
29
packages/server/src/services/Branches/BranchesSettings.ts
Normal 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 });
|
||||
};
|
||||
}
|
||||
30
packages/server/src/services/Branches/CRUDBranch.ts
Normal file
30
packages/server/src/services/Branches/CRUDBranch.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
64
packages/server/src/services/Branches/CreateBranch.ts
Normal file
64
packages/server/src/services/Branches/CreateBranch.ts
Normal 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;
|
||||
});
|
||||
};
|
||||
}
|
||||
76
packages/server/src/services/Branches/DeleteBranch.ts
Normal file
76
packages/server/src/services/Branches/DeleteBranch.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
}
|
||||
65
packages/server/src/services/Branches/EditBranch.ts
Normal file
65
packages/server/src/services/Branches/EditBranch.ts
Normal 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;
|
||||
});
|
||||
};
|
||||
}
|
||||
50
packages/server/src/services/Branches/EventsProvider.ts
Normal file
50
packages/server/src/services/Branches/EventsProvider.ts
Normal 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
|
||||
];
|
||||
26
packages/server/src/services/Branches/GetBranch.ts
Normal file
26
packages/server/src/services/Branches/GetBranch.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
22
packages/server/src/services/Branches/GetBranches.ts
Normal file
22
packages/server/src/services/Branches/GetBranches.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const ERRORS = {
|
||||
MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID:
|
||||
'MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID',
|
||||
};
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
export const ERRORS = {
|
||||
BRANCH_ID_REQUIRED: 'BRANCH_ID_REQUIRED',
|
||||
BRANCH_ID_NOT_FOUND: 'BRANCH_ID_NOT_FOUND'
|
||||
}
|
||||
67
packages/server/src/services/Branches/MarkBranchAsPrimary.ts
Normal file
67
packages/server/src/services/Branches/MarkBranchAsPrimary.ts
Normal 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;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
7
packages/server/src/services/Branches/constants.ts
Normal file
7
packages/server/src/services/Branches/constants.ts
Normal 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'
|
||||
};
|
||||
49
packages/server/src/services/Cache/index.ts
Normal file
49
packages/server/src/services/Cache/index.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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'
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user