mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
fix: handle make journal errors with contacts.
This commit is contained in:
@@ -12,6 +12,7 @@ const ERROR = {
|
||||
RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS',
|
||||
CREDIT_DEBIT_SUMATION_SHOULD_NOT_EQUAL_ZERO:
|
||||
'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
|
||||
ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
|
||||
};
|
||||
|
||||
// Transform API errors in toasts messages.
|
||||
@@ -27,14 +28,6 @@ export const transformErrors = (resErrors, { setErrors, errors }) => {
|
||||
newErrors = setWith(newErrors, `entries.[${index}].${prop}`, message);
|
||||
});
|
||||
|
||||
if ((error = getError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS))) {
|
||||
toastMessages.push(
|
||||
formatMessage({
|
||||
id: 'vendors_should_selected_with_payable_account_only',
|
||||
}),
|
||||
);
|
||||
setEntriesErrors(error.indexes, 'contact_id', 'error');
|
||||
}
|
||||
if ((error = getError(ERROR.RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS))) {
|
||||
toastMessages.push(
|
||||
formatMessage({
|
||||
@@ -43,21 +36,20 @@ export const transformErrors = (resErrors, { setErrors, errors }) => {
|
||||
);
|
||||
setEntriesErrors(error.indexes, 'contact_id', 'error');
|
||||
}
|
||||
if ((error = getError(ERROR.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC))) {
|
||||
toastMessages.push(
|
||||
formatMessage({
|
||||
id: 'customers_should_selected_with_receivable_account_only',
|
||||
}),
|
||||
);
|
||||
setEntriesErrors(error.indexes, 'account_id', 'error');
|
||||
}
|
||||
if ((error = getError(ERROR.VENDORS_NOT_WITH_PAYABLE_ACCOUNT))) {
|
||||
toastMessages.push(
|
||||
formatMessage({
|
||||
id: 'vendors_should_selected_with_payable_account_only',
|
||||
}),
|
||||
);
|
||||
setEntriesErrors(error.indexes, 'account_id', 'error');
|
||||
if ((error = getError(ERROR.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT))) {
|
||||
if (error.meta.contact_type === 'customer') {
|
||||
toastMessages.push(
|
||||
formatMessage({
|
||||
id: 'receivable_accounts_should_assign_with_customers',
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (error.meta.contact_type === 'vendor') {
|
||||
toastMessages.push(
|
||||
formatMessage({ id: 'payable_accounts_should_assign_with_vendors' }),
|
||||
);
|
||||
}
|
||||
setEntriesErrors(error.meta.indexes, 'contact_id', 'error');
|
||||
}
|
||||
if ((error = getError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS))) {
|
||||
newErrors = setWith(
|
||||
@@ -78,4 +70,4 @@ export const transformErrors = (resErrors, { setErrors, errors }) => {
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -30,12 +30,12 @@ export default class ManualJournalsController extends BaseController {
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getManualJournalsList.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.catchServiceErrors,
|
||||
this.catchServiceErrors.bind(this),
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
asyncMiddleware(this.getManualJournal.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
this.catchServiceErrors.bind(this),
|
||||
);
|
||||
router.post(
|
||||
'/publish', [
|
||||
@@ -43,7 +43,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.publishManualJournals.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
this.catchServiceErrors.bind(this),
|
||||
);
|
||||
router.post(
|
||||
'/:id/publish', [
|
||||
@@ -51,7 +51,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.publishManualJournal.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
this.catchServiceErrors.bind(this),
|
||||
);
|
||||
router.post(
|
||||
'/:id', [
|
||||
@@ -60,7 +60,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editManualJournal.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
this.catchServiceErrors.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
'/:id', [
|
||||
@@ -68,7 +68,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteManualJournal.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
this.catchServiceErrors.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
'/', [
|
||||
@@ -76,7 +76,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteBulkManualJournals.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
this.catchServiceErrors.bind(this),
|
||||
);
|
||||
router.post(
|
||||
'/', [
|
||||
@@ -84,7 +84,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.makeJournalEntries.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
this.catchServiceErrors.bind(this),
|
||||
);
|
||||
return router;
|
||||
}
|
||||
@@ -133,7 +133,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('description')
|
||||
.optional()
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
@@ -153,7 +153,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
.toFloat(),
|
||||
check('entries.*.account_id').isInt({ max: DATATYPES_LENGTH.INT_10 }).toInt(),
|
||||
check('entries.*.note')
|
||||
.optional()
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('entries.*.contact_id')
|
||||
@@ -368,43 +368,45 @@ export default class ManualJournalsController extends BaseController {
|
||||
if (error.errorType === 'credit_debit_not_equal_zero') {
|
||||
return res.boom.badRequest(
|
||||
'Credit and debit should not be equal zero.',
|
||||
{ errors: [{ type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO', code: 400, }] }
|
||||
{ errors: [{ type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO', code: 200, }] }
|
||||
)
|
||||
}
|
||||
if (error.errorType === 'credit_debit_not_equal') {
|
||||
return res.boom.badRequest(
|
||||
'Credit and debit should be equal.',
|
||||
{ errors: [{ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 }] }
|
||||
{ errors: [{ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 300 }] }
|
||||
)
|
||||
}
|
||||
if (error.errorType === 'acccounts_ids_not_found') {
|
||||
return res.boom.badRequest(
|
||||
'Journal entries some of accounts ids not exists.',
|
||||
{ errors: [{ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 }] }
|
||||
{ errors: [{ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 400 }] }
|
||||
)
|
||||
}
|
||||
if (error.errorType === 'journal_number_exists') {
|
||||
return res.boom.badRequest(
|
||||
'Journal number should be unique.',
|
||||
{ errors: [{ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 }] },
|
||||
{ errors: [{ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 500 }] },
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'payabel_entries_have_no_vendors') {
|
||||
if (error.errorType === 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT') {
|
||||
return res.boom.badRequest(
|
||||
'',
|
||||
{ errors: [{ type: '' }] },
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'receivable_entries_have_no_customers') {
|
||||
return res.boom.badRequest(
|
||||
'',
|
||||
{ errors: [{ type: '' }] },
|
||||
{
|
||||
errors: [
|
||||
{
|
||||
type: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
|
||||
code: 600,
|
||||
meta: this.transfromToResponse(error.payload),
|
||||
}
|
||||
]
|
||||
},
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'contacts_not_found') {
|
||||
return res.boom.badRequest(
|
||||
'',
|
||||
{ errors: [{ type: '' }] },
|
||||
{ errors: [{ type: 'CONTACTS_NOT_FOUND', code: 700 }] },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
export default class ServiceError {
|
||||
errorType: string;
|
||||
message: string;
|
||||
payload: any;
|
||||
|
||||
constructor(errorType: string, message?: string) {
|
||||
constructor(errorType: string, message?: string, payload?: any) {
|
||||
this.errorType = errorType;
|
||||
this.message = message || null;
|
||||
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { difference, sumBy, omit } from 'lodash';
|
||||
import { Service, Inject } from "typedi";
|
||||
import { difference, sumBy, omit, groupBy } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { ServiceError } from "exceptions";
|
||||
import { ServiceError } from 'exceptions';
|
||||
import {
|
||||
IManualJournalDTO,
|
||||
IManualJournalsService,
|
||||
@@ -27,11 +27,15 @@ const ERRORS = {
|
||||
CREDIT_DEBIT_NOT_EQUAL: 'credit_debit_not_equal',
|
||||
ACCCOUNTS_IDS_NOT_FOUND: 'acccounts_ids_not_found',
|
||||
JOURNAL_NUMBER_EXISTS: 'journal_number_exists',
|
||||
RECEIVABLE_ENTRIES_NO_CUSTOMERS: 'receivable_entries_have_no_customers',
|
||||
PAYABLE_ENTRIES_NO_VENDORS: 'payabel_entries_have_no_vendors',
|
||||
ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
|
||||
CONTACTS_NOT_FOUND: 'contacts_not_found',
|
||||
};
|
||||
|
||||
const CONTACTS_CONFIG = [
|
||||
{ accountBySlug: 'accounts-receivable', contactService: 'customer', assignRequired: false, },
|
||||
{ accountBySlug: 'accounts-payable', contactService: 'vendor', assignRequired: true },
|
||||
];
|
||||
|
||||
@Service()
|
||||
export default class ManualJournalsService implements IManualJournalsService {
|
||||
@Inject()
|
||||
@@ -48,31 +52,46 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
|
||||
/**
|
||||
* Validates the manual journal existance.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
*/
|
||||
private async validateManualJournalExistance(tenantId: number, manualJournalId: number) {
|
||||
private async validateManualJournalExistance(
|
||||
tenantId: number,
|
||||
manualJournalId: number
|
||||
) {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[manual_journal] trying to validate existance.', { tenantId, manualJournalId });
|
||||
this.logger.info('[manual_journal] trying to validate existance.', {
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
});
|
||||
const manualJournal = await ManualJournal.query().findById(manualJournalId);
|
||||
|
||||
if (!manualJournal) {
|
||||
this.logger.warn('[manual_journal] not exists on the storage.', { tenantId, manualJournalId });
|
||||
this.logger.warn('[manual_journal] not exists on the storage.', {
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
});
|
||||
throw new ServiceError(ERRORS.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate manual journals existance.
|
||||
* @param {number} tenantId
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} manualJournalsIds
|
||||
* @throws {ServiceError}
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
private async validateManualJournalsExistance(tenantId: number, manualJournalsIds: number[]) {
|
||||
private async validateManualJournalsExistance(
|
||||
tenantId: number,
|
||||
manualJournalsIds: number[]
|
||||
) {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
|
||||
const manualJournals = await ManualJournal.query().whereIn('id', manualJournalsIds);
|
||||
const manualJournals = await ManualJournal.query().whereIn(
|
||||
'id',
|
||||
manualJournalsIds
|
||||
);
|
||||
|
||||
const notFoundManualJournals = difference(
|
||||
manualJournalsIds,
|
||||
@@ -85,7 +104,7 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
|
||||
/**
|
||||
* Validate manual journal credit and debit should be equal.
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*/
|
||||
private valdiateCreditDebitTotalEquals(manualJournalDTO: IManualJournalDTO) {
|
||||
let totalCredit = 0;
|
||||
@@ -100,23 +119,30 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
}
|
||||
});
|
||||
if (totalCredit <= 0 || totalDebit <= 0) {
|
||||
this.logger.info('[manual_journal] the total credit and debit equals zero.');
|
||||
this.logger.info(
|
||||
'[manual_journal] the total credit and debit equals zero.'
|
||||
);
|
||||
throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL_ZERO);
|
||||
}
|
||||
if (totalCredit !== totalDebit) {
|
||||
this.logger.info('[manual_journal] the total credit not equals total debit.');
|
||||
this.logger.info(
|
||||
'[manual_journal] the total credit not equals total debit.'
|
||||
);
|
||||
throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate manual entries accounts existance on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*/
|
||||
private async validateAccountsExistance(tenantId: number, manualJournalDTO: IManualJournalDTO) {
|
||||
private async validateAccountsExistance(
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO
|
||||
) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const manualAccountsIds = manualJournalDTO.entries.map(e => e.accountId);
|
||||
const manualAccountsIds = manualJournalDTO.entries.map((e) => e.accountId);
|
||||
|
||||
const accounts = await Account.query()
|
||||
.whereIn('id', manualAccountsIds)
|
||||
@@ -125,96 +151,146 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
const storedAccountsIds = accounts.map((account) => account.id);
|
||||
|
||||
if (difference(manualAccountsIds, storedAccountsIds).length > 0) {
|
||||
this.logger.info('[manual_journal] some entries accounts not exist.', { tenantId, manualAccountsIds });
|
||||
this.logger.info('[manual_journal] some entries accounts not exist.', {
|
||||
tenantId,
|
||||
manualAccountsIds,
|
||||
});
|
||||
throw new ServiceError(ERRORS.ACCCOUNTS_IDS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate manual journal number unique.
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*/
|
||||
private async validateManualJournalNoUnique(tenantId: number, manualJournalDTO: IManualJournalDTO, notId?: numebr) {
|
||||
private async validateManualJournalNoUnique(
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
notId?: number
|
||||
) {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
const journalNumber = await ManualJournal.query().where(
|
||||
'journal_number',
|
||||
manualJournalDTO.journalNumber,
|
||||
).onBuild((builder) => {
|
||||
if (notId) {
|
||||
builder.whereNot('id', notId);
|
||||
}
|
||||
});
|
||||
const journalNumber = await ManualJournal.query()
|
||||
.where('journal_number', manualJournalDTO.journalNumber)
|
||||
.onBuild((builder) => {
|
||||
if (notId) {
|
||||
builder.whereNot('id', notId);
|
||||
}
|
||||
});
|
||||
if (journalNumber.length > 0) {
|
||||
throw new ServiceError(ERRORS.JOURNAL_NUMBER_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate entries that have receivable account should have customer type.
|
||||
* @param {number} tenantId -
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {string} accountBySlug
|
||||
* @param {string} contactType
|
||||
*/
|
||||
private async validateReceivableEntries(tenantId: number, manualJournalDTO: IManualJournalDTO): Promise<void> {
|
||||
private async validateAccountsWithContactType(
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
accountBySlug: string,
|
||||
contactType: string,
|
||||
contactRequired: boolean = true,
|
||||
): Promise<void> {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
const receivableAccount = await accountRepository.getBySlug('accounts-receivable');
|
||||
|
||||
const entriesHasNoReceivableAccount = manualJournalDTO.entries.filter(
|
||||
(e) => (e.accountId === receivableAccount.id) &&
|
||||
(!e.contactId || e.contactType !== 'customer')
|
||||
);
|
||||
if (entriesHasNoReceivableAccount.length > 0) {
|
||||
throw new ServiceError(ERRORS.RECEIVABLE_ENTRIES_NO_CUSTOMERS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates payable entries should have vendor type.
|
||||
* @param {number} tenantId -
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*/
|
||||
private async validatePayableEntries(tenantId: number, manualJournalDTO: IManualJournalDTO): Promise<void> {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
const payableAccount = await accountRepository.getBySlug('accounts-payable');
|
||||
|
||||
const payableAccount = await accountRepository.getBySlug(accountBySlug);
|
||||
const entriesHasNoVendorContact = manualJournalDTO.entries.filter(
|
||||
(e) => (e.accountId === payableAccount.id) &&
|
||||
(!e.contactId || e.contactType !== 'vendor')
|
||||
(e) =>
|
||||
e.accountId === payableAccount.id &&
|
||||
((!e.contactId && contactRequired) || e.contactType !== contactType)
|
||||
);
|
||||
if (entriesHasNoVendorContact.length > 0) {
|
||||
throw new ServiceError(ERRORS.PAYABLE_ENTRIES_NO_VENDORS);
|
||||
const indexes = entriesHasNoVendorContact.map(e => e.index);
|
||||
|
||||
throw new ServiceError(ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, '', {
|
||||
contactType,
|
||||
accountBySlug,
|
||||
indexes
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vaplidate entries contacts existance.
|
||||
* @param {number} tenantId -
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* Dynamic validates accounts with contacts.
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*/
|
||||
private async validateContactsExistance(tenantId: number, manualJournalDTO: IManualJournalDTO) {
|
||||
private async dynamicValidateAccountsWithContactType(
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
): Promise<any>{
|
||||
return Promise.all(
|
||||
CONTACTS_CONFIG.map(({ accountBySlug, contactService, assignRequired }) =>
|
||||
this.validateAccountsWithContactType(
|
||||
tenantId,
|
||||
manualJournalDTO,
|
||||
accountBySlug,
|
||||
contactService,
|
||||
assignRequired
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vaplidate entries contacts existance.
|
||||
* @param {number} tenantId -
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*/
|
||||
private async validateContactsExistance(
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
) {
|
||||
const { contactRepository } = this.tenancy.repositories(tenantId);
|
||||
const manualJCotactsIds = manualJournalDTO.entries
|
||||
.filter((entry) => entry.contactId)
|
||||
.map((entry) => entry.contactId);
|
||||
|
||||
if (manualJCotactsIds.length > 0) {
|
||||
const storedContacts = await contactRepository.findByIds(manualJCotactsIds);
|
||||
const storedContactsIds = storedContacts.map((c) => c.id);
|
||||
// Filters the entries that have contact only.
|
||||
const entriesContactPairs = manualJournalDTO.entries
|
||||
.filter((entry) => entry.contactId);
|
||||
|
||||
const notFoundContactsIds = difference(manualJCotactsIds, storedContactsIds);
|
||||
if (entriesContactPairs.length > 0) {
|
||||
const entriesContactsIds = entriesContactPairs.map(entry => entry.contactId);
|
||||
|
||||
// Retrieve all stored contacts on the storage from contacts entries.
|
||||
const storedContacts = await contactRepository.findByIds(
|
||||
entriesContactsIds,
|
||||
);
|
||||
// Converts the stored contacts to map with id as key and entry as value.
|
||||
const storedContactsMap = new Map(storedContacts.map(contact => [contact.id, contact]));
|
||||
const notFoundContactsIds = [];
|
||||
|
||||
entriesContactPairs.forEach((contactEntry) => {
|
||||
const storedContact = storedContactsMap.get(contactEntry.contactId);
|
||||
|
||||
// in case the contact id not found or contact type no equals pushed to
|
||||
// not found contacts.
|
||||
if (
|
||||
!storedContact ||
|
||||
storedContact.contactService !== contactEntry.contactType
|
||||
) {
|
||||
notFoundContactsIds.push(storedContact);
|
||||
}
|
||||
});
|
||||
if (notFoundContactsIds.length > 0) {
|
||||
throw new ServiceError(ERRORS.CONTACTS_NOT_FOUND);
|
||||
throw new ServiceError(ERRORS.CONTACTS_NOT_FOUND, '', {
|
||||
contactsIds: notFoundContactsIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform manual journal DTO to graphed model to save it.
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
private transformDTOToModel(manualJournalDTO: IManualJournalDTO, user: ISystemUser): IManualJournal {
|
||||
private transformDTOToModel(
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
user: ISystemUser
|
||||
): IManualJournal {
|
||||
const amount = sumBy(manualJournalDTO.entries, 'credit') || 0;
|
||||
const date = moment(manualJournalDTO.date).format('YYYY-MM-DD');
|
||||
|
||||
@@ -229,20 +305,20 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
|
||||
/**
|
||||
* Transform DTO to model.
|
||||
* @param {IManualJournalEntryDTO[]} entries
|
||||
* @param {IManualJournalEntryDTO[]} entries
|
||||
*/
|
||||
private transformDTOToEntriesModel(entries: IManualJournalEntryDTO[]) {
|
||||
return entries.map((entry: IManualJournalEntryDTO) => ({
|
||||
...omit(entry, ['accountId']),
|
||||
account: entry.accountId,
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make journal entries.
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
public async makeJournalEntries(
|
||||
tenantId: number,
|
||||
@@ -251,38 +327,51 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
): Promise<{ manualJournal: IManualJournal }> {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
|
||||
// Validate the total credit should equals debit.
|
||||
this.valdiateCreditDebitTotalEquals(manualJournalDTO);
|
||||
|
||||
await this.validateReceivableEntries(tenantId, manualJournalDTO);
|
||||
await this.validatePayableEntries(tenantId, manualJournalDTO);
|
||||
// Validate the contacts existance.
|
||||
await this.validateContactsExistance(tenantId, manualJournalDTO);
|
||||
|
||||
// Validate entries accounts existance.
|
||||
await this.validateAccountsExistance(tenantId, manualJournalDTO);
|
||||
|
||||
// Validate manual journal uniquiness on the storage.
|
||||
await this.validateManualJournalNoUnique(tenantId, manualJournalDTO);
|
||||
|
||||
this.logger.info('[manual_journal] trying to save manual journal to the storage.', { tenantId, manualJournalDTO });
|
||||
const manualJournalObj = this.transformDTOToModel(manualJournalDTO, authorizedUser);
|
||||
// Validate accounts with contact type from the given config.
|
||||
await this.dynamicValidateAccountsWithContactType(tenantId, manualJournalDTO);
|
||||
|
||||
const storedManualJournal = await ManualJournal.query().insert({
|
||||
this.logger.info(
|
||||
'[manual_journal] trying to save manual journal to the storage.',
|
||||
{ tenantId, manualJournalDTO }
|
||||
);
|
||||
const manualJournalObj = this.transformDTOToModel(
|
||||
manualJournalDTO,
|
||||
authorizedUser
|
||||
);
|
||||
const manualJournal = await ManualJournal.query().insertAndFetch({
|
||||
...omit(manualJournalObj, ['entries']),
|
||||
});
|
||||
const manualJournal: IManualJournal = { ...manualJournalObj, id: storedManualJournal.id };
|
||||
|
||||
// Triggers `onManualJournalCreated` event.
|
||||
this.eventDispatcher.dispatch(events.manualJournals.onCreated, {
|
||||
tenantId, manualJournal,
|
||||
tenantId,
|
||||
manualJournal,
|
||||
});
|
||||
this.logger.info('[manual_journal] the manual journal inserted successfully.', { tenantId });
|
||||
this.logger.info(
|
||||
'[manual_journal] the manual journal inserted successfully.',
|
||||
{ tenantId }
|
||||
);
|
||||
|
||||
return { manualJournal };
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits jouranl entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {IMakeJournalDTO} manualJournalDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {IMakeJournalDTO} manualJournalDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
public async editJournalEntries(
|
||||
tenantId: number,
|
||||
@@ -292,123 +381,200 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
): Promise<{ manualJournal: IManualJournal }> {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
|
||||
// Validates the manual journal existance on the storage.
|
||||
await this.validateManualJournalExistance(tenantId, manualJournalId);
|
||||
|
||||
// Validates the total credit and debit to be equals.
|
||||
this.valdiateCreditDebitTotalEquals(manualJournalDTO);
|
||||
|
||||
// Validate the contacts existance.
|
||||
await this.validateContactsExistance(tenantId, manualJournalDTO);
|
||||
|
||||
// Validates entries accounts existance.
|
||||
await this.validateAccountsExistance(tenantId, manualJournalDTO);
|
||||
await this.validateManualJournalNoUnique(tenantId, manualJournalDTO, manualJournalId);
|
||||
|
||||
const manualJournalObj = this.transformDTOToModel(manualJournalDTO, authorizedUser);
|
||||
// Validates the manual journal number uniquiness.
|
||||
await this.validateManualJournalNoUnique(
|
||||
tenantId,
|
||||
manualJournalDTO,
|
||||
manualJournalId
|
||||
);
|
||||
// Validate accounts with contact type from the given config.
|
||||
await this.dynamicValidateAccountsWithContactType(tenantId, manualJournalDTO);
|
||||
|
||||
const storedManualJournal = await ManualJournal.query().where('id', manualJournalId)
|
||||
const manualJournalObj = this.transformDTOToModel(
|
||||
manualJournalDTO,
|
||||
authorizedUser
|
||||
);
|
||||
|
||||
const storedManualJournal = await ManualJournal.query()
|
||||
.where('id', manualJournalId)
|
||||
.patch({
|
||||
...omit(manualJournalObj, ['entries']),
|
||||
});
|
||||
const manualJournal: IManualJournal = { ...manualJournalObj, id: manualJournalId };
|
||||
const manualJournal: IManualJournal = {
|
||||
...manualJournalObj,
|
||||
id: manualJournalId,
|
||||
};
|
||||
|
||||
// Triggers `onManualJournalEdited` event.
|
||||
this.eventDispatcher.dispatch(events.manualJournals.onEdited, {
|
||||
tenantId, manualJournal,
|
||||
tenantId,
|
||||
manualJournal,
|
||||
});
|
||||
return { manualJournal };
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given manual journal
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteManualJournal(tenantId: number, manualJournalId: number): Promise<void> {
|
||||
public async deleteManualJournal(
|
||||
tenantId: number,
|
||||
manualJournalId: number
|
||||
): Promise<void> {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
await this.validateManualJournalExistance(tenantId, manualJournalId);
|
||||
|
||||
this.logger.info('[manual_journal] trying to delete the manual journal.', { tenantId, manualJournalId });
|
||||
this.logger.info('[manual_journal] trying to delete the manual journal.', {
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
});
|
||||
await ManualJournal.query().findById(manualJournalId).delete();
|
||||
|
||||
// Triggers `onManualJournalDeleted` event.
|
||||
this.eventDispatcher.dispatch(events.manualJournals.onDeleted, {
|
||||
tenantId, manualJournalId,
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
});
|
||||
this.logger.info('[manual_journal] the given manual journal deleted successfully.', { tenantId, manualJournalId });
|
||||
this.logger.info(
|
||||
'[manual_journal] the given manual journal deleted successfully.',
|
||||
{ tenantId, manualJournalId }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the given manual journals.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} manualJournalsIds
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} manualJournalsIds
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteManualJournals(tenantId: number, manualJournalsIds: number[]): Promise<void> {
|
||||
public async deleteManualJournals(
|
||||
tenantId: number,
|
||||
manualJournalsIds: number[]
|
||||
): Promise<void> {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
await this.validateManualJournalsExistance(tenantId, manualJournalsIds);
|
||||
|
||||
this.logger.info('[manual_journal] trying to delete the manual journals.', { tenantId, manualJournalsIds });
|
||||
this.logger.info('[manual_journal] trying to delete the manual journals.', {
|
||||
tenantId,
|
||||
manualJournalsIds,
|
||||
});
|
||||
await ManualJournal.query().whereIn('id', manualJournalsIds).delete();
|
||||
|
||||
// Triggers `onManualJournalDeletedBulk` event.
|
||||
this.eventDispatcher.dispatch(events.manualJournals.onDeletedBulk, {
|
||||
tenantId, manualJournalsIds,
|
||||
tenantId,
|
||||
manualJournalsIds,
|
||||
});
|
||||
this.logger.info('[manual_journal] the given manual journals deleted successfully.', { tenantId, manualJournalsIds });
|
||||
this.logger.info(
|
||||
'[manual_journal] the given manual journals deleted successfully.',
|
||||
{ tenantId, manualJournalsIds }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the given manual journals.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} manualJournalsIds
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} manualJournalsIds
|
||||
*/
|
||||
public async publishManualJournals(tenantId: number, manualJournalsIds: number[]): Promise<void> {
|
||||
public async publishManualJournals(
|
||||
tenantId: number,
|
||||
manualJournalsIds: number[]
|
||||
): Promise<void> {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
await this.validateManualJournalsExistance(tenantId, manualJournalsIds);
|
||||
|
||||
this.logger.info('[manual_journal] trying to publish the manual journal.', { tenantId, manualJournalsIds });
|
||||
await ManualJournal.query().whereIn('id', manualJournalsIds).patch({ status: 1, });
|
||||
this.logger.info('[manual_journal] trying to publish the manual journal.', {
|
||||
tenantId,
|
||||
manualJournalsIds,
|
||||
});
|
||||
await ManualJournal.query()
|
||||
.whereIn('id', manualJournalsIds)
|
||||
.patch({ status: 1 });
|
||||
|
||||
// Triggers `onManualJournalPublishedBulk` event.
|
||||
this.eventDispatcher.dispatch(events.manualJournals.onPublishedBulk, {
|
||||
tenantId, manualJournalsIds,
|
||||
tenantId,
|
||||
manualJournalsIds,
|
||||
});
|
||||
this.logger.info('[manual_journal] the given manula journal published successfully.', { tenantId, manualJournalId });
|
||||
this.logger.info(
|
||||
'[manual_journal] the given manula journal published successfully.',
|
||||
{ tenantId, manualJournalId }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the given manual journal.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
*/
|
||||
public async publishManualJournal(tenantId: number, manualJournalId: number): Promise<void> {
|
||||
public async publishManualJournal(
|
||||
tenantId: number,
|
||||
manualJournalId: number
|
||||
): Promise<void> {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
await this.validateManualJournalExistance(tenantId, manualJournalId);
|
||||
|
||||
this.logger.info('[manual_journal] trying to publish the manual journal.', { tenantId, manualJournalId });
|
||||
await ManualJournal.query().findById(manualJournalId).patch({ status: 1, });
|
||||
this.logger.info('[manual_journal] trying to publish the manual journal.', {
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
});
|
||||
await ManualJournal.query().findById(manualJournalId).patch({ status: 1 });
|
||||
|
||||
// Triggers `onManualJournalPublishedBulk` event.
|
||||
this.eventDispatcher.dispatch(events.manualJournals.onPublished, {
|
||||
tenantId, manualJournalId,
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
});
|
||||
this.logger.info('[manual_journal] the given manula journal published successfully.', { tenantId, manualJournalId });
|
||||
this.logger.info(
|
||||
'[manual_journal] the given manula journal published successfully.',
|
||||
{ tenantId, manualJournalId }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve manual journals datatable list.
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalsFilter} filter
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalsFilter} filter
|
||||
*/
|
||||
public async getManualJournals(
|
||||
tenantId: number,
|
||||
filter: IManualJournalsFilter
|
||||
): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
|
||||
): Promise<{
|
||||
manualJournals: IManualJournal;
|
||||
pagination: IPaginationMeta;
|
||||
filterMeta: IFilterMeta;
|
||||
}> {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
const dynamicList = await this.dynamicListService.dynamicList(tenantId, ManualJournal, filter);
|
||||
const dynamicList = await this.dynamicListService.dynamicList(
|
||||
tenantId,
|
||||
ManualJournal,
|
||||
filter
|
||||
);
|
||||
|
||||
this.logger.info('[manual_journals] trying to get manual journals list.', { tenantId, filter });
|
||||
const { results, pagination } = await ManualJournal.query().onBuild((builder) => {
|
||||
dynamicList.buildQuery()(builder);
|
||||
builder.withGraphFetched('entries.account');
|
||||
}).pagination(filter.page - 1, filter.pageSize);
|
||||
this.logger.info('[manual_journals] trying to get manual journals list.', {
|
||||
tenantId,
|
||||
filter,
|
||||
});
|
||||
const { results, pagination } = await ManualJournal.query()
|
||||
.onBuild((builder) => {
|
||||
dynamicList.buildQuery()(builder);
|
||||
builder.withGraphFetched('entries.account');
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
return {
|
||||
manualJournals: results,
|
||||
@@ -416,18 +582,21 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
filterMeta: dynamicList.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve manual journal details with assocaited journal transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
*/
|
||||
public async getManualJournal(tenantId: number, manualJournalId: number) {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
|
||||
await this.validateManualJournalExistance(tenantId, manualJournalId);
|
||||
|
||||
this.logger.info('[manual_journals] trying to get specific manual journal.', { tenantId, manualJournalId });
|
||||
this.logger.info(
|
||||
'[manual_journals] trying to get specific manual journal.',
|
||||
{ tenantId, manualJournalId }
|
||||
);
|
||||
const manualJournal = await ManualJournal.query()
|
||||
.findById(manualJournalId)
|
||||
.withGraphFetched('entries')
|
||||
@@ -438,33 +607,42 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
|
||||
/**
|
||||
* Write manual journal entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {IManualJournal|null} manualJournalObj
|
||||
* @param {boolean} override
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {IManualJournal|null} manualJournalObj
|
||||
* @param {boolean} override
|
||||
*/
|
||||
public async writeJournalEntries(
|
||||
tenantId: number,
|
||||
manualJournalId: number,
|
||||
manualJournalObj?: IManualJournal|null,
|
||||
override?: Boolean,
|
||||
manualJournalObj?: IManualJournal | null,
|
||||
override?: Boolean
|
||||
) {
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const journalCommands = new JournalCommands(journal);
|
||||
|
||||
if (override) {
|
||||
this.logger.info('[manual_journal] trying to revert journal entries.', { tenantId, manualJournalId });
|
||||
this.logger.info('[manual_journal] trying to revert journal entries.', {
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
});
|
||||
await journalCommands.revertJournalEntries(manualJournalId, 'Journal');
|
||||
}
|
||||
if (manualJournalObj) {
|
||||
journalCommands.manualJournal(manualJournalObj, manualJournalId);
|
||||
}
|
||||
this.logger.info('[manual_journal] trying to save journal entries.', { tenantId, manualJournalId });
|
||||
this.logger.info('[manual_journal] trying to save journal entries.', {
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
});
|
||||
await Promise.all([
|
||||
journal.saveBalance(),
|
||||
journal.deleteEntries(),
|
||||
journal.saveEntries(),
|
||||
]);
|
||||
this.logger.info('[manual_journal] the journal entries saved successfully.', { tenantId, manualJournalId });
|
||||
this.logger.info(
|
||||
'[manual_journal] the journal entries saved successfully.',
|
||||
{ tenantId, manualJournalId }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user