mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 07:10:33 +00:00
feat: journal entries with expenses operations.
This commit is contained in:
@@ -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 }],
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
builder.withGraphFetched('paymentAccount');
|
expensesFilter,
|
||||||
builder.withGraphFetched('categories.expenseAccount');
|
});
|
||||||
dynamicFilter.buildQuery()(builder);
|
const { results, pagination } = await Expense.query()
|
||||||
}).pagination(expensesFilter.page - 1, expensesFilter.pageSize);
|
.onBuild((builder) => {
|
||||||
|
builder.withGraphFetched('paymentAccount');
|
||||||
|
builder.withGraphFetched('categories.expenseAccount');
|
||||||
|
dynamicFilter.buildQuery()(builder);
|
||||||
|
})
|
||||||
|
.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, [
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user