mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
fix: specific expense associated expense account graph. fix: specific manual journal associated account and contact graph.
900 lines
26 KiB
TypeScript
900 lines
26 KiB
TypeScript
import { difference, sumBy, omit, map } from 'lodash';
|
|
import { Service, Inject } from 'typedi';
|
|
import moment from 'moment';
|
|
import { ServiceError, ServiceErrors } from 'exceptions';
|
|
import {
|
|
IManualJournalDTO,
|
|
IManualJournalsService,
|
|
IManualJournalsFilter,
|
|
ISystemUser,
|
|
IManualJournal,
|
|
IPaginationMeta,
|
|
IManualJournalEntry,
|
|
} from 'interfaces';
|
|
import TenancyService from 'services/Tenancy/TenancyService';
|
|
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
|
import events from 'subscribers/events';
|
|
import {
|
|
EventDispatcher,
|
|
EventDispatcherInterface,
|
|
} from 'decorators/eventDispatcher';
|
|
import JournalPoster from 'services/Accounting/JournalPoster';
|
|
import JournalCommands from 'services/Accounting/JournalCommands';
|
|
import JournalPosterService from 'services/Sales/JournalPosterService';
|
|
import AutoIncrementOrdersService from 'services/Sales/AutoIncrementOrdersService';
|
|
import { ERRORS } from './constants';
|
|
|
|
@Service()
|
|
export default class ManualJournalsService implements IManualJournalsService {
|
|
@Inject()
|
|
tenancy: TenancyService;
|
|
|
|
@Inject()
|
|
dynamicListService: DynamicListingService;
|
|
|
|
@Inject()
|
|
journalService: JournalPosterService;
|
|
|
|
@Inject('logger')
|
|
logger: any;
|
|
|
|
@EventDispatcher()
|
|
eventDispatcher: EventDispatcherInterface;
|
|
|
|
@Inject()
|
|
autoIncrementOrdersService: AutoIncrementOrdersService;
|
|
|
|
/**
|
|
* Validates the manual journal existance.
|
|
* @param {number} tenantId
|
|
* @param {number} manualJournalId
|
|
*/
|
|
private async getManualJournalOrThrowError(
|
|
tenantId: number,
|
|
manualJournalId: number
|
|
) {
|
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
|
|
|
this.logger.info('[manual_journal] trying to validate existance.', {
|
|
tenantId,
|
|
manualJournalId,
|
|
});
|
|
const manualJournal = await ManualJournal.query()
|
|
.findById(manualJournalId)
|
|
.withGraphFetched('entries');
|
|
|
|
if (!manualJournal) {
|
|
this.logger.warn('[manual_journal] not exists on the storage.', {
|
|
tenantId,
|
|
manualJournalId,
|
|
});
|
|
throw new ServiceError(ERRORS.NOT_FOUND);
|
|
}
|
|
return manualJournal;
|
|
}
|
|
|
|
/**
|
|
* Validate manual journals existance.
|
|
* @param {number} tenantId - Tenant id.
|
|
* @param {number[]} manualJournalsIds - Manual jorunal ids.
|
|
* @throws {ServiceError}
|
|
*/
|
|
private async getManualJournalsOrThrowError(
|
|
tenantId: number,
|
|
manualJournalsIds: number[]
|
|
) {
|
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
|
|
|
const manualJournals = await ManualJournal.query()
|
|
.whereIn('id', manualJournalsIds)
|
|
.withGraphFetched('entries');
|
|
|
|
const notFoundManualJournals = difference(
|
|
manualJournalsIds,
|
|
manualJournals.map((m) => m.id)
|
|
);
|
|
if (notFoundManualJournals.length > 0) {
|
|
throw new ServiceError(ERRORS.NOT_FOUND);
|
|
}
|
|
return manualJournals;
|
|
}
|
|
|
|
/**
|
|
* Validate manual journal credit and debit should be equal.
|
|
* @param {IManualJournalDTO} manualJournalDTO
|
|
*/
|
|
private valdiateCreditDebitTotalEquals(manualJournalDTO: IManualJournalDTO) {
|
|
let totalCredit = 0;
|
|
let totalDebit = 0;
|
|
|
|
manualJournalDTO.entries.forEach((entry) => {
|
|
if (entry.credit > 0) {
|
|
totalCredit += entry.credit;
|
|
}
|
|
if (entry.debit > 0) {
|
|
totalDebit += entry.debit;
|
|
}
|
|
});
|
|
if (totalCredit <= 0 || totalDebit <= 0) {
|
|
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.'
|
|
);
|
|
throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate manual entries accounts existance on the storage.
|
|
* @param {number} tenantId -
|
|
* @param {IManualJournalDTO} manualJournalDTO -
|
|
*/
|
|
private async validateAccountsExistance(
|
|
tenantId: number,
|
|
manualJournalDTO: IManualJournalDTO
|
|
) {
|
|
const { Account } = this.tenancy.models(tenantId);
|
|
const manualAccountsIds = manualJournalDTO.entries.map((e) => e.accountId);
|
|
|
|
const accounts = await Account.query().whereIn('id', manualAccountsIds);
|
|
|
|
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,
|
|
});
|
|
throw new ServiceError(ERRORS.ACCCOUNTS_IDS_NOT_FOUND);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate manual journal number unique.
|
|
* @param {number} tenantId
|
|
* @param {IManualJournalDTO} manualJournalDTO
|
|
*/
|
|
private async validateManualJournalNoUnique(
|
|
tenantId: number,
|
|
journalNumber: string,
|
|
notId?: number
|
|
) {
|
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
|
const journals = await ManualJournal.query()
|
|
.where('journal_number', journalNumber)
|
|
.onBuild((builder) => {
|
|
if (notId) {
|
|
builder.whereNot('id', notId);
|
|
}
|
|
});
|
|
if (journals.length > 0) {
|
|
throw new ServiceError(ERRORS.JOURNAL_NUMBER_EXISTS);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate accounts with contact type.
|
|
* @param {number} tenantId
|
|
* @param {IManualJournalDTO} manualJournalDTO
|
|
* @param {string} accountBySlug
|
|
* @param {string} contactType
|
|
*/
|
|
private async validateAccountWithContactType(
|
|
tenantId: number,
|
|
entriesDTO: IManualJournalEntry[],
|
|
accountBySlug: string,
|
|
contactType: string
|
|
): Promise<void | ServiceError> {
|
|
const { Account } = this.tenancy.models(tenantId);
|
|
const { contactRepository } = this.tenancy.repositories(tenantId);
|
|
|
|
// Retrieve account meta by the given account slug.
|
|
const account = await Account.query().findOne('slug', accountBySlug);
|
|
|
|
// Retrieve all stored contacts on the storage from contacts entries.
|
|
const storedContacts = await contactRepository.findWhereIn(
|
|
'id',
|
|
entriesDTO
|
|
.filter((entry) => entry.contactId)
|
|
.map((entry) => entry.contactId)
|
|
);
|
|
// Converts the stored contacts to map with id as key and entry as value.
|
|
const storedContactsMap = new Map(
|
|
storedContacts.map((contact) => [contact.id, contact])
|
|
);
|
|
|
|
// Filter all entries of the given account.
|
|
const accountEntries = entriesDTO.filter(
|
|
(entry) => entry.accountId === account.id
|
|
);
|
|
// Can't continue if there is no entry that associate to the given account.
|
|
if (accountEntries.length === 0) {
|
|
return;
|
|
}
|
|
// Filter entries that have no contact type or not equal the valid type.
|
|
const entriesNoContact = accountEntries.filter((entry) => {
|
|
const contact = storedContactsMap.get(entry.contactId);
|
|
return !contact || contact.contactService !== contactType;
|
|
});
|
|
// Throw error in case one of entries that has invalid contact type.
|
|
if (entriesNoContact.length > 0) {
|
|
const indexes = entriesNoContact.map((e) => e.index);
|
|
|
|
return new ServiceError(ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, '', {
|
|
accountSlug: accountBySlug,
|
|
contactType,
|
|
indexes,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dynamic validates accounts with contacts.
|
|
* @param {number} tenantId
|
|
* @param {IManualJournalDTO} manualJournalDTO
|
|
*/
|
|
private async dynamicValidateAccountsWithContactType(
|
|
tenantId: number,
|
|
entriesDTO: IManualJournalEntry[]
|
|
): Promise<any> {
|
|
return Promise.all([
|
|
this.validateAccountWithContactType(
|
|
tenantId,
|
|
entriesDTO,
|
|
'accounts-receivable',
|
|
'customer'
|
|
),
|
|
this.validateAccountWithContactType(
|
|
tenantId,
|
|
entriesDTO,
|
|
'accounts-payable',
|
|
'vendor'
|
|
),
|
|
]).then((results) => {
|
|
const metadataErrors = results
|
|
.filter((result) => result instanceof ServiceError)
|
|
.map((result: ServiceError) => result.payload);
|
|
|
|
if (metadataErrors.length > 0) {
|
|
throw new ServiceError(
|
|
ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT,
|
|
'',
|
|
metadataErrors
|
|
);
|
|
}
|
|
|
|
return results;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validate entries contacts existance.
|
|
* @param {number} tenantId -
|
|
* @param {IManualJournalDTO} manualJournalDTO
|
|
*/
|
|
private async validateContactsExistance(
|
|
tenantId: number,
|
|
manualJournalDTO: IManualJournalDTO
|
|
) {
|
|
const { contactRepository } = this.tenancy.repositories(tenantId);
|
|
|
|
// Filters the entries that have contact only.
|
|
const entriesContactPairs = manualJournalDTO.entries.filter(
|
|
(entry) => entry.contactId
|
|
);
|
|
|
|
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.findWhereIn(
|
|
'id',
|
|
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.
|
|
if (!storedContact) {
|
|
notFoundContactsIds.push(storedContact);
|
|
}
|
|
});
|
|
if (notFoundContactsIds.length > 0) {
|
|
throw new ServiceError(ERRORS.CONTACTS_NOT_FOUND, '', {
|
|
contactsIds: notFoundContactsIds,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the next journal number.
|
|
*/
|
|
getNextJournalNumber(tenantId: number): string {
|
|
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
|
tenantId,
|
|
'manual_journals'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Increment the manual journal number.
|
|
* @param {number} tenantId
|
|
*/
|
|
incrementNextJournalNumber(tenantId: number) {
|
|
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
|
tenantId,
|
|
'manual_journals'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Validates the manual journal number require.
|
|
* @param {string} journalNumber
|
|
*/
|
|
private validateJournalNoRequire(journalNumber: string) {
|
|
if (!journalNumber) {
|
|
throw new ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transform the new manual journal DTO to upsert graph operation.
|
|
* @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO.
|
|
* @param {ISystemUser} authorizedUser
|
|
*/
|
|
private transformNewDTOToModel(
|
|
tenantId,
|
|
manualJournalDTO: IManualJournalDTO,
|
|
authorizedUser: ISystemUser
|
|
) {
|
|
const amount = sumBy(manualJournalDTO.entries, 'credit') || 0;
|
|
const date = moment(manualJournalDTO.date).format('YYYY-MM-DD');
|
|
|
|
// Retrieve the next manual journal number.
|
|
const autoNextNumber = this.getNextJournalNumber(tenantId);
|
|
|
|
const journalNumber = manualJournalDTO.journalNumber || autoNextNumber;
|
|
|
|
// Settings tenant service.
|
|
const settings = this.tenancy.settings(tenantId);
|
|
const currencyCode = settings.get({
|
|
group: 'organization',
|
|
key: 'base_currency',
|
|
});
|
|
// Validate manual journal number require.
|
|
this.validateJournalNoRequire(journalNumber);
|
|
|
|
return {
|
|
...omit(manualJournalDTO, ['publish']),
|
|
...(manualJournalDTO.publish
|
|
? { publishedAt: moment().toMySqlDateTime() }
|
|
: {}),
|
|
amount,
|
|
currencyCode,
|
|
date,
|
|
journalNumber,
|
|
userId: authorizedUser.id,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Transform the edit manual journal DTO to upsert graph operation.
|
|
* @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO.
|
|
* @param {IManualJournal} oldManualJournal
|
|
*/
|
|
private transformEditDTOToModel(
|
|
manualJournalDTO: IManualJournalDTO,
|
|
oldManualJournal: IManualJournal
|
|
) {
|
|
const amount = sumBy(manualJournalDTO.entries, 'credit') || 0;
|
|
const date = moment(manualJournalDTO.date).format('YYYY-MM-DD');
|
|
|
|
return {
|
|
id: oldManualJournal.id,
|
|
...omit(manualJournalDTO, ['publish']),
|
|
...(manualJournalDTO.publish && !oldManualJournal.publishedAt
|
|
? { publishedAt: moment().toMySqlDateTime() }
|
|
: {}),
|
|
amount,
|
|
date,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Make journal entries.
|
|
* @param {number} tenantId
|
|
* @param {IManualJournalDTO} manualJournalDTO
|
|
* @param {ISystemUser} authorizedUser
|
|
*/
|
|
public async makeJournalEntries(
|
|
tenantId: number,
|
|
manualJournalDTO: IManualJournalDTO,
|
|
authorizedUser: ISystemUser
|
|
): Promise<{ manualJournal: IManualJournal }> {
|
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
|
|
|
// Transformes the next DTO to model.
|
|
const manualJournalObj = this.transformNewDTOToModel(
|
|
tenantId,
|
|
manualJournalDTO,
|
|
authorizedUser
|
|
);
|
|
// Validate the total credit should equals debit.
|
|
this.valdiateCreditDebitTotalEquals(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,
|
|
manualJournalObj.journalNumber
|
|
);
|
|
// Validate accounts with contact type from the given config.
|
|
await this.dynamicValidateAccountsWithContactType(
|
|
tenantId,
|
|
manualJournalDTO.entries
|
|
);
|
|
this.logger.info(
|
|
'[manual_journal] trying to save manual journal to the storage.',
|
|
{ tenantId, manualJournalDTO }
|
|
);
|
|
// Upsert the manual journal object.
|
|
const manualJournal = await ManualJournal.query().upsertGraph({
|
|
...manualJournalObj,
|
|
});
|
|
// Triggers `onManualJournalCreated` event.
|
|
this.eventDispatcher.dispatch(events.manualJournals.onCreated, {
|
|
tenantId,
|
|
manualJournal,
|
|
manualJournalId: manualJournal.id,
|
|
});
|
|
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
|
|
*/
|
|
public async editJournalEntries(
|
|
tenantId: number,
|
|
manualJournalId: number,
|
|
manualJournalDTO: IManualJournalDTO,
|
|
authorizedUser: ISystemUser
|
|
): Promise<{
|
|
manualJournal: IManualJournal;
|
|
oldManualJournal: IManualJournal;
|
|
}> {
|
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
|
|
|
// Validates the manual journal existance on the storage.
|
|
const oldManualJournal = await this.getManualJournalOrThrowError(
|
|
tenantId,
|
|
manualJournalId
|
|
);
|
|
// Transform manual journal DTO to model.
|
|
const manualJournalObj = this.transformEditDTOToModel(
|
|
manualJournalDTO,
|
|
oldManualJournal
|
|
);
|
|
// 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);
|
|
|
|
// Validates the manual journal number uniquiness.
|
|
if (manualJournalDTO.journalNumber) {
|
|
await this.validateManualJournalNoUnique(
|
|
tenantId,
|
|
manualJournalDTO.journalNumber,
|
|
manualJournalId
|
|
);
|
|
}
|
|
// Validate accounts with contact type from the given config.
|
|
await this.dynamicValidateAccountsWithContactType(
|
|
tenantId,
|
|
manualJournalDTO.entries
|
|
);
|
|
|
|
await ManualJournal.query().upsertGraph({
|
|
...manualJournalObj,
|
|
});
|
|
// Retrieve the given manual journal with associated entries after modifications.
|
|
const manualJournal = await ManualJournal.query()
|
|
.findById(manualJournalId)
|
|
.withGraphFetched('entries');
|
|
|
|
// Triggers `onManualJournalEdited` event.
|
|
this.eventDispatcher.dispatch(events.manualJournals.onEdited, {
|
|
tenantId,
|
|
manualJournal,
|
|
oldManualJournal,
|
|
});
|
|
return { manualJournal, oldManualJournal };
|
|
}
|
|
|
|
/**
|
|
* Deletes the given manual journal
|
|
* @param {number} tenantId
|
|
* @param {number} manualJournalId
|
|
* @return {Promise<void>}
|
|
*/
|
|
public async deleteManualJournal(
|
|
tenantId: number,
|
|
manualJournalId: number
|
|
): Promise<{
|
|
oldManualJournal: IManualJournal;
|
|
}> {
|
|
const { ManualJournal, ManualJournalEntry } = this.tenancy.models(tenantId);
|
|
|
|
// Validate the manual journal exists on the storage.
|
|
const oldManualJournal = await this.getManualJournalOrThrowError(
|
|
tenantId,
|
|
manualJournalId
|
|
);
|
|
this.logger.info('[manual_journal] trying to delete the manual journal.', {
|
|
tenantId,
|
|
manualJournalId,
|
|
});
|
|
// Deletes the manual journal entries.
|
|
await ManualJournalEntry.query()
|
|
.where('manual_journal_id', manualJournalId)
|
|
.delete();
|
|
|
|
// Deletes the manual journal transaction.
|
|
await ManualJournal.query().findById(manualJournalId).delete();
|
|
|
|
// Triggers `onManualJournalDeleted` event.
|
|
this.eventDispatcher.dispatch(events.manualJournals.onDeleted, {
|
|
tenantId,
|
|
manualJournalId,
|
|
oldManualJournal,
|
|
});
|
|
this.logger.info(
|
|
'[manual_journal] the given manual journal deleted successfully.',
|
|
{ tenantId, manualJournalId }
|
|
);
|
|
|
|
return { oldManualJournal };
|
|
}
|
|
|
|
/**
|
|
* Deletes the given manual journals.
|
|
* @param {number} tenantId
|
|
* @param {number[]} manualJournalsIds
|
|
* @return {Promise<void>}
|
|
*/
|
|
public async deleteManualJournals(
|
|
tenantId: number,
|
|
manualJournalsIds: number[]
|
|
): Promise<{
|
|
oldManualJournals: IManualJournal[];
|
|
}> {
|
|
const { ManualJournal, ManualJournalEntry } = this.tenancy.models(tenantId);
|
|
|
|
// Validate the manual journals exist on the storage.
|
|
const oldManualJournals = await this.getManualJournalsOrThrowError(
|
|
tenantId,
|
|
manualJournalsIds
|
|
);
|
|
this.logger.info('[manual_journal] trying to delete the manual journals.', {
|
|
tenantId,
|
|
manualJournalsIds,
|
|
});
|
|
// Deletes the manual journal entries.
|
|
await ManualJournalEntry.query()
|
|
.whereIn('manual_journal_id', manualJournalsIds)
|
|
.delete();
|
|
|
|
// Deletes the manual journal transaction.
|
|
await ManualJournal.query().whereIn('id', manualJournalsIds).delete();
|
|
|
|
// Triggers `onManualJournalDeletedBulk` event.
|
|
this.eventDispatcher.dispatch(events.manualJournals.onDeletedBulk, {
|
|
tenantId,
|
|
manualJournalsIds,
|
|
});
|
|
this.logger.info(
|
|
'[manual_journal] the given manual journals deleted successfully.',
|
|
{ tenantId, manualJournalsIds }
|
|
);
|
|
return { oldManualJournals };
|
|
}
|
|
|
|
/**
|
|
* Publish the given manual journals.
|
|
* @param {number} tenantId
|
|
* @param {number[]} manualJournalsIds
|
|
*/
|
|
public async publishManualJournals(
|
|
tenantId: number,
|
|
manualJournalsIds: number[]
|
|
): Promise<{
|
|
meta: {
|
|
alreadyPublished: number;
|
|
published: number;
|
|
total: number;
|
|
};
|
|
}> {
|
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
|
|
|
// Retrieve manual journals or throw service error.
|
|
const oldManualJournals = await this.getManualJournalsOrThrowError(
|
|
tenantId,
|
|
manualJournalsIds
|
|
);
|
|
// Filters the not published journals.
|
|
const notPublishedJournals = this.getNonePublishedManualJournals(
|
|
oldManualJournals
|
|
);
|
|
// Filters the published journals.
|
|
const publishedJournals = this.getPublishedManualJournals(
|
|
oldManualJournals
|
|
);
|
|
// Mappes the not-published journals to get id.
|
|
const notPublishedJournalsIds = map(notPublishedJournals, 'id');
|
|
|
|
this.logger.info('[manual_journal] trying to publish the manual journal.', {
|
|
tenantId,
|
|
manualJournalsIds,
|
|
});
|
|
|
|
if (notPublishedJournals.length > 0) {
|
|
// Mark the given manual journals as published.
|
|
await ManualJournal.query().whereIn('id', notPublishedJournalsIds).patch({
|
|
publishedAt: moment().toMySqlDateTime(),
|
|
});
|
|
}
|
|
// Triggers `onManualJournalPublishedBulk` event.
|
|
this.eventDispatcher.dispatch(events.manualJournals.onPublishedBulk, {
|
|
tenantId,
|
|
manualJournalsIds,
|
|
oldManualJournals,
|
|
});
|
|
this.logger.info(
|
|
'[manual_journal] the given manula journal published successfully.',
|
|
{ tenantId, manualJournalsIds }
|
|
);
|
|
|
|
return {
|
|
meta: {
|
|
alreadyPublished: publishedJournals.length,
|
|
published: notPublishedJournals.length,
|
|
total: oldManualJournals.length,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validates expenses is not already published before.
|
|
* @param {IManualJournal} manualJournal
|
|
*/
|
|
private validateManualJournalIsNotPublished(manualJournal: IManualJournal) {
|
|
if (manualJournal.publishedAt) {
|
|
throw new ServiceError(ERRORS.MANUAL_JOURNAL_ALREADY_PUBLISHED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filters the not published manual jorunals.
|
|
* @param {IManualJournal[]} manualJournal - Manual journal.
|
|
* @return {IManualJournal[]}
|
|
*/
|
|
public getNonePublishedManualJournals(
|
|
manualJournals: IManualJournal[]
|
|
): IManualJournal[] {
|
|
return manualJournals.filter((manualJournal) => !manualJournal.publishedAt);
|
|
}
|
|
|
|
/**
|
|
* Filters the published manual journals.
|
|
* @param {IManualJournal[]} manualJournal - Manual journal.
|
|
* @return {IManualJournal[]}
|
|
*/
|
|
public getPublishedManualJournals(
|
|
manualJournals: IManualJournal[]
|
|
): IManualJournal[] {
|
|
return manualJournals.filter((expense) => expense.publishedAt);
|
|
}
|
|
|
|
/**
|
|
* Publish the given manual journal.
|
|
* @param {number} tenantId - Tenant id.
|
|
* @param {number} manualJournalId - Manual journal id.
|
|
*/
|
|
public async publishManualJournal(
|
|
tenantId: number,
|
|
manualJournalId: number
|
|
): Promise<void> {
|
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
|
|
|
const oldManualJournal = await this.getManualJournalOrThrowError(
|
|
tenantId,
|
|
manualJournalId
|
|
);
|
|
this.logger.info('[manual_journal] trying to publish the manual journal.', {
|
|
tenantId,
|
|
manualJournalId,
|
|
});
|
|
this.validateManualJournalIsNotPublished(oldManualJournal);
|
|
|
|
// Mark the given manual journal as published.
|
|
await ManualJournal.query().findById(manualJournalId).patch({
|
|
publishedAt: moment().toMySqlDateTime(),
|
|
});
|
|
// Retrieve the manual journal with enrties after modification.
|
|
const manualJournal = await ManualJournal.query()
|
|
.findById(manualJournalId)
|
|
.withGraphFetched('entries');
|
|
|
|
// Triggers `onManualJournalPublishedBulk` event.
|
|
this.eventDispatcher.dispatch(events.manualJournals.onPublished, {
|
|
tenantId,
|
|
manualJournal,
|
|
manualJournalId,
|
|
oldManualJournal,
|
|
});
|
|
this.logger.info(
|
|
'[manual_journal] the given manula journal published successfully.',
|
|
{ tenantId, manualJournalId }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Retrieve manual journals datatable list.
|
|
* @param {number} tenantId
|
|
* @param {IManualJournalsFilter} filter
|
|
*/
|
|
public async getManualJournals(
|
|
tenantId: number,
|
|
filter: IManualJournalsFilter
|
|
): Promise<{
|
|
manualJournals: IManualJournal;
|
|
pagination: IPaginationMeta;
|
|
filterMeta: IFilterMeta;
|
|
}> {
|
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
|
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);
|
|
|
|
return {
|
|
manualJournals: results,
|
|
pagination,
|
|
filterMeta: dynamicList.getResponseMeta(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retrieve manual journal details with assocaited journal transactions.
|
|
* @param {number} tenantId
|
|
* @param {number} manualJournalId
|
|
*/
|
|
public async getManualJournal(tenantId: number, manualJournalId: number) {
|
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
|
|
|
await this.getManualJournalOrThrowError(tenantId, manualJournalId);
|
|
|
|
this.logger.info(
|
|
'[manual_journals] trying to get specific manual journal.',
|
|
{ tenantId, manualJournalId }
|
|
);
|
|
const manualJournal = await ManualJournal.query()
|
|
.findById(manualJournalId)
|
|
.withGraphFetched('entries.account')
|
|
.withGraphFetched('entries.contact')
|
|
.withGraphFetched('transactions')
|
|
.withGraphFetched('media');
|
|
|
|
return manualJournal;
|
|
}
|
|
|
|
/**
|
|
* Reverts the manual journal journal entries.
|
|
* @param {number} tenantId
|
|
* @param {number|number[]} manualJournalId
|
|
* @return {Promise<void>}
|
|
*/
|
|
public async revertJournalEntries(
|
|
tenantId: number,
|
|
manualJournalId: number | number[]
|
|
): Promise<void> {
|
|
this.logger.info('[manual_journal] trying to revert journal entries.', {
|
|
tenantId,
|
|
manualJournalId,
|
|
});
|
|
return this.journalService.revertJournalTransactions(
|
|
tenantId,
|
|
manualJournalId,
|
|
'Journal'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Write manual journal entries.
|
|
* @param {number} tenantId
|
|
* @param {number} manualJournalId
|
|
* @param {IManualJournal|null} manualJournalObj
|
|
* @param {boolean} override
|
|
*/
|
|
public async writeJournalEntries(
|
|
tenantId: number,
|
|
manualJorunal: IManualJournal | IManualJournal[],
|
|
override: Boolean = false
|
|
) {
|
|
const journal = new JournalPoster(tenantId);
|
|
const journalCommands = new JournalCommands(journal);
|
|
|
|
const manualJournals = Array.isArray(manualJorunal)
|
|
? manualJorunal
|
|
: [manualJorunal];
|
|
const manualJournalsIds = map(manualJournals, 'id');
|
|
|
|
if (override) {
|
|
this.logger.info('[manual_journal] trying to revert journal entries.', {
|
|
tenantId,
|
|
manualJorunal,
|
|
});
|
|
await journalCommands.revertJournalEntries(manualJournalsIds, 'Journal');
|
|
}
|
|
manualJournals.forEach((manualJournal) => {
|
|
journalCommands.manualJournal(manualJournal);
|
|
});
|
|
this.logger.info('[manual_journal] trying to save journal entries.', {
|
|
tenantId,
|
|
manualJorunal,
|
|
});
|
|
await Promise.all([
|
|
journal.saveBalance(),
|
|
journal.deleteEntries(),
|
|
journal.saveEntries(),
|
|
journal.saveContactsBalance(),
|
|
]);
|
|
this.logger.info(
|
|
'[manual_journal] the journal entries saved successfully.',
|
|
{ tenantId, manualJournalId: manualJorunal.id }
|
|
);
|
|
}
|
|
}
|