mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
add server to monorepo.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
|
||||
|
||||
@Service()
|
||||
export class AutoIncrementManualJournal {
|
||||
@Inject()
|
||||
private autoIncrementOrdersService: AutoIncrementOrdersService;
|
||||
|
||||
public autoIncrementEnabled = (tenantId: number) => {
|
||||
return this.autoIncrementOrdersService.autoIncrementEnabled(
|
||||
tenantId,
|
||||
'manual_journals'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the next journal number.
|
||||
*/
|
||||
public getNextJournalNumber = (tenantId: number): string => {
|
||||
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
||||
tenantId,
|
||||
'manual_journals'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Increment the manual journal number.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
public incrementNextJournalNumber = (tenantId: number) => {
|
||||
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
||||
tenantId,
|
||||
'manual_journals'
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
import { difference, sumBy, omit, map } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
IManualJournalDTO,
|
||||
IManualJournalEntry,
|
||||
IManualJournal,
|
||||
IManualJournalEntryDTO,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ERRORS } from './constants';
|
||||
import { AutoIncrementManualJournal } from './AutoIncrementManualJournal';
|
||||
|
||||
@Service()
|
||||
export class CommandManualJournalValidators {
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
private autoIncrement: AutoIncrementManualJournal;
|
||||
|
||||
/**
|
||||
* Validate manual journal credit and debit should be equal.
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*/
|
||||
public 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) {
|
||||
throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL_ZERO);
|
||||
}
|
||||
if (totalCredit !== totalDebit) {
|
||||
throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate manual entries accounts existance on the storage.
|
||||
* @param {number} tenantId -
|
||||
* @param {IManualJournalDTO} manualJournalDTO -
|
||||
*/
|
||||
public 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) {
|
||||
throw new ServiceError(ERRORS.ACCCOUNTS_IDS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate manual journal number unique.
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*/
|
||||
public 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
|
||||
*/
|
||||
public 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
|
||||
*/
|
||||
public 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
|
||||
*/
|
||||
public 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates expenses is not already published before.
|
||||
* @param {IManualJournal} manualJournal
|
||||
*/
|
||||
public validateManualJournalIsNotPublished(manualJournal: IManualJournal) {
|
||||
if (manualJournal.publishedAt) {
|
||||
throw new ServiceError(ERRORS.MANUAL_JOURNAL_ALREADY_PUBLISHED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the manual journal number require.
|
||||
* @param {string} journalNumber
|
||||
*/
|
||||
public validateJournalNoRequireWhenAutoNotEnabled = (
|
||||
tenantId: number,
|
||||
journalNumber: string
|
||||
) => {
|
||||
// Retrieve the next manual journal number.
|
||||
const autoIncrmenetEnabled =
|
||||
this.autoIncrement.autoIncrementEnabled(tenantId);
|
||||
|
||||
if (!journalNumber || !autoIncrmenetEnabled) {
|
||||
throw new ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*/
|
||||
public validateJournalCurrencyWithAccountsCurrency = async (
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
baseCurrency: string,
|
||||
) => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const accountsIds = manualJournalDTO.entries.map((e) => e.accountId);
|
||||
const accounts = await Account.query().whereIn('id', accountsIds);
|
||||
|
||||
// Filters the accounts that has no base currency or DTO currency.
|
||||
const notSupportedCurrency = accounts.filter((account) => {
|
||||
if (
|
||||
account.currencyCode === baseCurrency ||
|
||||
account.currencyCode === manualJournalDTO.currencyCode
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (notSupportedCurrency.length > 0) {
|
||||
throw new ServiceError(
|
||||
ERRORS.COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { sumBy, omit } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IManualJournalDTO,
|
||||
ISystemUser,
|
||||
IManualJournal,
|
||||
IManualJournalEventCreatedPayload,
|
||||
IManualJournalCreatingPayload,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import events from '@/subscribers/events';
|
||||
import { Tenant, TenantMetadata } from '@/system/models';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { CommandManualJournalValidators } from './CommandManualJournalValidators';
|
||||
import { AutoIncrementManualJournal } from './AutoIncrementManualJournal';
|
||||
import { ManualJournalBranchesDTOTransformer } from '@/services/Branches/Integrations/ManualJournals/ManualJournalDTOTransformer';
|
||||
@Service()
|
||||
export class CreateManualJournalService {
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validator: CommandManualJournalValidators;
|
||||
|
||||
@Inject()
|
||||
private autoIncrement: AutoIncrementManualJournal;
|
||||
|
||||
@Inject()
|
||||
private branchesDTOTransformer: ManualJournalBranchesDTOTransformer;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
baseCurrency: string
|
||||
) {
|
||||
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.autoIncrement.getNextJournalNumber(tenantId);
|
||||
|
||||
// The manual or auto-increment journal number.
|
||||
const journalNumber = manualJournalDTO.journalNumber || autoNextNumber;
|
||||
|
||||
const initialDTO = {
|
||||
...omit(manualJournalDTO, ['publish']),
|
||||
...(manualJournalDTO.publish
|
||||
? { publishedAt: moment().toMySqlDateTime() }
|
||||
: {}),
|
||||
amount,
|
||||
currencyCode: manualJournalDTO.currencyCode || baseCurrency,
|
||||
exchangeRate: manualJournalDTO.exchangeRate || 1,
|
||||
date,
|
||||
journalNumber,
|
||||
userId: authorizedUser.id,
|
||||
};
|
||||
return R.compose(
|
||||
// Omits the `branchId` from entries if multiply branches feature not active.
|
||||
this.branchesDTOTransformer.transformDTO(tenantId)
|
||||
)(
|
||||
initialDTO
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize the manual journal creating.
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
private authorize = async (
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
authorizedUser: ISystemUser,
|
||||
baseCurrency: string
|
||||
) => {
|
||||
// Validate the total credit should equals debit.
|
||||
this.validator.valdiateCreditDebitTotalEquals(manualJournalDTO);
|
||||
|
||||
// Validate the contacts existance.
|
||||
await this.validator.validateContactsExistance(tenantId, manualJournalDTO);
|
||||
|
||||
// Validate entries accounts existance.
|
||||
await this.validator.validateAccountsExistance(tenantId, manualJournalDTO);
|
||||
|
||||
// Validate manual journal number require when auto-increment not enabled.
|
||||
this.validator.validateJournalNoRequireWhenAutoNotEnabled(
|
||||
tenantId,
|
||||
manualJournalDTO.journalNumber
|
||||
);
|
||||
// Validate manual journal uniquiness on the storage.
|
||||
if (manualJournalDTO.journalNumber) {
|
||||
await this.validator.validateManualJournalNoUnique(
|
||||
tenantId,
|
||||
manualJournalDTO.journalNumber
|
||||
);
|
||||
}
|
||||
// Validate accounts with contact type from the given config.
|
||||
await this.validator.dynamicValidateAccountsWithContactType(
|
||||
tenantId,
|
||||
manualJournalDTO.entries
|
||||
);
|
||||
// Validates the accounts currency with journal currency.
|
||||
await this.validator.validateJournalCurrencyWithAccountsCurrency(
|
||||
tenantId,
|
||||
manualJournalDTO,
|
||||
baseCurrency
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make journal entries.
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
public makeJournalEntries = async (
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<{ manualJournal: IManualJournal }> => {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves the tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Authorize manual journal creating.
|
||||
await this.authorize(
|
||||
tenantId,
|
||||
manualJournalDTO,
|
||||
authorizedUser,
|
||||
tenantMeta.baseCurrency
|
||||
);
|
||||
// Transformes the next DTO to model.
|
||||
const manualJournalObj = this.transformNewDTOToModel(
|
||||
tenantId,
|
||||
manualJournalDTO,
|
||||
authorizedUser,
|
||||
tenantMeta.baseCurrency
|
||||
);
|
||||
// Creates a manual journal transactions with associated transactions
|
||||
// under unit-of-work envirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onManualJournalCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.manualJournals.onCreating, {
|
||||
tenantId,
|
||||
manualJournalDTO,
|
||||
trx,
|
||||
} as IManualJournalCreatingPayload);
|
||||
|
||||
// Upsert the manual journal object.
|
||||
const manualJournal = await ManualJournal.query(trx).upsertGraph({
|
||||
...manualJournalObj,
|
||||
});
|
||||
// Triggers `onManualJournalCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.manualJournals.onCreated, {
|
||||
tenantId,
|
||||
manualJournal,
|
||||
manualJournalId: manualJournal.id,
|
||||
trx,
|
||||
} as IManualJournalEventCreatedPayload);
|
||||
|
||||
return { manualJournal };
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IManualJournal,
|
||||
IManualJournalEventDeletedPayload,
|
||||
IManualJournalDeletingPayload,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import events from '@/subscribers/events';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
@Service()
|
||||
export class DeleteManualJournal {
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Deletes the given manual journal
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public deleteManualJournal = async (
|
||||
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 ManualJournal.query()
|
||||
.findById(manualJournalId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Deletes the manual journal with associated transactions under unit-of-work envirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onManualJournalDeleting` event.
|
||||
await this.eventPublisher.emitAsync(events.manualJournals.onDeleting, {
|
||||
tenantId,
|
||||
oldManualJournal,
|
||||
trx,
|
||||
} as IManualJournalDeletingPayload);
|
||||
|
||||
// Deletes the manual journal entries.
|
||||
await ManualJournalEntry.query(trx)
|
||||
.where('manualJournalId', manualJournalId)
|
||||
.delete();
|
||||
|
||||
// Deletes the manual journal transaction.
|
||||
await ManualJournal.query(trx).findById(manualJournalId).delete();
|
||||
|
||||
// Triggers `onManualJournalDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.manualJournals.onDeleted, {
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
oldManualJournal,
|
||||
trx,
|
||||
} as IManualJournalEventDeletedPayload);
|
||||
|
||||
return { oldManualJournal };
|
||||
});
|
||||
};
|
||||
}
|
||||
152
packages/server/src/services/ManualJournals/EditManualJournal.ts
Normal file
152
packages/server/src/services/ManualJournals/EditManualJournal.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
IManualJournalDTO,
|
||||
ISystemUser,
|
||||
IManualJournal,
|
||||
IManualJournalEventEditedPayload,
|
||||
IManualJournalEditingPayload,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import events from '@/subscribers/events';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { CommandManualJournalValidators } from './CommandManualJournalValidators';
|
||||
|
||||
@Service()
|
||||
export class EditManualJournal {
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validator: CommandManualJournalValidators;
|
||||
|
||||
/**
|
||||
* Authorize the manual journal editing.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
*/
|
||||
private authorize = async (
|
||||
tenantId: number,
|
||||
manualJournalId: number,
|
||||
manualJournalDTO: IManualJournalDTO
|
||||
) => {
|
||||
// Validates the total credit and debit to be equals.
|
||||
this.validator.valdiateCreditDebitTotalEquals(manualJournalDTO);
|
||||
|
||||
// Validate the contacts existance.
|
||||
await this.validator.validateContactsExistance(tenantId, manualJournalDTO);
|
||||
|
||||
// Validates entries accounts existance.
|
||||
await this.validator.validateAccountsExistance(tenantId, manualJournalDTO);
|
||||
|
||||
// Validates the manual journal number uniquiness.
|
||||
if (manualJournalDTO.journalNumber) {
|
||||
await this.validator.validateManualJournalNoUnique(
|
||||
tenantId,
|
||||
manualJournalDTO.journalNumber,
|
||||
manualJournalId
|
||||
);
|
||||
}
|
||||
// Validate accounts with contact type from the given config.
|
||||
await this.validator.dynamicValidateAccountsWithContactType(
|
||||
tenantId,
|
||||
manualJournalDTO.entries
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 ManualJournal.query()
|
||||
.findById(manualJournalId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Authorize manual journal editing.
|
||||
await this.authorize(tenantId, manualJournalId, manualJournalDTO);
|
||||
|
||||
// Transform manual journal DTO to model.
|
||||
const manualJournalObj = this.transformEditDTOToModel(
|
||||
manualJournalDTO,
|
||||
oldManualJournal
|
||||
);
|
||||
// Edits the manual journal transactions with associated transactions
|
||||
// under unit-of-work envirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onManualJournalEditing` event.
|
||||
await this.eventPublisher.emitAsync(events.manualJournals.onEditing, {
|
||||
tenantId,
|
||||
manualJournalDTO,
|
||||
oldManualJournal,
|
||||
trx,
|
||||
} as IManualJournalEditingPayload);
|
||||
|
||||
// Upserts the manual journal graph to the storage.
|
||||
await ManualJournal.query(trx).upsertGraph({
|
||||
...manualJournalObj,
|
||||
});
|
||||
// Retrieve the given manual journal with associated entries after modifications.
|
||||
const manualJournal = await ManualJournal.query(trx)
|
||||
.findById(manualJournalId)
|
||||
.withGraphFetched('entries');
|
||||
|
||||
// Triggers `onManualJournalEdited` event.
|
||||
await this.eventPublisher.emitAsync(events.manualJournals.onEdited, {
|
||||
tenantId,
|
||||
manualJournal,
|
||||
oldManualJournal,
|
||||
trx,
|
||||
} as IManualJournalEventEditedPayload);
|
||||
|
||||
return { manualJournal, oldManualJournal };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ManualJournalTransfromer } from './ManualJournalTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
@Service()
|
||||
export class GetManualJournal {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve manual journal details with assocaited journal transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
*/
|
||||
public getManualJournal = async (
|
||||
tenantId: number,
|
||||
manualJournalId: number
|
||||
) => {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
|
||||
const manualJournal = await ManualJournal.query()
|
||||
.findById(manualJournalId)
|
||||
.withGraphFetched('entries.account')
|
||||
.withGraphFetched('entries.contact')
|
||||
.withGraphFetched('entries.branch')
|
||||
.withGraphFetched('transactions')
|
||||
.withGraphFetched('media')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
manualJournal,
|
||||
new ManualJournalTransfromer()
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IManualJournalsFilter,
|
||||
IManualJournal,
|
||||
IPaginationMeta,
|
||||
IFilterMeta,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { ManualJournalTransfromer } from './ManualJournalTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
@Service()
|
||||
export class GetManualJournals {
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Parses filter DTO of the manual journals list.
|
||||
* @param filterDTO
|
||||
*/
|
||||
private parseListFilterDTO = (filterDTO) => {
|
||||
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve manual journals datatable list.
|
||||
* @param {number} tenantId -
|
||||
* @param {IManualJournalsFilter} filter -
|
||||
*/
|
||||
public getManualJournals = async (
|
||||
tenantId: number,
|
||||
filterDTO: IManualJournalsFilter
|
||||
): Promise<{
|
||||
manualJournals: IManualJournal;
|
||||
pagination: IPaginationMeta;
|
||||
filterMeta: IFilterMeta;
|
||||
}> => {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
|
||||
// Parses filter DTO.
|
||||
const filter = this.parseListFilterDTO(filterDTO);
|
||||
|
||||
// Dynamic service.
|
||||
const dynamicService = await this.dynamicListService.dynamicList(
|
||||
tenantId,
|
||||
ManualJournal,
|
||||
filter
|
||||
);
|
||||
const { results, pagination } = await ManualJournal.query()
|
||||
.onBuild((builder) => {
|
||||
dynamicService.buildQuery()(builder);
|
||||
builder.withGraphFetched('entries.account');
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
// Transformes the manual journals models to POJO.
|
||||
const manualJournals = await this.transformer.transform(
|
||||
tenantId,
|
||||
results,
|
||||
new ManualJournalTransfromer()
|
||||
);
|
||||
|
||||
return {
|
||||
manualJournals,
|
||||
pagination,
|
||||
filterMeta: dynamicService.getResponseMeta(),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IManualJournal,
|
||||
IManualJournalEntry,
|
||||
IAccount,
|
||||
ILedgerEntry,
|
||||
} from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { LedgerRevert } from '@/services/Accounting/LedgerStorageRevert';
|
||||
|
||||
@Service()
|
||||
export class ManualJournalGLEntries {
|
||||
@Inject()
|
||||
ledgerStorage: LedgerStorageService;
|
||||
|
||||
@Inject()
|
||||
ledgerRevert: LedgerRevert;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Create manual journal GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public createManualJournalGLEntries = async (
|
||||
tenantId: number,
|
||||
manualJournalId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves the given manual journal with associated entries.
|
||||
const manualJournal = await ManualJournal.query(trx)
|
||||
.findById(manualJournalId)
|
||||
.withGraphFetched('entries.account');
|
||||
|
||||
// Retrieves the ledger entries of the given manual journal.
|
||||
const ledger = this.getManualJournalGLedger(manualJournal);
|
||||
|
||||
// Commits the given ledger on the storage.
|
||||
await this.ledgerStorage.commit(tenantId, ledger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits manual journal GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public editManualJournalGLEntries = async (
|
||||
tenantId: number,
|
||||
manualJournalId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
// Reverts the manual journal GL entries.
|
||||
await this.revertManualJournalGLEntries(tenantId, manualJournalId, trx);
|
||||
|
||||
// Write the manual journal GL entries.
|
||||
await this.createManualJournalGLEntries(tenantId, manualJournalId, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the manual journal GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public revertManualJournalGLEntries = async (
|
||||
tenantId: number,
|
||||
manualJournalId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
return this.ledgerRevert.revertGLEntries(
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
'Journal',
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IManualJournal} manualJournal
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
private getManualJournalGLedger = (manualJournal: IManualJournal) => {
|
||||
const entries = this.getManualJournalGLEntries(manualJournal);
|
||||
|
||||
return new Ledger(entries);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IManualJournal} manualJournal
|
||||
* @returns {}
|
||||
*/
|
||||
private getManualJournalCommonEntry = (manualJournal: IManualJournal) => {
|
||||
return {
|
||||
transactionNumber: manualJournal.journalNumber,
|
||||
referenceNumber: manualJournal.reference,
|
||||
createdAt: manualJournal.createdAt,
|
||||
date: manualJournal.date,
|
||||
currencyCode: manualJournal.currencyCode,
|
||||
exchangeRate: manualJournal.exchangeRate,
|
||||
|
||||
transactionType: 'Journal',
|
||||
transactionId: manualJournal.id,
|
||||
|
||||
userId: manualJournal.userId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IManualJournal} manualJournal -
|
||||
* @param {IManualJournalEntry} entry -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getManualJournalEntry = R.curry(
|
||||
(
|
||||
manualJournal: IManualJournal,
|
||||
entry: IManualJournalEntry
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getManualJournalCommonEntry(manualJournal);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: entry.debit,
|
||||
credit: entry.credit,
|
||||
accountId: entry.accountId,
|
||||
|
||||
contactId: entry.contactId,
|
||||
note: entry.note,
|
||||
|
||||
index: entry.index,
|
||||
accountNormal: entry.account.accountNormal,
|
||||
|
||||
branchId: entry.branchId,
|
||||
projectId: entry.projectId,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IManualJournal} manualJournal
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
private getManualJournalGLEntries = (
|
||||
manualJournal: IManualJournal
|
||||
): ILedgerEntry[] => {
|
||||
const transformEntry = this.getManualJournalEntry(manualJournal);
|
||||
|
||||
return manualJournal.entries.map(transformEntry).flat();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { EventSubscriber } from 'event-dispatch';
|
||||
import {
|
||||
IManualJournalEventCreatedPayload,
|
||||
IManualJournalEventEditedPayload,
|
||||
IManualJournalEventPublishedPayload,
|
||||
IManualJournalEventDeletedPayload,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { ManualJournalGLEntries } from './ManualJournalGLEntries';
|
||||
import { AutoIncrementManualJournal } from './AutoIncrementManualJournal';
|
||||
|
||||
@EventSubscriber()
|
||||
export class ManualJournalWriteGLSubscriber {
|
||||
@Inject()
|
||||
private manualJournalGLEntries: ManualJournalGLEntries;
|
||||
|
||||
@Inject()
|
||||
private manualJournalAutoIncrement: AutoIncrementManualJournal;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
* @param bus
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.manualJournals.onCreated,
|
||||
this.handleWriteJournalEntriesOnCreated
|
||||
);
|
||||
bus.subscribe(
|
||||
events.manualJournals.onCreated,
|
||||
this.handleJournalNumberIncrement
|
||||
);
|
||||
bus.subscribe(
|
||||
events.manualJournals.onEdited,
|
||||
this.handleRewriteJournalEntriesOnEdited
|
||||
);
|
||||
bus.subscribe(
|
||||
events.manualJournals.onPublished,
|
||||
this.handleWriteJournalEntriesOnPublished
|
||||
);
|
||||
bus.subscribe(
|
||||
events.manualJournals.onDeleted,
|
||||
this.handleRevertJournalEntries
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle manual journal created event.
|
||||
* @param {IManualJournalEventCreatedPayload} payload -
|
||||
*/
|
||||
private handleWriteJournalEntriesOnCreated = async ({
|
||||
tenantId,
|
||||
manualJournal,
|
||||
trx,
|
||||
}: IManualJournalEventCreatedPayload) => {
|
||||
// Ingore writing manual journal journal entries in case was not published.
|
||||
if (manualJournal.publishedAt) {
|
||||
await this.manualJournalGLEntries.createManualJournalGLEntries(
|
||||
tenantId,
|
||||
manualJournal.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the manual journal next number increment once the journal be created.
|
||||
* @param {IManualJournalEventCreatedPayload} payload -
|
||||
*/
|
||||
private handleJournalNumberIncrement = async ({
|
||||
tenantId,
|
||||
}: IManualJournalEventCreatedPayload) => {
|
||||
await this.manualJournalAutoIncrement.incrementNextJournalNumber(tenantId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle manual journal edited event.
|
||||
* @param {IManualJournalEventEditedPayload}
|
||||
*/
|
||||
private handleRewriteJournalEntriesOnEdited = async ({
|
||||
tenantId,
|
||||
manualJournal,
|
||||
oldManualJournal,
|
||||
trx,
|
||||
}: IManualJournalEventEditedPayload) => {
|
||||
if (manualJournal.publishedAt) {
|
||||
await this.manualJournalGLEntries.editManualJournalGLEntries(
|
||||
tenantId,
|
||||
manualJournal.id,
|
||||
trx
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles writing journal entries once the manula journal publish.
|
||||
* @param {IManualJournalEventPublishedPayload} payload -
|
||||
*/
|
||||
private handleWriteJournalEntriesOnPublished = async ({
|
||||
tenantId,
|
||||
manualJournal,
|
||||
trx,
|
||||
}: IManualJournalEventPublishedPayload) => {
|
||||
await this.manualJournalGLEntries.createManualJournalGLEntries(
|
||||
tenantId,
|
||||
manualJournal.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle manual journal deleted event.
|
||||
* @param {IManualJournalEventDeletedPayload} payload -
|
||||
*/
|
||||
private handleRevertJournalEntries = async ({
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
trx,
|
||||
}: IManualJournalEventDeletedPayload) => {
|
||||
await this.manualJournalGLEntries.revertManualJournalGLEntries(
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { IManualJournal } from '@/interfaces';
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
export class ManualJournalTransfromer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to expense object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['formattedAmount', 'formattedDate', 'formattedPublishedAt'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted journal amount.
|
||||
* @param {IManualJournal} manualJournal
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (manualJorunal: IManualJournal): string => {
|
||||
return formatNumber(manualJorunal.amount, {
|
||||
currencyCode: manualJorunal.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted date.
|
||||
* @param {IManualJournal} manualJournal
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDate = (manualJorunal: IManualJournal): string => {
|
||||
return this.formatDate(manualJorunal.date);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted published at date.
|
||||
* @param {IManualJournal} manualJournal
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedPublishedAt = (manualJorunal: IManualJournal): string => {
|
||||
return this.formatDate(manualJorunal.publishedAt);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
IManualJournalDTO,
|
||||
IManualJournalsFilter,
|
||||
ISystemUser,
|
||||
} from '@/interfaces';
|
||||
import { CreateManualJournalService } from './CreateManualJournal';
|
||||
import { DeleteManualJournal } from './DeleteManualJournal';
|
||||
import { EditManualJournal } from './EditManualJournal';
|
||||
import { PublishManualJournal } from './PublishManualJournal';
|
||||
import { GetManualJournals } from './GetManualJournals';
|
||||
import { GetManualJournal } from './GetManualJournal';
|
||||
|
||||
@Service()
|
||||
export class ManualJournalsApplication {
|
||||
@Inject()
|
||||
private createManualJournalService: CreateManualJournalService;
|
||||
|
||||
@Inject()
|
||||
private editManualJournalService: EditManualJournal;
|
||||
|
||||
@Inject()
|
||||
private deleteManualJournalService: DeleteManualJournal;
|
||||
|
||||
@Inject()
|
||||
private publishManualJournalService: PublishManualJournal;
|
||||
|
||||
@Inject()
|
||||
private getManualJournalsService: GetManualJournals;
|
||||
|
||||
@Inject()
|
||||
private getManualJournalService: GetManualJournal;
|
||||
|
||||
/**
|
||||
* Make journal entries.
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalDTO} manualJournalDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @returns {Promise<IManualJournal>}
|
||||
*/
|
||||
public createManualJournal = (
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
authorizedUser: ISystemUser
|
||||
) => {
|
||||
return this.createManualJournalService.makeJournalEntries(
|
||||
tenantId,
|
||||
manualJournalDTO,
|
||||
authorizedUser
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits jouranl entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @param {IMakeJournalDTO} manualJournalDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
public editManualJournal = (
|
||||
tenantId: number,
|
||||
manualJournalId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
authorizedUser: ISystemUser
|
||||
) => {
|
||||
return this.editManualJournalService.editJournalEntries(
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
manualJournalDTO,
|
||||
authorizedUser
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given manual journal
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public deleteManualJournal = (tenantId: number, manualJournalId: number) => {
|
||||
return this.deleteManualJournalService.deleteManualJournal(
|
||||
tenantId,
|
||||
manualJournalId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Publish the given manual journal.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} manualJournalId - Manual journal id.
|
||||
*/
|
||||
public publishManualJournal = (tenantId: number, manualJournalId: number) => {
|
||||
return this.publishManualJournalService.publishManualJournal(
|
||||
tenantId,
|
||||
manualJournalId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the specific manual journal.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
* @returns
|
||||
*/
|
||||
public getManualJournal = (tenantId: number, manualJournalId: number) => {
|
||||
return this.getManualJournalService.getManualJournal(
|
||||
tenantId,
|
||||
manualJournalId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the paginated manual journals.
|
||||
* @param {number} tenantId
|
||||
* @param {IManualJournalsFilter} filterDTO
|
||||
* @returns
|
||||
*/
|
||||
public getManualJournals = (
|
||||
tenantId: number,
|
||||
filterDTO: IManualJournalsFilter
|
||||
) => {
|
||||
return this.getManualJournalsService.getManualJournals(tenantId, filterDTO);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IManualJournal,
|
||||
IManualJournalEventPublishedPayload,
|
||||
IManualJournalPublishingPayload,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { CommandManualJournalValidators } from './CommandManualJournalValidators';
|
||||
|
||||
@Service()
|
||||
export class PublishManualJournal {
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validator: CommandManualJournalValidators;
|
||||
|
||||
/**
|
||||
* Authorize the manual journal publishing.
|
||||
* @param {number} tenantId
|
||||
* @param {number} manualJournalId
|
||||
*/
|
||||
private authorize = (tenantId: number, oldManualJournal: IManualJournal) => {
|
||||
// Validate the manual journal is not published.
|
||||
this.validator.validateManualJournalIsNotPublished(oldManualJournal);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// Find the old manual journal or throw not found error.
|
||||
const oldManualJournal = await ManualJournal.query()
|
||||
.findById(manualJournalId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Authorize the manual journal publishing.
|
||||
await this.authorize(tenantId, oldManualJournal);
|
||||
|
||||
// Publishes the manual journal with associated transactions.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onManualJournalPublishing` event.
|
||||
await this.eventPublisher.emitAsync(events.manualJournals.onPublishing, {
|
||||
oldManualJournal,
|
||||
trx,
|
||||
tenantId,
|
||||
} as IManualJournalPublishingPayload);
|
||||
|
||||
// Mark the given manual journal as published.
|
||||
await ManualJournal.query(trx).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.
|
||||
await this.eventPublisher.emitAsync(events.manualJournals.onPublished, {
|
||||
tenantId,
|
||||
manualJournal,
|
||||
manualJournalId,
|
||||
oldManualJournal,
|
||||
trx,
|
||||
} as IManualJournalEventPublishedPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
31
packages/server/src/services/ManualJournals/constants.ts
Normal file
31
packages/server/src/services/ManualJournals/constants.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const ERRORS = {
|
||||
NOT_FOUND: 'manual_journal_not_found',
|
||||
CREDIT_DEBIT_NOT_EQUAL_ZERO: 'credit_debit_not_equal_zero',
|
||||
CREDIT_DEBIT_NOT_EQUAL: 'credit_debit_not_equal',
|
||||
ACCCOUNTS_IDS_NOT_FOUND: 'acccounts_ids_not_found',
|
||||
JOURNAL_NUMBER_EXISTS: 'journal_number_exists',
|
||||
ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
|
||||
CONTACTS_NOT_FOUND: 'contacts_not_found',
|
||||
ENTRIES_CONTACTS_NOT_FOUND: 'ENTRIES_CONTACTS_NOT_FOUND',
|
||||
MANUAL_JOURNAL_ALREADY_PUBLISHED: 'MANUAL_JOURNAL_ALREADY_PUBLISHED',
|
||||
MANUAL_JOURNAL_NO_REQUIRED: 'MANUAL_JOURNAL_NO_REQUIRED',
|
||||
COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS:
|
||||
'COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS',
|
||||
MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID:
|
||||
'MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID',
|
||||
};
|
||||
|
||||
export const CONTACTS_CONFIG = [
|
||||
{
|
||||
accountBySlug: 'accounts-receivable',
|
||||
contactService: 'customer',
|
||||
assignRequired: true,
|
||||
},
|
||||
{
|
||||
accountBySlug: 'accounts-payable',
|
||||
contactService: 'vendor',
|
||||
assignRequired: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_VIEWS = [];
|
||||
Reference in New Issue
Block a user