add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -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'
);
};
}

View File

@@ -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
);
}
};
}

View File

@@ -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 };
});
};
}

View File

@@ -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 };
});
};
}

View 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 };
});
}
}

View File

@@ -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()
);
};
}

View File

@@ -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(),
};
};
}

View File

@@ -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();
};
}

View File

@@ -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
);
};
}

View File

@@ -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);
};
}

View File

@@ -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);
};
}

View File

@@ -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);
});
}
}

View 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 = [];