diff --git a/client/src/containers/Accounting/utils.js b/client/src/containers/Accounting/utils.js index bf93def5e..d33496c1b 100644 --- a/client/src/containers/Accounting/utils.js +++ b/client/src/containers/Accounting/utils.js @@ -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, }); } -}; \ No newline at end of file +}; diff --git a/server/src/api/controllers/ManualJournals.ts b/server/src/api/controllers/ManualJournals.ts index d57be2afc..21764dd48 100644 --- a/server/src/api/controllers/ManualJournals.ts +++ b/server/src/api/controllers/ManualJournals.ts @@ -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 }] }, ); } } diff --git a/server/src/exceptions/ServiceError.ts b/server/src/exceptions/ServiceError.ts index 4a2ce8d12..2e3139805 100644 --- a/server/src/exceptions/ServiceError.ts +++ b/server/src/exceptions/ServiceError.ts @@ -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; } } \ No newline at end of file diff --git a/server/src/services/ManualJournals/ManualJournalsService.ts b/server/src/services/ManualJournals/ManualJournalsService.ts index 30ef8dab0..f7cace98d 100644 --- a/server/src/services/ManualJournals/ManualJournalsService.ts +++ b/server/src/services/ManualJournals/ManualJournalsService.ts @@ -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 { + private async validateAccountsWithContactType( + tenantId: number, + manualJournalDTO: IManualJournalDTO, + accountBySlug: string, + contactType: string, + contactRequired: boolean = true, + ): Promise { 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 { - 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{ + 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} */ - public async deleteManualJournal(tenantId: number, manualJournalId: number): Promise { + public async deleteManualJournal( + tenantId: number, + manualJournalId: number + ): Promise { 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} */ - public async deleteManualJournals(tenantId: number, manualJournalsIds: number[]): Promise { + public async deleteManualJournals( + tenantId: number, + manualJournalsIds: number[] + ): Promise { 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 { + public async publishManualJournals( + tenantId: number, + manualJournalsIds: number[] + ): Promise { 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 { + public async publishManualJournal( + tenantId: number, + manualJournalId: number + ): Promise { 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 } + ); } -} \ No newline at end of file +}