fix: handle make journal errors with contacts.

This commit is contained in:
Ahmed Bouhuolia
2020-11-30 17:56:13 +02:00
parent 5c5a9438ee
commit b5b9764676
4 changed files with 370 additions and 195 deletions

View File

@@ -12,6 +12,7 @@ const ERROR = {
RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS', RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS',
CREDIT_DEBIT_SUMATION_SHOULD_NOT_EQUAL_ZERO: CREDIT_DEBIT_SUMATION_SHOULD_NOT_EQUAL_ZERO:
'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. // Transform API errors in toasts messages.
@@ -27,14 +28,6 @@ export const transformErrors = (resErrors, { setErrors, errors }) => {
newErrors = setWith(newErrors, `entries.[${index}].${prop}`, message); 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))) { if ((error = getError(ERROR.RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS))) {
toastMessages.push( toastMessages.push(
formatMessage({ formatMessage({
@@ -43,21 +36,20 @@ export const transformErrors = (resErrors, { setErrors, errors }) => {
); );
setEntriesErrors(error.indexes, 'contact_id', 'error'); setEntriesErrors(error.indexes, 'contact_id', 'error');
} }
if ((error = getError(ERROR.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC))) { if ((error = getError(ERROR.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT))) {
if (error.meta.contact_type === 'customer') {
toastMessages.push( toastMessages.push(
formatMessage({ formatMessage({
id: 'customers_should_selected_with_receivable_account_only', id: 'receivable_accounts_should_assign_with_customers',
}), }),
); );
setEntriesErrors(error.indexes, 'account_id', 'error');
} }
if ((error = getError(ERROR.VENDORS_NOT_WITH_PAYABLE_ACCOUNT))) { if (error.meta.contact_type === 'vendor') {
toastMessages.push( toastMessages.push(
formatMessage({ formatMessage({ id: 'payable_accounts_should_assign_with_vendors' }),
id: 'vendors_should_selected_with_payable_account_only',
}),
); );
setEntriesErrors(error.indexes, 'account_id', 'error'); }
setEntriesErrors(error.meta.indexes, 'contact_id', 'error');
} }
if ((error = getError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS))) { if ((error = getError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS))) {
newErrors = setWith( newErrors = setWith(

View File

@@ -30,12 +30,12 @@ export default class ManualJournalsController extends BaseController {
this.validationResult, this.validationResult,
asyncMiddleware(this.getManualJournalsList.bind(this)), asyncMiddleware(this.getManualJournalsList.bind(this)),
this.dynamicListService.handlerErrorsToResponse, this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors, this.catchServiceErrors.bind(this),
); );
router.get( router.get(
'/:id', '/:id',
asyncMiddleware(this.getManualJournal.bind(this)), asyncMiddleware(this.getManualJournal.bind(this)),
this.catchServiceErrors, this.catchServiceErrors.bind(this),
); );
router.post( router.post(
'/publish', [ '/publish', [
@@ -43,7 +43,7 @@ export default class ManualJournalsController extends BaseController {
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.publishManualJournals.bind(this)), asyncMiddleware(this.publishManualJournals.bind(this)),
this.catchServiceErrors, this.catchServiceErrors.bind(this),
); );
router.post( router.post(
'/:id/publish', [ '/:id/publish', [
@@ -51,7 +51,7 @@ export default class ManualJournalsController extends BaseController {
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.publishManualJournal.bind(this)), asyncMiddleware(this.publishManualJournal.bind(this)),
this.catchServiceErrors, this.catchServiceErrors.bind(this),
); );
router.post( router.post(
'/:id', [ '/:id', [
@@ -60,7 +60,7 @@ export default class ManualJournalsController extends BaseController {
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.editManualJournal.bind(this)), asyncMiddleware(this.editManualJournal.bind(this)),
this.catchServiceErrors, this.catchServiceErrors.bind(this),
); );
router.delete( router.delete(
'/:id', [ '/:id', [
@@ -68,7 +68,7 @@ export default class ManualJournalsController extends BaseController {
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteManualJournal.bind(this)), asyncMiddleware(this.deleteManualJournal.bind(this)),
this.catchServiceErrors, this.catchServiceErrors.bind(this),
); );
router.delete( router.delete(
'/', [ '/', [
@@ -76,7 +76,7 @@ export default class ManualJournalsController extends BaseController {
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteBulkManualJournals.bind(this)), asyncMiddleware(this.deleteBulkManualJournals.bind(this)),
this.catchServiceErrors, this.catchServiceErrors.bind(this),
); );
router.post( router.post(
'/', [ '/', [
@@ -84,7 +84,7 @@ export default class ManualJournalsController extends BaseController {
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.makeJournalEntries.bind(this)), asyncMiddleware(this.makeJournalEntries.bind(this)),
this.catchServiceErrors, this.catchServiceErrors.bind(this),
); );
return router; return router;
} }
@@ -133,7 +133,7 @@ export default class ManualJournalsController extends BaseController {
.escape() .escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('description') check('description')
.optional() .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape() .escape()
@@ -153,7 +153,7 @@ export default class ManualJournalsController extends BaseController {
.toFloat(), .toFloat(),
check('entries.*.account_id').isInt({ max: DATATYPES_LENGTH.INT_10 }).toInt(), check('entries.*.account_id').isInt({ max: DATATYPES_LENGTH.INT_10 }).toInt(),
check('entries.*.note') check('entries.*.note')
.optional() .optional({ nullable: true })
.isString() .isString()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('entries.*.contact_id') check('entries.*.contact_id')
@@ -368,43 +368,45 @@ export default class ManualJournalsController extends BaseController {
if (error.errorType === 'credit_debit_not_equal_zero') { if (error.errorType === 'credit_debit_not_equal_zero') {
return res.boom.badRequest( return res.boom.badRequest(
'Credit and debit should not be equal zero.', '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') { if (error.errorType === 'credit_debit_not_equal') {
return res.boom.badRequest( return res.boom.badRequest(
'Credit and debit should be equal.', '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') { if (error.errorType === 'acccounts_ids_not_found') {
return res.boom.badRequest( return res.boom.badRequest(
'Journal entries some of accounts ids not exists.', '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') { if (error.errorType === 'journal_number_exists') {
return res.boom.badRequest( return res.boom.badRequest(
'Journal number should be unique.', '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( return res.boom.badRequest(
'', '',
{ errors: [{ type: '' }] }, {
); errors: [
{
type: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
code: 600,
meta: this.transfromToResponse(error.payload),
} }
if (error.errorType === 'receivable_entries_have_no_customers') { ]
return res.boom.badRequest( },
'',
{ errors: [{ type: '' }] },
); );
} }
if (error.errorType === 'contacts_not_found') { if (error.errorType === 'contacts_not_found') {
return res.boom.badRequest( return res.boom.badRequest(
'', '',
{ errors: [{ type: '' }] }, { errors: [{ type: 'CONTACTS_NOT_FOUND', code: 700 }] },
); );
} }
} }

View File

@@ -3,9 +3,12 @@
export default class ServiceError { export default class ServiceError {
errorType: string; errorType: string;
message: string; message: string;
payload: any;
constructor(errorType: string, message?: string) { constructor(errorType: string, message?: string, payload?: any) {
this.errorType = errorType; this.errorType = errorType;
this.message = message || null; this.message = message || null;
this.payload = payload;
} }
} }

View File

@@ -1,7 +1,7 @@
import { difference, sumBy, omit } from 'lodash'; import { difference, sumBy, omit, groupBy } from 'lodash';
import { Service, Inject } from "typedi"; import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { ServiceError } from "exceptions"; import { ServiceError } from 'exceptions';
import { import {
IManualJournalDTO, IManualJournalDTO,
IManualJournalsService, IManualJournalsService,
@@ -27,11 +27,15 @@ const ERRORS = {
CREDIT_DEBIT_NOT_EQUAL: 'credit_debit_not_equal', CREDIT_DEBIT_NOT_EQUAL: 'credit_debit_not_equal',
ACCCOUNTS_IDS_NOT_FOUND: 'acccounts_ids_not_found', ACCCOUNTS_IDS_NOT_FOUND: 'acccounts_ids_not_found',
JOURNAL_NUMBER_EXISTS: 'journal_number_exists', JOURNAL_NUMBER_EXISTS: 'journal_number_exists',
RECEIVABLE_ENTRIES_NO_CUSTOMERS: 'receivable_entries_have_no_customers', ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
PAYABLE_ENTRIES_NO_VENDORS: 'payabel_entries_have_no_vendors',
CONTACTS_NOT_FOUND: 'contacts_not_found', CONTACTS_NOT_FOUND: 'contacts_not_found',
}; };
const CONTACTS_CONFIG = [
{ accountBySlug: 'accounts-receivable', contactService: 'customer', assignRequired: false, },
{ accountBySlug: 'accounts-payable', contactService: 'vendor', assignRequired: true },
];
@Service() @Service()
export default class ManualJournalsService implements IManualJournalsService { export default class ManualJournalsService implements IManualJournalsService {
@Inject() @Inject()
@@ -51,14 +55,23 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {number} tenantId * @param {number} tenantId
* @param {number} manualJournalId * @param {number} manualJournalId
*/ */
private async validateManualJournalExistance(tenantId: number, manualJournalId: number) { private async validateManualJournalExistance(
tenantId: number,
manualJournalId: number
) {
const { ManualJournal } = this.tenancy.models(tenantId); 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); const manualJournal = await ManualJournal.query().findById(manualJournalId);
if (!manualJournal) { 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); throw new ServiceError(ERRORS.NOT_FOUND);
} }
} }
@@ -69,10 +82,16 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {number[]} manualJournalsIds * @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 { ManualJournal } = this.tenancy.models(tenantId);
const manualJournals = await ManualJournal.query().whereIn('id', manualJournalsIds); const manualJournals = await ManualJournal.query().whereIn(
'id',
manualJournalsIds
);
const notFoundManualJournals = difference( const notFoundManualJournals = difference(
manualJournalsIds, manualJournalsIds,
@@ -100,11 +119,15 @@ export default class ManualJournalsService implements IManualJournalsService {
} }
}); });
if (totalCredit <= 0 || totalDebit <= 0) { 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); throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL_ZERO);
} }
if (totalCredit !== totalDebit) { 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); throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL);
} }
} }
@@ -114,9 +137,12 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {number} tenantId * @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO * @param {IManualJournalDTO} manualJournalDTO
*/ */
private async validateAccountsExistance(tenantId: number, manualJournalDTO: IManualJournalDTO) { private async validateAccountsExistance(
tenantId: number,
manualJournalDTO: IManualJournalDTO
) {
const { Account } = this.tenancy.models(tenantId); 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() const accounts = await Account.query()
.whereIn('id', manualAccountsIds) .whereIn('id', manualAccountsIds)
@@ -125,7 +151,10 @@ export default class ManualJournalsService implements IManualJournalsService {
const storedAccountsIds = accounts.map((account) => account.id); const storedAccountsIds = accounts.map((account) => account.id);
if (difference(manualAccountsIds, storedAccountsIds).length > 0) { 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); throw new ServiceError(ERRORS.ACCCOUNTS_IDS_NOT_FOUND);
} }
} }
@@ -135,12 +164,15 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {number} tenantId * @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO * @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 { ManualJournal } = this.tenancy.models(tenantId);
const journalNumber = await ManualJournal.query().where( const journalNumber = await ManualJournal.query()
'journal_number', .where('journal_number', manualJournalDTO.journalNumber)
manualJournalDTO.journalNumber, .onBuild((builder) => {
).onBuild((builder) => {
if (notId) { if (notId) {
builder.whereNot('id', notId); builder.whereNot('id', notId);
} }
@@ -151,39 +183,57 @@ export default class ManualJournalsService implements IManualJournalsService {
} }
/** /**
* Validate entries that have receivable account should have customer type. *
* @param {number} tenantId - * @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO * @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 { accountRepository } = this.tenancy.repositories(tenantId);
const receivableAccount = await accountRepository.getBySlug('accounts-receivable'); const payableAccount = await accountRepository.getBySlug(accountBySlug);
const entriesHasNoVendorContact = manualJournalDTO.entries.filter(
const entriesHasNoReceivableAccount = manualJournalDTO.entries.filter( (e) =>
(e) => (e.accountId === receivableAccount.id) && e.accountId === payableAccount.id &&
(!e.contactId || e.contactType !== 'customer') ((!e.contactId && contactRequired) || e.contactType !== contactType)
); );
if (entriesHasNoReceivableAccount.length > 0) { if (entriesHasNoVendorContact.length > 0) {
throw new ServiceError(ERRORS.RECEIVABLE_ENTRIES_NO_CUSTOMERS); const indexes = entriesHasNoVendorContact.map(e => e.index);
throw new ServiceError(ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, '', {
contactType,
accountBySlug,
indexes
});
} }
} }
/** /**
* Validates payable entries should have vendor type. * Dynamic validates accounts with contacts.
* @param {number} tenantId - * @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO * @param {IManualJournalDTO} manualJournalDTO
*/ */
private async validatePayableEntries(tenantId: number, manualJournalDTO: IManualJournalDTO): Promise<void> { private async dynamicValidateAccountsWithContactType(
const { accountRepository } = this.tenancy.repositories(tenantId); tenantId: number,
const payableAccount = await accountRepository.getBySlug('accounts-payable'); manualJournalDTO: IManualJournalDTO,
): Promise<any>{
const entriesHasNoVendorContact = manualJournalDTO.entries.filter( return Promise.all(
(e) => (e.accountId === payableAccount.id) && CONTACTS_CONFIG.map(({ accountBySlug, contactService, assignRequired }) =>
(!e.contactId || e.contactType !== 'vendor') this.validateAccountsWithContactType(
tenantId,
manualJournalDTO,
accountBySlug,
contactService,
assignRequired
),
)
); );
if (entriesHasNoVendorContact.length > 0) {
throw new ServiceError(ERRORS.PAYABLE_ENTRIES_NO_VENDORS);
}
} }
/** /**
@@ -191,20 +241,43 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {number} tenantId - * @param {number} tenantId -
* @param {IManualJournalDTO} manualJournalDTO * @param {IManualJournalDTO} manualJournalDTO
*/ */
private async validateContactsExistance(tenantId: number, manualJournalDTO: IManualJournalDTO) { private async validateContactsExistance(
tenantId: number,
manualJournalDTO: IManualJournalDTO,
) {
const { contactRepository } = this.tenancy.repositories(tenantId); const { contactRepository } = this.tenancy.repositories(tenantId);
const manualJCotactsIds = manualJournalDTO.entries
.filter((entry) => entry.contactId)
.map((entry) => entry.contactId);
if (manualJCotactsIds.length > 0) { // Filters the entries that have contact only.
const storedContacts = await contactRepository.findByIds(manualJCotactsIds); const entriesContactPairs = manualJournalDTO.entries
const storedContactsIds = storedContacts.map((c) => c.id); .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) { if (notFoundContactsIds.length > 0) {
throw new ServiceError(ERRORS.CONTACTS_NOT_FOUND); throw new ServiceError(ERRORS.CONTACTS_NOT_FOUND, '', {
contactsIds: notFoundContactsIds,
});
} }
} }
} }
@@ -214,7 +287,10 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {IManualJournalDTO} manualJournalDTO * @param {IManualJournalDTO} manualJournalDTO
* @param {ISystemUser} authorizedUser * @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 amount = sumBy(manualJournalDTO.entries, 'credit') || 0;
const date = moment(manualJournalDTO.date).format('YYYY-MM-DD'); const date = moment(manualJournalDTO.date).format('YYYY-MM-DD');
@@ -235,7 +311,7 @@ export default class ManualJournalsService implements IManualJournalsService {
return entries.map((entry: IManualJournalEntryDTO) => ({ return entries.map((entry: IManualJournalEntryDTO) => ({
...omit(entry, ['accountId']), ...omit(entry, ['accountId']),
account: entry.accountId, account: entry.accountId,
})) }));
} }
/** /**
@@ -251,28 +327,41 @@ export default class ManualJournalsService implements IManualJournalsService {
): Promise<{ manualJournal: IManualJournal }> { ): Promise<{ manualJournal: IManualJournal }> {
const { ManualJournal } = this.tenancy.models(tenantId); const { ManualJournal } = this.tenancy.models(tenantId);
// Validate the total credit should equals debit.
this.valdiateCreditDebitTotalEquals(manualJournalDTO); this.valdiateCreditDebitTotalEquals(manualJournalDTO);
await this.validateReceivableEntries(tenantId, manualJournalDTO); // Validate the contacts existance.
await this.validatePayableEntries(tenantId, manualJournalDTO);
await this.validateContactsExistance(tenantId, manualJournalDTO); await this.validateContactsExistance(tenantId, manualJournalDTO);
// Validate entries accounts existance.
await this.validateAccountsExistance(tenantId, manualJournalDTO); await this.validateAccountsExistance(tenantId, manualJournalDTO);
// Validate manual journal uniquiness on the storage.
await this.validateManualJournalNoUnique(tenantId, manualJournalDTO); await this.validateManualJournalNoUnique(tenantId, manualJournalDTO);
this.logger.info('[manual_journal] trying to save manual journal to the storage.', { tenantId, manualJournalDTO }); // Validate accounts with contact type from the given config.
const manualJournalObj = this.transformDTOToModel(manualJournalDTO, authorizedUser); 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']), ...omit(manualJournalObj, ['entries']),
}); });
const manualJournal: IManualJournal = { ...manualJournalObj, id: storedManualJournal.id };
// Triggers `onManualJournalCreated` event. // Triggers `onManualJournalCreated` event.
this.eventDispatcher.dispatch(events.manualJournals.onCreated, { 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 }; return { manualJournal };
} }
@@ -292,24 +381,46 @@ export default class ManualJournalsService implements IManualJournalsService {
): Promise<{ manualJournal: IManualJournal }> { ): Promise<{ manualJournal: IManualJournal }> {
const { ManualJournal } = this.tenancy.models(tenantId); const { ManualJournal } = this.tenancy.models(tenantId);
// Validates the manual journal existance on the storage.
await this.validateManualJournalExistance(tenantId, manualJournalId); await this.validateManualJournalExistance(tenantId, manualJournalId);
// Validates the total credit and debit to be equals.
this.valdiateCreditDebitTotalEquals(manualJournalDTO); this.valdiateCreditDebitTotalEquals(manualJournalDTO);
// Validate the contacts existance.
await this.validateContactsExistance(tenantId, manualJournalDTO);
// Validates entries accounts existance.
await this.validateAccountsExistance(tenantId, manualJournalDTO); 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({ .patch({
...omit(manualJournalObj, ['entries']), ...omit(manualJournalObj, ['entries']),
}); });
const manualJournal: IManualJournal = { ...manualJournalObj, id: manualJournalId }; const manualJournal: IManualJournal = {
...manualJournalObj,
id: manualJournalId,
};
// Triggers `onManualJournalEdited` event. // Triggers `onManualJournalEdited` event.
this.eventDispatcher.dispatch(events.manualJournals.onEdited, { this.eventDispatcher.dispatch(events.manualJournals.onEdited, {
tenantId, manualJournal, tenantId,
manualJournal,
}); });
return { manualJournal }; return { manualJournal };
} }
@@ -320,18 +431,28 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {number} manualJournalId * @param {number} manualJournalId
* @return {Promise<void>} * @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); const { ManualJournal } = this.tenancy.models(tenantId);
await this.validateManualJournalExistance(tenantId, manualJournalId); 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(); await ManualJournal.query().findById(manualJournalId).delete();
// Triggers `onManualJournalDeleted` event. // Triggers `onManualJournalDeleted` event.
this.eventDispatcher.dispatch(events.manualJournals.onDeleted, { 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 }
);
} }
/** /**
@@ -340,18 +461,28 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {number[]} manualJournalsIds * @param {number[]} manualJournalsIds
* @return {Promise<void>} * @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); const { ManualJournal } = this.tenancy.models(tenantId);
await this.validateManualJournalsExistance(tenantId, manualJournalsIds); 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(); await ManualJournal.query().whereIn('id', manualJournalsIds).delete();
// Triggers `onManualJournalDeletedBulk` event. // Triggers `onManualJournalDeletedBulk` event.
this.eventDispatcher.dispatch(events.manualJournals.onDeletedBulk, { 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 }
);
} }
/** /**
@@ -359,18 +490,30 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {number} tenantId * @param {number} tenantId
* @param {number[]} manualJournalsIds * @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); const { ManualJournal } = this.tenancy.models(tenantId);
await this.validateManualJournalsExistance(tenantId, manualJournalsIds); await this.validateManualJournalsExistance(tenantId, manualJournalsIds);
this.logger.info('[manual_journal] trying to publish the manual journal.', { tenantId, manualJournalsIds }); this.logger.info('[manual_journal] trying to publish the manual journal.', {
await ManualJournal.query().whereIn('id', manualJournalsIds).patch({ status: 1, }); tenantId,
manualJournalsIds,
});
await ManualJournal.query()
.whereIn('id', manualJournalsIds)
.patch({ status: 1 });
// Triggers `onManualJournalPublishedBulk` event. // Triggers `onManualJournalPublishedBulk` event.
this.eventDispatcher.dispatch(events.manualJournals.onPublishedBulk, { 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 }
);
} }
/** /**
@@ -378,18 +521,28 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {number} tenantId * @param {number} tenantId
* @param {number} manualJournalId * @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); const { ManualJournal } = this.tenancy.models(tenantId);
await this.validateManualJournalExistance(tenantId, manualJournalId); await this.validateManualJournalExistance(tenantId, manualJournalId);
this.logger.info('[manual_journal] trying to publish the manual journal.', { tenantId, manualJournalId }); this.logger.info('[manual_journal] trying to publish the manual journal.', {
await ManualJournal.query().findById(manualJournalId).patch({ status: 1, }); tenantId,
manualJournalId,
});
await ManualJournal.query().findById(manualJournalId).patch({ status: 1 });
// Triggers `onManualJournalPublishedBulk` event. // Triggers `onManualJournalPublishedBulk` event.
this.eventDispatcher.dispatch(events.manualJournals.onPublished, { 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 }
);
} }
/** /**
@@ -400,15 +553,28 @@ export default class ManualJournalsService implements IManualJournalsService {
public async getManualJournals( public async getManualJournals(
tenantId: number, tenantId: number,
filter: IManualJournalsFilter filter: IManualJournalsFilter
): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }> { ): Promise<{
manualJournals: IManualJournal;
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { ManualJournal } = this.tenancy.models(tenantId); 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 }); this.logger.info('[manual_journals] trying to get manual journals list.', {
const { results, pagination } = await ManualJournal.query().onBuild((builder) => { tenantId,
filter,
});
const { results, pagination } = await ManualJournal.query()
.onBuild((builder) => {
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
builder.withGraphFetched('entries.account'); builder.withGraphFetched('entries.account');
}).pagination(filter.page - 1, filter.pageSize); })
.pagination(filter.page - 1, filter.pageSize);
return { return {
manualJournals: results, manualJournals: results,
@@ -427,7 +593,10 @@ export default class ManualJournalsService implements IManualJournalsService {
await this.validateManualJournalExistance(tenantId, manualJournalId); 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() const manualJournal = await ManualJournal.query()
.findById(manualJournalId) .findById(manualJournalId)
.withGraphFetched('entries') .withGraphFetched('entries')
@@ -446,25 +615,34 @@ export default class ManualJournalsService implements IManualJournalsService {
public async writeJournalEntries( public async writeJournalEntries(
tenantId: number, tenantId: number,
manualJournalId: number, manualJournalId: number,
manualJournalObj?: IManualJournal|null, manualJournalObj?: IManualJournal | null,
override?: Boolean, override?: Boolean
) { ) {
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal); const journalCommands = new JournalCommands(journal);
if (override) { 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'); await journalCommands.revertJournalEntries(manualJournalId, 'Journal');
} }
if (manualJournalObj) { if (manualJournalObj) {
journalCommands.manualJournal(manualJournalObj, manualJournalId); 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([ await Promise.all([
journal.saveBalance(), journal.saveBalance(),
journal.deleteEntries(), journal.deleteEntries(),
journal.saveEntries(), 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 }
);
} }
} }