feat: journal entries with expenses operations.

This commit is contained in:
a.bouhuolia
2021-01-03 12:47:20 +02:00
parent f18ab184e2
commit a2284945f1
9 changed files with 658 additions and 303 deletions

View File

@@ -170,7 +170,10 @@ export default class ExpensesController extends BaseController {
expenseDTO, expenseDTO,
user user
); );
return res.status(200).send({ id: expense.id }); return res.status(200).send({
id: expense.id,
message: 'The expense has been created successfully.',
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -196,7 +199,7 @@ export default class ExpensesController extends BaseController {
); );
return res.status(200).send({ return res.status(200).send({
id: expenseId, id: expenseId,
message: 'The expense has been created successfully.' message: 'The expense has been edited successfully.',
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -283,7 +286,9 @@ export default class ExpensesController extends BaseController {
const { ids: expensesIds } = req.query; const { ids: expensesIds } = req.query;
try { try {
await this.expensesService.publishBulkExpenses( const {
meta: { alreadyPublished, published, total },
} = await this.expensesService.publishBulkExpenses(
tenantId, tenantId,
expensesIds, expensesIds,
user user
@@ -291,6 +296,11 @@ export default class ExpensesController extends BaseController {
return res.status(200).send({ return res.status(200).send({
ids: expensesIds, ids: expensesIds,
message: 'The expenses have been published successfully.', message: 'The expenses have been published successfully.',
meta: {
alreadyPublished,
published,
total,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -372,6 +382,11 @@ export default class ExpensesController extends BaseController {
errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }], errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }],
}); });
} }
if (error.errorType === 'EXPENSES_NOT_FOUND') {
return res.boom.badRequest('Expenses not found.', {
errors: [{ type: 'EXPENSES_NOT_FOUND', code: 110 }],
});
}
if (error.errorType === 'total_amount_equals_zero') { if (error.errorType === 'total_amount_equals_zero') {
return res.boom.badRequest('Expense total should not equal zero.', { return res.boom.badRequest('Expense total should not equal zero.', {
errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }], errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }],

View File

@@ -1,70 +1,111 @@
import { ISystemUser } from "./User"; import { ISystemUser } from './User';
export interface IPaginationMeta { export interface IPaginationMeta {
total: number, total: number;
page: number, page: number;
pageSize: number, pageSize: number;
}; }
export interface IExpensesFilter{ export interface IExpensesFilter {
page: number, page: number;
pageSize: number, pageSize: number;
}; }
export interface IExpense { export interface IExpense {
id: number, id: number;
totalAmount: number, totalAmount: number;
currencyCode: string, currencyCode: string;
description?: string, description?: string;
paymentAccountId: number, paymentAccountId: number;
peyeeId?: number, peyeeId?: number;
referenceNo?: string, referenceNo?: string;
publishedAt: Date|null, publishedAt: Date | null;
userId: number, userId: number;
paymentDate: Date, paymentDate: Date;
payeeId: number, payeeId: number;
categories: IExpenseCategory[], categories: IExpenseCategory[];
} }
export interface IExpenseCategory { export interface IExpenseCategory {
expenseAccountId: number, expenseAccountId: number;
index: number, index: number;
description: string, description: string;
expenseId: number, expenseId: number;
amount: number, amount: number;
} }
export interface IExpenseDTO { export interface IExpenseDTO {
currencyCode: string, currencyCode: string;
description?: string, description?: string;
paymentAccountId: number, paymentAccountId: number;
peyeeId?: number, peyeeId?: number;
referenceNo?: string, referenceNo?: string;
publish: boolean, publish: boolean;
userId: number, userId: number;
paymentDate: Date, paymentDate: Date;
payeeId: number, payeeId: number;
categories: IExpenseCategoryDTO[], categories: IExpenseCategoryDTO[];
} }
export interface IExpenseCategoryDTO { export interface IExpenseCategoryDTO {
expenseAccountId: number, expenseAccountId: number;
index: number, index: number;
description?: string, description?: string;
expenseId: number, expenseId: number;
}; }
export interface IExpensesService { export interface IExpensesService {
newExpense(tenantid: number, expenseDTO: IExpenseDTO, authorizedUser: ISystemUser): Promise<IExpense>; newExpense(
editExpense(tenantid: number, expenseId: number, expenseDTO: IExpenseDTO, authorizedUser: ISystemUser): void; tenantid: number,
expenseDTO: IExpenseDTO,
authorizedUser: ISystemUser
): Promise<IExpense>;
publishExpense(tenantId: number, expenseId: number, authorizedUser: ISystemUser): Promise<void>; editExpense(
tenantid: number,
expenseId: number,
expenseDTO: IExpenseDTO,
authorizedUser: ISystemUser
): void;
deleteExpense(tenantId: number, expenseId: number, authorizedUser: ISystemUser): Promise<void>; publishExpense(
deleteBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise<void>; tenantId: number,
expenseId: number,
authorizedUser: ISystemUser
): Promise<void>;
publishBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise<void>; deleteExpense(
tenantId: number,
expenseId: number,
authorizedUser: ISystemUser
): Promise<void>;
deleteBulkExpenses(
tenantId: number,
expensesIds: number[],
authorizedUser: ISystemUser
): Promise<void>;
publishBulkExpenses(
tenantId: number,
expensesIds: number[],
authorizedUser: ISystemUser
): Promise<{
meta: {
alreadyPublished: number;
published: number;
total: number,
},
}>;
getExpensesList(
tenantId: number,
expensesFilter: IExpensesFilter
): Promise<{
expenses: IExpense[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}>;
getExpensesList(tenantId: number, expensesFilter: IExpensesFilter): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }>;
getExpense(tenantId: number, expenseId: number): Promise<IExpense>; getExpense(tenantId: number, expenseId: number): Promise<IExpense>;
} }

View File

@@ -1,7 +1,7 @@
import TenantRepository from "./TenantRepository"; import TenantRepository from "./TenantRepository";
import { ExpenseCategory } from 'models'; import { ExpenseCategory } from 'models';
export default class ExpenseEntyRepository extends TenantRepository { export default class ExpenseEntryRepository extends TenantRepository {
/** /**
* Gets the repository's model. * Gets the repository's model.
*/ */

View File

@@ -225,12 +225,15 @@ export default class JournalCommands {
* Writes journal entries of expense model object. * Writes journal entries of expense model object.
* @param {IExpense} expense * @param {IExpense} expense
*/ */
expense(expense: IExpense) { expense(
expense: IExpense,
userId: number,
) {
const mixinEntry = { const mixinEntry = {
referenceType: 'Expense', referenceType: 'Expense',
referenceId: expense.id, referenceId: expense.id,
date: expense.paymentDate, date: expense.paymentDate,
userId: expense.userId, userId,
draft: !expense.publishedAt, draft: !expense.publishedAt,
}; };
const paymentJournalEntry = new JournalEntry({ const paymentJournalEntry = new JournalEntry({

View File

@@ -16,6 +16,22 @@ import {
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events'; import events from 'subscribers/events';
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',
}
@Service() @Service()
export default class AccountsService { export default class AccountsService {
@Inject() @Inject()
@@ -50,7 +66,7 @@ export default class AccountsService {
if (!accountType) { if (!accountType) {
this.logger.info('[accounts] account type not found.'); this.logger.info('[accounts] account type not found.');
throw new ServiceError('account_type_not_found'); throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_FOUND);
} }
return accountType; return accountType;
} }
@@ -85,7 +101,7 @@ export default class AccountsService {
tenantId, tenantId,
accountId, accountId,
}); });
throw new ServiceError('parent_account_not_found'); throw new ServiceError(ERRORS.PARENT_ACCOUNT_NOT_FOUND);
} }
return parentAccount; return parentAccount;
} }
@@ -124,7 +140,7 @@ export default class AccountsService {
tenantId, tenantId,
accountCode, accountCode,
}); });
throw new ServiceError('account_code_not_unique'); throw new ServiceError(ERRORS.ACCOUNT_CODE_NOT_UNIQUE);
} }
} }
@@ -138,7 +154,7 @@ export default class AccountsService {
parentAccount: IAccount parentAccount: IAccount
) { ) {
if (accountDTO.accountTypeId !== parentAccount.accountTypeId) { if (accountDTO.accountTypeId !== parentAccount.accountTypeId) {
throw new ServiceError('parent_has_different_type'); throw new ServiceError(ERRORS.PARENT_ACCOUNT_HAS_DIFFERENT_TYPE);
} }
} }
@@ -161,7 +177,7 @@ export default class AccountsService {
this.logger.info('[accounts] the given account not found.', { this.logger.info('[accounts] the given account not found.', {
accountId, accountId,
}); });
throw new ServiceError('account_not_found'); throw new ServiceError(ERRORS.ACCOUNT_NOT_FOUND);
} }
return account; return account;
} }
@@ -178,7 +194,7 @@ export default class AccountsService {
newAccount: IAccount | IAccountDTO newAccount: IAccount | IAccountDTO
) { ) {
if (oldAccount.accountTypeId !== newAccount.accountTypeId) { if (oldAccount.accountTypeId !== newAccount.accountTypeId) {
throw new ServiceError('account_type_not_allowed_to_changed'); throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE);
} }
} }
@@ -208,7 +224,7 @@ export default class AccountsService {
} }
}); });
if (foundAccount) { if (foundAccount) {
throw new ServiceError('account_name_not_unqiue'); throw new ServiceError(ERRORS.ACCOUNT_NAME_NOT_UNIQUE);
} }
} }
@@ -346,7 +362,7 @@ export default class AccountsService {
*/ */
private throwErrorIfAccountPredefined(account: IAccount) { private throwErrorIfAccountPredefined(account: IAccount) {
if (account.predefined) { if (account.predefined) {
throw new ServiceError('account_predefined'); throw new ServiceError(ERRORS.ACCOUNT_PREDEFINED);
} }
} }
@@ -384,7 +400,7 @@ export default class AccountsService {
accountId accountId
); );
if (accountTransactions.length > 0) { if (accountTransactions.length > 0) {
throw new ServiceError('account_has_associated_transactions'); throw new ServiceError(ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS);
} }
} }
@@ -441,7 +457,7 @@ export default class AccountsService {
tenantId, tenantId,
notFoundAccounts, notFoundAccounts,
}); });
throw new ServiceError('accounts_not_found'); throw new ServiceError(ERRORS.ACCOUNTS_NOT_FOUND);
} }
return storedAccounts; return storedAccounts;
} }
@@ -458,7 +474,7 @@ export default class AccountsService {
if (predefined.length > 0) { if (predefined.length > 0) {
this.logger.error('[accounts] some accounts predefined.', { predefined }); this.logger.error('[accounts] some accounts predefined.', { predefined });
throw new ServiceError('predefined_accounts'); throw new ServiceError(ERRORS.PREDEFINED_ACCOUNTS);
} }
return predefined; return predefined;
} }
@@ -487,7 +503,7 @@ export default class AccountsService {
} }
}); });
if (accountsHasTransactions.length > 0) { if (accountsHasTransactions.length > 0) {
throw new ServiceError('accounts_have_transactions'); throw new ServiceError(ERRORS.ACCOUNTS_HAVE_TRANSACTIONS);
} }
} }
@@ -677,7 +693,7 @@ export default class AccountsService {
); );
if (accountType.rootType !== toAccountType.rootType) { if (accountType.rootType !== toAccountType.rootType) {
throw new ServiceError('close_account_and_to_account_not_same_type'); throw new ServiceError(ERRORS.CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE);
} }
const updateAccountBalanceOper = await accountRepository.balanceChange( const updateAccountBalanceOper = await accountRepository.balanceChange(
accountId, accountId,

View File

@@ -1,18 +1,26 @@
import { Service, Inject } from "typedi"; import { Service, Inject } from 'typedi';
import { difference, sumBy, omit } from 'lodash'; import { difference, sumBy, omit, map } from 'lodash';
import moment from "moment"; import moment from 'moment';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
} from 'decorators/eventDispatcher'; } from 'decorators/eventDispatcher';
import { ServiceError } from "exceptions"; import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands'; import JournalCommands from 'services/Accounting/JournalCommands';
import { IExpense, IExpensesFilter, IAccount, IExpenseDTO, IExpensesService, ISystemUser, IPaginationMeta } from 'interfaces'; import {
IExpense,
IExpensesFilter,
IAccount,
IExpenseDTO,
IExpensesService,
ISystemUser,
IPaginationMeta,
} from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events'; import events from 'subscribers/events';
import ContactsService from "services/Contacts/ContactsService"; import ContactsService from 'services/Contacts/ContactsService';
const ERRORS = { const ERRORS = {
EXPENSE_NOT_FOUND: 'expense_not_found', EXPENSE_NOT_FOUND: 'expense_not_found',
@@ -49,14 +57,25 @@ export default class ExpensesService implements IExpensesService {
* @param {number} paymentAccountId * @param {number} paymentAccountId
* @returns {Promise<IAccount>} * @returns {Promise<IAccount>}
*/ */
private async getPaymentAccountOrThrowError(tenantId: number, paymentAccountId: number) { private async getPaymentAccountOrThrowError(
this.logger.info('[expenses] trying to get the given payment account.', { tenantId, paymentAccountId }); tenantId: number,
paymentAccountId: number
) {
this.logger.info('[expenses] trying to get the given payment account.', {
tenantId,
paymentAccountId,
});
const { accountRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
const paymentAccount = await accountRepository.findOneById(paymentAccountId) const paymentAccount = await accountRepository.findOneById(
paymentAccountId
);
if (!paymentAccount) { if (!paymentAccount) {
this.logger.info('[expenses] the given payment account not found.', { tenantId, paymentAccountId }); this.logger.info('[expenses] the given payment account not found.', {
tenantId,
paymentAccountId,
});
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND); throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND);
} }
return paymentAccount; return paymentAccount;
@@ -70,20 +89,32 @@ export default class ExpensesService implements IExpensesService {
* @throws {ServiceError} * @throws {ServiceError}
* @returns {Promise<IAccount[]>} * @returns {Promise<IAccount[]>}
*/ */
private async getExpensesAccountsOrThrowError(tenantId: number, expenseAccountsIds: number[]) { private async getExpensesAccountsOrThrowError(
this.logger.info('[expenses] trying to get expenses accounts.', { tenantId, expenseAccountsIds }); tenantId: number,
expenseAccountsIds: number[]
) {
this.logger.info('[expenses] trying to get expenses accounts.', {
tenantId,
expenseAccountsIds,
});
const { accountRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
const storedExpenseAccounts = await accountRepository.findWhereIn( const storedExpenseAccounts = await accountRepository.findWhereIn(
'id', expenseAccountsIds, 'id',
expenseAccountsIds
);
const storedExpenseAccountsIds = storedExpenseAccounts.map(
(a: IAccount) => a.id
); );
const storedExpenseAccountsIds = storedExpenseAccounts.map((a: IAccount) => a.id);
const notStoredAccountsIds = difference( const notStoredAccountsIds = difference(
expenseAccountsIds, expenseAccountsIds,
storedExpenseAccountsIds storedExpenseAccountsIds
); );
if (notStoredAccountsIds.length > 0) { if (notStoredAccountsIds.length > 0) {
this.logger.info('[expenses] some of expense accounts not found.', { tenantId, expenseAccountsIds }); this.logger.info('[expenses] some of expense accounts not found.', {
tenantId,
expenseAccountsIds,
});
throw new ServiceError(ERRORS.SOME_ACCOUNTS_NOT_FOUND); throw new ServiceError(ERRORS.SOME_ACCOUNTS_NOT_FOUND);
} }
return storedExpenseAccounts; return storedExpenseAccounts;
@@ -95,11 +126,16 @@ export default class ExpensesService implements IExpensesService {
* @throws {ServiceError} * @throws {ServiceError}
*/ */
private validateCategoriesNotEqualZero(expenseDTO: IExpenseDTO) { private validateCategoriesNotEqualZero(expenseDTO: IExpenseDTO) {
this.logger.info('[expenses] validate the expenses categoires not equal zero.', { expenseDTO }); this.logger.info(
'[expenses] validate the expenses categoires not equal zero.',
{ expenseDTO }
);
const totalAmount = sumBy(expenseDTO.categories, 'amount') || 0; const totalAmount = sumBy(expenseDTO.categories, 'amount') || 0;
if (totalAmount <= 0) { if (totalAmount <= 0) {
this.logger.info('[expenses] the given expense categories equal zero.', { expenseDTO }); this.logger.info('[expenses] the given expense categories equal zero.', {
expenseDTO,
});
throw new ServiceError(ERRORS.TOTAL_AMOUNT_EQUALS_ZERO); throw new ServiceError(ERRORS.TOTAL_AMOUNT_EQUALS_ZERO);
} }
} }
@@ -109,15 +145,21 @@ export default class ExpensesService implements IExpensesService {
* @param {number} tenantId * @param {number} tenantId
* @param {number[]} expensesAccountsIds * @param {number[]} expensesAccountsIds
*/ */
private async validateExpensesAccountsType(tenantId: number, expensesAccounts: number[]) { private async validateExpensesAccountsType(
this.logger.info('[expenses] trying to validate expenses accounts type.', { tenantId, expensesAccounts }); tenantId: number,
expensesAccounts: number[]
) {
this.logger.info('[expenses] trying to validate expenses accounts type.', {
tenantId,
expensesAccounts,
});
const { accountTypeRepository } = this.tenancy.repositories(tenantId); const { accountTypeRepository } = this.tenancy.repositories(tenantId);
// Retrieve accounts types of the given root type. // Retrieve accounts types of the given root type.
const expensesTypes = await accountTypeRepository.getByRootType('expense'); const expensesTypes = await accountTypeRepository.getByRootType('expense');
const expensesTypesIds = expensesTypes.map(t => t.id); const expensesTypesIds = expensesTypes.map((t) => t.id);
const invalidExpenseAccounts: number[] = []; const invalidExpenseAccounts: number[] = [];
expensesAccounts.forEach((expenseAccount) => { expensesAccounts.forEach((expenseAccount) => {
@@ -136,19 +178,29 @@ export default class ExpensesService implements IExpensesService {
* @param {number} paymentAccountId * @param {number} paymentAccountId
* @throws {ServiceError} * @throws {ServiceError}
*/ */
private async validatePaymentAccountType(tenantId: number, paymentAccount: number[]) { private async validatePaymentAccountType(
this.logger.info('[expenses] trying to validate payment account type.', { tenantId, paymentAccount }); tenantId: number,
paymentAccount: number[]
) {
this.logger.info('[expenses] trying to validate payment account type.', {
tenantId,
paymentAccount,
});
const { accountTypeRepository } = this.tenancy.repositories(tenantId); const { accountTypeRepository } = this.tenancy.repositories(tenantId);
// Retrieve account tpy eof the given key. // Retrieve account tpy eof the given key.
const validAccountsType = await accountTypeRepository.getByKeys([ const validAccountsType = await accountTypeRepository.getByKeys([
'current_asset', 'fixed_asset', 'current_asset',
'fixed_asset',
]); ]);
const validAccountsTypeIds = validAccountsType.map(t => t.id); const validAccountsTypeIds = validAccountsType.map((t) => t.id);
if (validAccountsTypeIds.indexOf(paymentAccount.accountTypeId) === -1) { if (validAccountsTypeIds.indexOf(paymentAccount.accountTypeId) === -1) {
this.logger.info('[expenses] the given payment account has invalid type', { tenantId, paymentAccount }); this.logger.info(
'[expenses] the given payment account has invalid type',
{ tenantId, paymentAccount }
);
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_HAS_INVALID_TYPE); throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_HAS_INVALID_TYPE);
} }
} }
@@ -160,17 +212,14 @@ export default class ExpensesService implements IExpensesService {
*/ */
public async revertJournalEntries( public async revertJournalEntries(
tenantId: number, tenantId: number,
expenseId: number|number[], expenseId: number | number[]
): Promise<void> { ): Promise<void> {
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal); const journalCommands = new JournalCommands(journal);
await journalCommands.revertJournalEntries(expenseId, 'Expense'); await journalCommands.revertJournalEntries(expenseId, 'Expense');
await Promise.all([ await Promise.all([journal.saveBalance(), journal.deleteEntries()]);
journal.saveBalance(),
journal.deleteEntries(),
]);
} }
/** /**
@@ -181,19 +230,27 @@ export default class ExpensesService implements IExpensesService {
*/ */
public async writeJournalEntries( public async writeJournalEntries(
tenantId: number, tenantId: number,
expense: IExpense, expense: IExpense | IExpense[],
revertOld: boolean, authorizedUserId: number,
) { override: boolean = false
this.logger.info('[expense[ trying to write expense journal entries.', { tenantId, expense }); ): Promise<void> {
this.logger.info('[expense] trying to write expense journal entries.', {
tenantId,
expense,
});
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal); const journalCommands = new JournalCommands(journal);
if (revertOld) { const expenses = Array.isArray(expense) ? expense : [expense];
await journalCommands.revertJournalEntries(expense.id, 'Expense'); const expensesIds = expenses.map((expense) => expense.id);
}
journalCommands.expense(expense);
return Promise.all([ if (override) {
await journalCommands.revertJournalEntries(expensesIds, 'Expense');
}
expenses.forEach((expense: IExpense) => {
journalCommands.expense(expense, authorizedUserId);
});
await Promise.all([
journal.saveBalance(), journal.saveBalance(),
journal.saveEntries(), journal.saveEntries(),
journal.deleteEntries(), journal.deleteEntries(),
@@ -209,13 +266,19 @@ export default class ExpensesService implements IExpensesService {
private async getExpenseOrThrowError(tenantId: number, expenseId: number) { private async getExpenseOrThrowError(tenantId: number, expenseId: number) {
const { expenseRepository } = this.tenancy.repositories(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to get the given expense.', { tenantId, expenseId }); this.logger.info('[expense] trying to get the given expense.', {
tenantId,
expenseId,
});
// Retrieve the given expense by id. // Retrieve the given expense by id.
const expense = await expenseRepository.findOneById(expenseId); const expense = await expenseRepository.findOneById(expenseId);
if (!expense) { if (!expense) {
this.logger.info('[expense] the given expense not found.', { tenantId, expenseId }); this.logger.info('[expense] the given expense not found.', {
tenantId,
expenseId,
});
throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND); throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND);
} }
return expense; return expense;
@@ -229,17 +292,24 @@ export default class ExpensesService implements IExpensesService {
async getExpensesOrThrowError( async getExpensesOrThrowError(
tenantId: number, tenantId: number,
expensesIds: number[] expensesIds: number[]
): Promise<IExpense> { ): Promise<IExpense[]> {
const { expenseRepository } = this.tenancy.repositories(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
const storedExpenses = expenseRepository.findWhereIn('id', expensesIds); const storedExpenses = await expenseRepository.findWhereIn(
'id',
expensesIds,
'categories'
);
const storedExpensesIds = storedExpenses.map((expense) => expense.id); const storedExpensesIds = storedExpenses.map((expense) => expense.id);
const notFoundExpenses = difference(expensesIds, storedExpensesIds); const notFoundExpenses = difference(expensesIds, storedExpensesIds);
// In case there is not found expenses throw service error.
if (notFoundExpenses.length > 0) { if (notFoundExpenses.length > 0) {
this.logger.info('[expense] the give expenses ids not found.', { tenantId, expensesIds }); this.logger.info('[expense] the give expenses ids not found.', {
throw new ServiceError(ERRORS.EXPENSES_NOT_FOUND) tenantId,
expensesIds,
});
throw new ServiceError(ERRORS.EXPENSES_NOT_FOUND);
} }
return storedExpenses; return storedExpenses;
} }
@@ -268,13 +338,17 @@ export default class ExpensesService implements IExpensesService {
...omit(expenseDTO, ['publish']), ...omit(expenseDTO, ['publish']),
totalAmount, totalAmount,
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(), paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),
...(user) ? { ...(user
? {
userId: user.id, userId: user.id,
} : {},
...(expenseDTO.publish) ? {
publishedAt: moment().toMySqlDateTime(),
} : {},
} }
: {}),
...(expenseDTO.publish
? {
publishedAt: moment().toMySqlDateTime(),
}
: {}),
};
} }
/** /**
@@ -286,6 +360,71 @@ export default class ExpensesService implements IExpensesService {
return expenseDTO.categories.map((category) => category.expenseAccountId); return expenseDTO.categories.map((category) => category.expenseAccountId);
} }
/**
* Precedures.
* ---------
* 1. Validate payment account existance on the storage.
* 2. Validate expense accounts exist on the storage.
* 3. Validate payment account type.
* 4. Validate expenses accounts type.
* 5. Validate the expense payee contact id existance on storage.
* 6. Validate the given expense categories not equal zero.
* 7. Stores the expense to the storage.
* ---------
* @param {number} tenantId
* @param {IExpenseDTO} expenseDTO
*/
public async newExpense(
tenantId: number,
expenseDTO: IExpenseDTO,
authorizedUser: ISystemUser
): Promise<IExpense> {
const { expenseRepository } = this.tenancy.repositories(tenantId);
// Validate payment account existance on the storage.
const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId,
expenseDTO.paymentAccountId
);
// Validate expense accounts exist on the storage.
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
tenantId,
this.mapExpensesAccountsIdsFromDTO(expenseDTO)
);
// Validate payment account type.
await this.validatePaymentAccountType(tenantId, paymentAccount);
// Validate expenses accounts type.
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
// Validate the expense payee contact id existance on storage.
if (expenseDTO.payeeId) {
await this.contactsService.getContactByIdOrThrowError(
tenantId,
expenseDTO.payeeId
);
}
// Validate the given expense categories not equal zero.
this.validateCategoriesNotEqualZero(expenseDTO);
// Save the expense to the storage.
const expenseObj = this.expenseDTOToModel(expenseDTO, authorizedUser);
const expense = await expenseRepository.upsertGraph(expenseObj);
this.logger.info(
'[expense] the expense stored to the storage successfully.',
{ tenantId, expenseDTO }
);
// Triggers `onExpenseCreated` event.
this.eventDispatcher.dispatch(events.expenses.onCreated, {
tenantId,
expenseId: expense.id,
authorizedUser,
expense,
});
return expense;
}
/** /**
* Precedures. * Precedures.
* --------- * ---------
@@ -309,17 +448,17 @@ export default class ExpensesService implements IExpensesService {
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<IExpense> { ): Promise<IExpense> {
const { expenseRepository } = this.tenancy.repositories(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
const expense = await this.getExpenseOrThrowError(tenantId, expenseId); const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId);
// - Validate payment account existance on the storage. // - Validate payment account existance on the storage.
const paymentAccount = await this.getPaymentAccountOrThrowError( const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId, tenantId,
expenseDTO.paymentAccountId, expenseDTO.paymentAccountId
); );
// - Validate expense accounts exist on the storage. // - Validate expense accounts exist on the storage.
const expensesAccounts = await this.getExpensesAccountsOrThrowError( const expensesAccounts = await this.getExpensesAccountsOrThrowError(
tenantId, tenantId,
this.mapExpensesAccountsIdsFromDTO(expenseDTO), this.mapExpensesAccountsIdsFromDTO(expenseDTO)
); );
// - Validate payment account type. // - Validate payment account type.
await this.validatePaymentAccountType(tenantId, paymentAccount); await this.validatePaymentAccountType(tenantId, paymentAccount);
@@ -331,8 +470,8 @@ export default class ExpensesService implements IExpensesService {
if (expenseDTO.payeeId) { if (expenseDTO.payeeId) {
await this.contactsService.getContactByIdOrThrowError( await this.contactsService.getContactByIdOrThrowError(
tenantId, tenantId,
expenseDTO.payeeId, expenseDTO.payeeId
) );
} }
// - Validate the given expense categories not equal zero. // - Validate the given expense categories not equal zero.
this.validateCategoriesNotEqualZero(expenseDTO); this.validateCategoriesNotEqualZero(expenseDTO);
@@ -341,72 +480,25 @@ export default class ExpensesService implements IExpensesService {
const expenseObj = this.expenseDTOToModel(expenseDTO); const expenseObj = this.expenseDTOToModel(expenseDTO);
// - Upsert the expense object with expense entries. // - Upsert the expense object with expense entries.
const expenseModel = await expenseRepository.upsertGraph({ const expense = await expenseRepository.upsertGraph({
id: expenseId, id: expenseId,
...expenseObj, ...expenseObj,
}); });
this.logger.info('[expense] the expense updated on the storage successfully.', { tenantId, expenseDTO }); this.logger.info(
return expenseModel; '[expense] the expense updated on the storage successfully.',
} { tenantId, expenseId }
/**
* Precedures.
* ---------
* 1. Validate payment account existance on the storage.
* 2. Validate expense accounts exist on the storage.
* 3. Validate payment account type.
* 4. Validate expenses accounts type.
* 5. Validate the expense payee contact id existance on storage.
* 6. Validate the given expense categories not equal zero.
* 7. Stores the expense to the storage.
* ---------
* @param {number} tenantId
* @param {IExpenseDTO} expenseDTO
*/
public async newExpense(
tenantId: number,
expenseDTO: IExpenseDTO,
authorizedUser: ISystemUser,
): Promise<IExpense> {
const { expenseRepository } = this.tenancy.repositories(tenantId);
// - Validate payment account existance on the storage.
const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId,
expenseDTO.paymentAccountId,
); );
// - Validate expense accounts exist on the storage.
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
tenantId,
this.mapExpensesAccountsIdsFromDTO(expenseDTO),
);
// - Validate payment account type.
await this.validatePaymentAccountType(tenantId, paymentAccount);
// - Validate expenses accounts type.
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
// - Validate the expense payee contact id existance on storage.
if (expenseDTO.payeeId) {
await this.contactsService.getContactByIdOrThrowError(
tenantId,
expenseDTO.payeeId,
)
}
// - Validate the given expense categories not equal zero.
this.validateCategoriesNotEqualZero(expenseDTO);
// - Save the expense to the storage.
const expenseObj = this.expenseDTOToModel(expenseDTO, authorizedUser);
const expenseModel = await expenseRepository.upsertGraph(expenseObj);
this.logger.info('[expense] the expense stored to the storage successfully.', { tenantId, expenseDTO });
// Triggers `onExpenseCreated` event. // Triggers `onExpenseCreated` event.
this.eventDispatcher.dispatch(events.expenses.onCreated, { tenantId, expenseId: expenseModel.id }); this.eventDispatcher.dispatch(events.expenses.onEdited, {
tenantId,
return expenseModel; expenseId,
expense,
expenseDTO,
authorizedUser,
oldExpense,
});
return expense;
} }
/** /**
@@ -416,22 +508,44 @@ export default class ExpensesService implements IExpensesService {
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
* @return {Promise<void>} * @return {Promise<void>}
*/ */
public async publishExpense(tenantId: number, expenseId: number, authorizedUser: ISystemUser) { public async publishExpense(
tenantId: number,
expenseId: number,
authorizedUser: ISystemUser
) {
const { expenseRepository } = this.tenancy.repositories(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
const expense = await this.getExpenseOrThrowError(tenantId, expenseId); const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId);
if (expense instanceof ServiceError) { if (oldExpense instanceof ServiceError) {
throw expense; throw oldExpense;
} }
this.validateExpenseIsNotPublished(expense); this.validateExpenseIsNotPublished(oldExpense);
this.logger.info('[expense] trying to publish the expense.', { tenantId, expenseId }); this.logger.info('[expense] trying to publish the expense.', {
tenantId,
expenseId,
});
// Publish the given expense on the storage.
await expenseRepository.publish(expenseId); await expenseRepository.publish(expenseId);
this.logger.info('[expense] the expense published successfully.', { tenantId, expenseId }); // Retrieve the new expense after modification.
const expense = await expenseRepository.findOneById(
expenseId,
'categories'
);
this.logger.info('[expense] the expense published successfully.', {
tenantId,
expenseId,
});
// Triggers `onExpensePublished` event. // Triggers `onExpensePublished` event.
this.eventDispatcher.dispatch(events.expenses.onPublished, { tenantId, expenseId }); this.eventDispatcher.dispatch(events.expenses.onPublished, {
tenantId,
expenseId,
oldExpense,
expense,
authorizedUser,
});
} }
/** /**
@@ -440,17 +554,36 @@ export default class ExpensesService implements IExpensesService {
* @param {number} expenseId * @param {number} expenseId
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
*/ */
public async deleteExpense(tenantId: number, expenseId: number, authorizedUser: ISystemUser) { public async deleteExpense(
const expense = await this.getExpenseOrThrowError(tenantId, expenseId); tenantId: number,
const { expenseRepository } = this.tenancy.repositories(tenantId); expenseId: number,
authorizedUser: ISystemUser
): Promise<void> {
const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId);
const {
expenseRepository,
expenseEntryRepository,
} = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to delete the expense.', { tenantId, expenseId }); this.logger.info('[expense] trying to delete the expense.', {
tenantId,
expenseId,
});
await expenseEntryRepository.deleteBy({ expenseId });
await expenseRepository.deleteById(expenseId); await expenseRepository.deleteById(expenseId);
this.logger.info('[expense] the expense deleted successfully.', { tenantId, expenseId }); this.logger.info('[expense] the expense deleted successfully.', {
tenantId,
expenseId,
});
// Triggers `onExpenseDeleted` event. // Triggers `onExpenseDeleted` event.
this.eventDispatcher.dispatch(events.expenses.onDeleted, { tenantId, expenseId }); this.eventDispatcher.dispatch(events.expenses.onDeleted, {
tenantId,
expenseId,
authorizedUser,
oldExpense,
});
} }
/** /**
@@ -459,17 +592,57 @@ export default class ExpensesService implements IExpensesService {
* @param {number[]} expensesIds * @param {number[]} expensesIds
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
*/ */
public async deleteBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser) { public async deleteBulkExpenses(
const expenses = await this.getExpensesOrThrowError(tenantId, expensesIds); tenantId: number,
const { expenseRepository } = this.tenancy.repositories(tenantId); expensesIds: number[],
authorizedUser: ISystemUser
) {
const {
expenseRepository,
expenseEntryRepository,
} = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to delete the given expenses.', { tenantId, expensesIds }); // Retrieve olds expenses.
const oldExpenses = await this.getExpensesOrThrowError(
tenantId,
expensesIds
);
this.logger.info('[expense] trying to delete the given expenses.', {
tenantId,
expensesIds,
});
await expenseEntryRepository.deleteWhereIn('expenseId', expensesIds);
await expenseRepository.deleteWhereIdIn(expensesIds); await expenseRepository.deleteWhereIdIn(expensesIds);
this.logger.info('[expense] the given expenses deleted successfully.', { tenantId, expensesIds }); this.logger.info('[expense] the given expenses deleted successfully.', {
tenantId,
expensesIds,
});
// Triggers `onExpenseBulkDeleted` event. // Triggers `onExpenseBulkDeleted` event.
this.eventDispatcher.dispatch(events.expenses.onBulkDeleted, { tenantId, expensesIds }); this.eventDispatcher.dispatch(events.expenses.onBulkDeleted, {
tenantId,
expensesIds,
oldExpenses,
authorizedUser,
});
}
/**
* Filters the not published expenses.
* @param {IExpense[]} expenses -
*/
public getNonePublishedExpenses(expenses: IExpense[]): IExpense[] {
return expenses.filter((expense) => !expense.publishedAt);
}
/**
* Filtesr the published expenses.
* @param {IExpense[]} expenses -
* @return {IExpense[]}
*/
public getPublishedExpenses(expenses: IExpense[]): IExpense[] {
return expenses.filter((expense) => expense.publishedAt);
} }
/** /**
@@ -478,17 +651,66 @@ export default class ExpensesService implements IExpensesService {
* @param {number[]} expensesIds * @param {number[]} expensesIds
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
*/ */
public async publishBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser) { public async publishBulkExpenses(
const expenses = await this.getExpensesOrThrowError(tenantId, expensesIds); tenantId: number,
expensesIds: number[],
authorizedUser: ISystemUser
): Promise<{
meta: {
alreadyPublished: number;
published: number;
total: number,
},
}> {
const oldExpenses = await this.getExpensesOrThrowError(
tenantId,
expensesIds
);
const { expenseRepository } = this.tenancy.repositories(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to publish the given expenses.', { tenantId, expensesIds }); // Filters the not published expenses.
await expenseRepository.whereIdInPublish(expensesIds); const notPublishedExpenses = this.getNonePublishedExpenses(oldExpenses);
this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds }); // Filters the published expenses.
const publishedExpenses = this.getPublishedExpenses(oldExpenses);
// Mappes the published expenses to get id.
const notPublishedExpensesIds = map(notPublishedExpenses, 'id');
if (notPublishedExpensesIds.length > 0) {
this.logger.info('[expense] trying to publish the given expenses.', {
tenantId,
expensesIds,
});
await expenseRepository.whereIdInPublish(notPublishedExpensesIds);
this.logger.info(
'[expense] the given expenses ids published successfully.',
{ tenantId, expensesIds }
);
}
// Retrieve the new expenses after modification.
const expenses = await expenseRepository.findWhereIn(
'id',
expensesIds,
'categories'
);
// Triggers `onExpenseBulkDeleted` event. // Triggers `onExpenseBulkDeleted` event.
this.eventDispatcher.dispatch(events.expenses.onBulkPublished, { tenantId, expensesIds }); this.eventDispatcher.dispatch(events.expenses.onBulkPublished, {
tenantId,
expensesIds,
oldExpenses,
expenses,
authorizedUser,
});
return {
meta: {
alreadyPublished: publishedExpenses.length,
published: notPublishedExpenses.length,
total: oldExpenses.length,
},
};
} }
/** /**
@@ -500,21 +722,34 @@ export default class ExpensesService implements IExpensesService {
public async getExpensesList( public async getExpensesList(
tenantId: number, tenantId: number,
expensesFilter: IExpensesFilter expensesFilter: IExpensesFilter
): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { ): Promise<{
expenses: IExpense[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { Expense } = this.tenancy.models(tenantId); const { Expense } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Expense, expensesFilter); const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
Expense,
expensesFilter
);
this.logger.info('[expense] trying to get expenses datatable list.', { tenantId, expensesFilter }); this.logger.info('[expense] trying to get expenses datatable list.', {
const { results, pagination } = await Expense.query().onBuild((builder) => { tenantId,
expensesFilter,
});
const { results, pagination } = await Expense.query()
.onBuild((builder) => {
builder.withGraphFetched('paymentAccount'); builder.withGraphFetched('paymentAccount');
builder.withGraphFetched('categories.expenseAccount'); builder.withGraphFetched('categories.expenseAccount');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}).pagination(expensesFilter.page - 1, expensesFilter.pageSize); })
.pagination(expensesFilter.page - 1, expensesFilter.pageSize);
return { return {
expenses: results, expenses: results,
pagination, filterMeta: pagination,
dynamicFilter.getResponseMeta(), filterMeta: dynamicFilter.getResponseMeta(),
}; };
} }
@@ -524,7 +759,10 @@ export default class ExpensesService implements IExpensesService {
* @param {number} expenseId * @param {number} expenseId
* @return {Promise<IExpense>} * @return {Promise<IExpense>}
*/ */
public async getExpense(tenantId: number, expenseId: number): Promise<IExpense> { public async getExpense(
tenantId: number,
expenseId: number
): Promise<IExpense> {
const { expenseRepository } = this.tenancy.repositories(tenantId); const { expenseRepository } = this.tenancy.repositories(tenantId);
const expense = await expenseRepository.findOneById(expenseId, [ const expense = await expenseRepository.findOneById(expenseId, [

View File

@@ -400,7 +400,11 @@ export default class ItemsService implements IItemsService {
const { Item } = this.tenancy.models(tenantId); const { Item } = this.tenancy.models(tenantId);
this.logger.info('[items] trying to delete item.', { tenantId, itemId }); this.logger.info('[items] trying to delete item.', { tenantId, itemId });
// Retreive the given item or throw not found service error.
await this.getItemOrThrowError(tenantId, itemId); await this.getItemOrThrowError(tenantId, itemId);
// Validate the item has no associated invoices or bills.
await this.validateHasNoInvoicesOrBills(tenantId, itemId); await this.validateHasNoInvoicesOrBills(tenantId, itemId);
await Item.query().findById(itemId).delete(); await Item.query().findById(itemId).delete();

View File

@@ -68,7 +68,6 @@ export default {
onEdited: 'onExpenseEdited', onEdited: 'onExpenseEdited',
onDeleted: 'onExpenseDelted', onDeleted: 'onExpenseDelted',
onPublished: 'onExpensePublished', onPublished: 'onExpensePublished',
onBulkDeleted: 'onExpenseBulkDeleted', onBulkDeleted: 'onExpenseBulkDeleted',
onBulkPublished: 'onBulkPublished', onBulkPublished: 'onBulkPublished',
}, },

View File

@@ -16,36 +16,49 @@ export default class ExpensesSubscriber {
} }
/** /**
* On expense created. * Handles the writing journal entries once the expense created.
*/ */
@On(events.expenses.onCreated) @On(events.expenses.onCreated)
public async onExpenseCreated({ expenseId, tenantId }) { public async onExpenseCreated({
const { expenseRepository } = this.tenancy.repositories(tenantId); expenseId,
const expense = await expenseRepository.getById(expenseId); expense,
tenantId,
authorizedUser,
}) {
// In case expense published, write journal entries. // In case expense published, write journal entries.
if (expense.publishedAt) { if (expense.publishedAt) {
await this.expensesService.writeJournalEntries(tenantId, expense, false); await this.expensesService.writeJournalEntries(
tenantId,
expense,
authorizedUser.id,
false
);
} }
} }
/** /**
* On expense edited. * Handle writing expense journal entries once the expense edited.
*/ */
@On(events.expenses.onEdited) @On(events.expenses.onEdited)
public async onExpenseEdited({ expenseId, tenantId }) { public async onExpenseEdited({
const { expenseRepository } = this.tenancy.repositories(tenantId); expenseId,
const expense = await expenseRepository.getById(expenseId); tenantId,
expense,
authorizedUser,
}) {
// In case expense published, write journal entries. // In case expense published, write journal entries.
if (expense.publishedAt) { if (expense.publishedAt) {
await this.expensesService.writeJournalEntries(tenantId, expense, true); await this.expensesService.writeJournalEntries(
tenantId,
expense,
authorizedUser.id,
true
);
} }
} }
/** /**
* * Reverts expense journal entries once the expense deleted.
* @param param0
*/ */
@On(events.expenses.onDeleted) @On(events.expenses.onDeleted)
public async onExpenseDeleted({ expenseId, tenantId }) { public async onExpenseDeleted({ expenseId, tenantId }) {
@@ -53,35 +66,61 @@ export default class ExpensesSubscriber {
} }
/** /**
* * Handles writing expense journal once the expense publish.
* @param param0
*/ */
@On(events.expenses.onPublished) @On(events.expenses.onPublished)
public async onExpensePublished({ expenseId, tenantId }) { public async onExpensePublished({
const { expenseRepository } = this.tenancy.repositories(tenantId); expenseId,
const expense = await expenseRepository.getById(expenseId); tenantId,
expense,
authorizedUser,
}) {
// In case expense published, write journal entries. // In case expense published, write journal entries.
if (expense.publishedAt) { if (expense.publishedAt) {
await this.expensesService.writeJournalEntries(tenantId, expense, false); await this.expensesService.writeJournalEntries(
tenantId,
expense,
authorizedUser.id,
false
);
} }
} }
/** /**
* * Handles the revert journal entries once the expenses deleted in bulk.
* @param param0
*/ */
@On(events.expenses.onBulkDeleted) @On(events.expenses.onBulkDeleted)
public onExpenseBulkDeleted({ expensesIds, tenantId }) { public async handleRevertJournalEntriesOnceDeleted({
expensesIds,
tenantId,
}) {
await this.expensesService.revertJournalEntries(tenantId, expensesIds);
} }
/** /**
* * Handles writing journal entriers of the not-published expenses.
* @param param0
*/ */
@On(events.expenses.onBulkPublished) @On(events.expenses.onBulkPublished)
public onExpenseBulkPublished({ expensesIds, tenantId }) { public async onExpenseBulkPublished({
expensesIds,
tenantId,
expenses,
oldExpenses,
authorizedUser,
}) {
// Filters the not published expenses.
const notPublishedExpenses = this.expensesService.getNonePublishedExpenses(
oldExpenses
);
// Can't continue if there is no not-published expoenses.
if (notPublishedExpenses.length === 0) { return; }
// Writing the journal entries of not-published expenses.
await this.expensesService.writeJournalEntries(
tenantId,
notPublishedExpenses,
authorizedUser.id,
false
);
} }
} }