feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,101 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { ManualJournalsApplication } from './ManualJournalsApplication.service';
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import {
CreateManualJournalDto,
EditManualJournalDto,
} from './dtos/ManualJournal.dto';
@Controller('manual-journals')
@ApiTags('manual-journals')
export class ManualJournalsController {
constructor(private manualJournalsApplication: ManualJournalsApplication) {}
@Post()
@ApiOperation({ summary: 'Create a new manual journal.' })
public createManualJournal(@Body() manualJournalDTO: CreateManualJournalDto) {
return this.manualJournalsApplication.createManualJournal(manualJournalDTO);
}
@Put(':id')
@ApiOperation({ summary: 'Edit the given manual journal.' })
@ApiResponse({
status: 200,
description: 'The manual journal has been successfully edited.',
})
@ApiResponse({ status: 404, description: 'The manual journal not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The manual journal id',
})
public editManualJournal(
@Param('id') manualJournalId: number,
@Body() manualJournalDTO: EditManualJournalDto,
) {
return this.manualJournalsApplication.editManualJournal(
manualJournalId,
manualJournalDTO,
);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete the given manual journal.' })
@ApiResponse({
status: 200,
description: 'The manual journal has been successfully deleted.',
})
@ApiResponse({ status: 404, description: 'The manual journal not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The manual journal id',
})
public deleteManualJournal(@Param('id') manualJournalId: number) {
return this.manualJournalsApplication.deleteManualJournal(manualJournalId);
}
@Put(':id/publish')
@ApiOperation({ summary: 'Publish the given manual journal.' })
@ApiResponse({
status: 200,
description: 'The manual journal has been successfully published.',
})
@ApiResponse({ status: 404, description: 'The manual journal not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The manual journal id',
})
public publishManualJournal(@Param('id') manualJournalId: number) {
return this.manualJournalsApplication.publishManualJournal(manualJournalId);
}
@Get(':id')
@ApiOperation({ summary: 'Retrieves the manual journal details.' })
@ApiResponse({
status: 200,
description: 'The manual journal details have been successfully retrieved.',
})
@ApiResponse({ status: 404, description: 'The manual journal not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The manual journal id',
})
public getManualJournal(@Param('id') manualJournalId: number) {
return this.manualJournalsApplication.getManualJournal(manualJournalId);
}
}

View File

@@ -0,0 +1,39 @@
import { Module } from '@nestjs/common';
import { CreateManualJournalService } from './commands/CreateManualJournal.service';
import { EditManualJournal } from './commands/EditManualJournal.service';
import { DeleteManualJournalService } from './commands/DeleteManualJournal.service';
import { PublishManualJournal } from './commands/PublishManualJournal.service';
import { CommandManualJournalValidators } from './commands/CommandManualJournalValidators.service';
import { AutoIncrementManualJournal } from './commands/AutoIncrementManualJournal.service';
import { ManualJournalBranchesDTOTransformer } from '../Branches/integrations/ManualJournals/ManualJournalDTOTransformer.service';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { AutoIncrementOrdersService } from '../AutoIncrementOrders/AutoIncrementOrders.service';
import { BranchesModule } from '../Branches/Branches.module';
import { ManualJournalsController } from './ManualJournals.controller';
import { ManualJournalsApplication } from './ManualJournalsApplication.service';
import { GetManualJournal } from './queries/GetManualJournal.service';
import { ManualJournalWriteGLSubscriber } from './commands/ManualJournalGLEntriesSubscriber';
import { ManualJournalGLEntries } from './commands/ManualJournalGLEntries';
import { LedgerModule } from '../Ledger/Ledger.module';
@Module({
imports: [BranchesModule, LedgerModule],
controllers: [ManualJournalsController],
providers: [
TenancyContext,
CreateManualJournalService,
EditManualJournal,
DeleteManualJournalService,
PublishManualJournal,
CommandManualJournalValidators,
AutoIncrementManualJournal,
CommandManualJournalValidators,
ManualJournalBranchesDTOTransformer,
AutoIncrementOrdersService,
ManualJournalsApplication,
GetManualJournal,
ManualJournalGLEntries,
ManualJournalWriteGLSubscriber
],
})
export class ManualJournalsModule {}

View File

@@ -0,0 +1,90 @@
import { Injectable } from '@nestjs/common';
import { CreateManualJournalService } from './commands/CreateManualJournal.service';
import { EditManualJournal } from './commands/EditManualJournal.service';
import { PublishManualJournal } from './commands/PublishManualJournal.service';
import { GetManualJournal } from './queries/GetManualJournal.service';
import { DeleteManualJournalService } from './commands/DeleteManualJournal.service';
import { IManualJournalDTO, } from './types/ManualJournals.types';
import { CreateManualJournalDto, EditManualJournalDto } from './dtos/ManualJournal.dto';
// import { GetManualJournals } from './queries/GetManualJournals';
@Injectable()
export class ManualJournalsApplication {
constructor(
private createManualJournalService: CreateManualJournalService,
private editManualJournalService: EditManualJournal,
private deleteManualJournalService: DeleteManualJournalService,
private publishManualJournalService: PublishManualJournal,
private getManualJournalService: GetManualJournal,
// private getManualJournalsService: GetManualJournals,
) {}
/**
* Make journal entries.
* @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO
* @returns {Promise<IManualJournal>}
*/
public createManualJournal = (manualJournalDTO: CreateManualJournalDto) => {
return this.createManualJournalService.makeJournalEntries(manualJournalDTO);
};
/**
* Edits jouranl entries.
* @param {number} manualJournalId
* @param {IMakeJournalDTO} manualJournalDTO
*/
public editManualJournal = (
manualJournalId: number,
manualJournalDTO: EditManualJournalDto,
) => {
return this.editManualJournalService.editJournalEntries(
manualJournalId,
manualJournalDTO,
);
};
/**
* Deletes the given manual journal
* @param {number} manualJournalId
* @return {Promise<void>}
*/
public deleteManualJournal = (manualJournalId: number) => {
return this.deleteManualJournalService.deleteManualJournal(
manualJournalId,
);
};
/**
* Publish the given manual journal.
* @param {number} manualJournalId - Manual journal id.
*/
public publishManualJournal = (manualJournalId: number) => {
return this.publishManualJournalService.publishManualJournal(
manualJournalId,
);
};
/**
* Retrieves the specific manual journal.
* @param {number} manualJournalId
* @returns
*/
public getManualJournal = (manualJournalId: number) => {
return this.getManualJournalService.getManualJournal(
manualJournalId,
);
};
/**
* Retrieves the paginated manual journals.
* @param {number} tenantId
* @param {IManualJournalsFilter} filterDTO
* @returns
*/
// public getManualJournals = (
// filterDTO: IManualJournalsFilter,
// ) => {
// // return this.getManualJournalsService.getManualJournals(filterDTO);
// };
}

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service';
@Injectable()
export class AutoIncrementManualJournal {
/**
*
* @param autoIncrementOrdersService
*/
constructor(
private readonly autoIncrementOrdersService: AutoIncrementOrdersService
) {}
/**
*
* @returns {boolean}
*/
public autoIncrementEnabled = () => {
return this.autoIncrementOrdersService.autoIncrementEnabled(
'manual_journals'
);
};
/**
* Retrieve the next journal number.
* @returns {Promise<string>}
*/
public getNextJournalNumber = (): Promise<string> => {
return this.autoIncrementOrdersService.getNextTransactionNumber(
'manual_journals'
);
};
/**
* Increment the manual journal number.
* @param {number} tenantId
*/
public incrementNextJournalNumber = () => {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
'manual_journals'
);
};
}

View File

@@ -0,0 +1,305 @@
import { Inject, Injectable } from '@nestjs/common';
import { difference, isEmpty, round, sumBy } from 'lodash';
import { ERRORS } from '../constants';
import { ServiceError } from '@/modules/Items/ServiceError';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ManualJournal } from '../models/ManualJournal';
import { Contact } from '@/modules/Contacts/models/Contact';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import {
CreateManualJournalDto,
EditManualJournalDto,
ManualJournalEntryDto,
} from '../dtos/ManualJournal.dto';
@Injectable()
export class CommandManualJournalValidators {
constructor(
@Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>,
@Inject(ManualJournal.name)
private readonly manualJournalModel: TenantModelProxy<typeof ManualJournal>,
@Inject(Contact.name)
private readonly contactModel: TenantModelProxy<typeof Contact>,
) {}
/**
* Validate manual journal credit and debit should be equal.
* @param {CreateManualJournalDto | EditManualJournalDto} manualJournalDTO
*/
public valdiateCreditDebitTotalEquals(
manualJournalDTO: CreateManualJournalDto | EditManualJournalDto,
) {
const totalCredit = round(
sumBy(manualJournalDTO.entries, (entry) => entry.credit || 0),
2,
);
const totalDebit = round(
sumBy(manualJournalDTO.entries, (entry) => entry.debit || 0),
2,
);
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 {CreateManualJournalDto | EditManualJournalDto} manualJournalDTO -
*/
public async validateAccountsExistance(
manualJournalDTO: CreateManualJournalDto | EditManualJournalDto,
) {
const manualAccountsIds = manualJournalDTO.entries.map((e) => e.accountId);
const accounts = await this.accountModel()
.query()
.whereIn('id', manualAccountsIds);
const storedAccountsIds = accounts.map((account) => account.id);
if (difference(manualAccountsIds, storedAccountsIds).length > 0) {
throw new ServiceError(ERRORS.ACCOUNTS_IDS_NOT_FOUND);
}
}
/**
* Validate manual journal number unique.
* @param {number} tenantId
* @param {CreateManualJournalDto | EditManualJournalDto} manualJournalDTO
*/
public async validateManualJournalNoUnique(
journalNumber: string,
notId?: number,
) {
const journals = await this.manualJournalModel()
.query()
.where('journal_number', journalNumber)
.onBuild((builder) => {
if (notId) {
builder.whereNot('id', notId);
}
});
if (journals.length > 0) {
throw new ServiceError(
ERRORS.JOURNAL_NUMBER_EXISTS,
'The journal number is already exist.',
);
}
}
/**
* Validate accounts with contact type.
* @param {number} tenantId
* @param {CreateManualJournalDto | EditManualJournalDto} manualJournalDTO
* @param {string} accountBySlug
* @param {string} contactType
*/
public async validateAccountWithContactType(
entriesDTO: ManualJournalEntryDto[],
accountBySlug: string,
contactType: string,
): Promise<void | ServiceError> {
// Retrieve account meta by the given account slug.
const account = await this.accountModel()
.query()
.findOne('slug', accountBySlug);
// Retrieve all stored contacts on the storage from contacts entries.
const storedContacts = await this.contactModel()
.query()
.whereIn(
'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 {CreateManualJournalDto | EditManualJournalDto} manualJournalDTO
*/
public async dynamicValidateAccountsWithContactType(
entriesDTO: ManualJournalEntryDto[],
): Promise<any> {
return Promise.all([
this.validateAccountWithContactType(
entriesDTO,
'accounts-receivable',
'customer',
),
this.validateAccountWithContactType(
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 {CreateManualJournalDto | EditManualJournalDto} manualJournalDTO
*/
public async validateContactsExistance(
manualJournalDTO: CreateManualJournalDto | EditManualJournalDto,
) {
// 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 this.contactModel()
.query()
.whereIn('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 {ManualJournal} manualJournal
*/
public validateManualJournalIsNotPublished(manualJournal: ManualJournal) {
if (manualJournal.publishedAt) {
throw new ServiceError(ERRORS.MANUAL_JOURNAL_ALREADY_PUBLISHED);
}
}
/**
* Validates the manual journal number require.
* @param {string} journalNumber
* @throws {ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED)}
*/
public validateJournalNoRequireWhenAutoNotEnabled = (
journalNumber: string,
) => {
if (isEmpty(journalNumber)) {
throw new ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED);
}
};
/**
* Filters the not published manual jorunals.
* @param {IManualJournal[]} manualJournal - Manual journal.
* @return {IManualJournal[]}
*/
public getNonePublishedManualJournals(
manualJournals: ManualJournal[],
): ManualJournal[] {
return manualJournals.filter((manualJournal) => !manualJournal.publishedAt);
}
/**
* Filters the published manual journals.
* @param {IManualJournal[]} manualJournal - Manual journal.
* @return {IManualJournal[]}
*/
public getPublishedManualJournals(
manualJournals: ManualJournal[],
): ManualJournal[] {
return manualJournals.filter((expense) => expense.publishedAt);
}
/**
*
* @param {CreateManualJournalDto | EditManualJournalDto} manualJournalDTO
*/
public validateJournalCurrencyWithAccountsCurrency = async (
manualJournalDTO: CreateManualJournalDto | EditManualJournalDto,
baseCurrency: string,
) => {
const accountsIds = manualJournalDTO.entries.map((e) => e.accountId);
const accounts = await this.accountModel()
.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,163 @@
import { sumBy, omit } from 'lodash';
import * as moment from 'moment';
import * as R from 'ramda';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import * as composeAsync from 'async/compose';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
IManualJournalDTO,
IManualJournalEventCreatedPayload,
IManualJournalCreatingPayload,
} from '../types/ManualJournals.types';
import { CommandManualJournalValidators } from './CommandManualJournalValidators.service';
import { AutoIncrementManualJournal } from './AutoIncrementManualJournal.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { ManualJournal } from '../models/ManualJournal';
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { ManualJournalBranchesDTOTransformer } from '@/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { CreateManualJournalDto } from '../dtos/ManualJournal.dto';
@Injectable()
export class CreateManualJournalService {
constructor(
private tenancyContext: TenancyContext,
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
private validator: CommandManualJournalValidators,
private autoIncrement: AutoIncrementManualJournal,
private branchesDTOTransformer: ManualJournalBranchesDTOTransformer,
@Inject(ManualJournal.name)
private manualJournalModel: TenantModelProxy<typeof ManualJournal>,
) {}
/**
* Transform the new manual journal DTO to upsert graph operation.
* @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO.
* @returns {Promise<ManualJournal>}
*/
private async transformNewDTOToModel(
manualJournalDTO: CreateManualJournalDto,
): Promise<ManualJournal> {
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();
// The manual or auto-increment journal number.
const journalNumber = manualJournalDTO.journalNumber || autoNextNumber;
const tenant = await this.tenancyContext.getTenant(true);
const authorizedUser = await this.tenancyContext.getSystemUser();
const entries = R.compose(
// Associate the default index to each item entry.
assocItemEntriesDefaultIndex,
)(manualJournalDTO.entries);
const initialDTO = {
...omit(manualJournalDTO, ['publish', 'attachments']),
...(manualJournalDTO.publish
? { publishedAt: moment().toMySqlDateTime() }
: {}),
amount,
date,
currencyCode:
manualJournalDTO.currencyCode || tenant?.metadata?.baseCurrency,
exchangeRate: manualJournalDTO.exchangeRate || 1,
journalNumber,
entries,
userId: authorizedUser.id,
};
return composeAsync(
// Omits the `branchId` from entries if multiply branches feature not active.
this.branchesDTOTransformer.transformDTO,
)(initialDTO) as ManualJournal;
}
/**
* Authorize the manual journal creating.
* @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO
* @param {ISystemUser} authorizedUser
*/
private authorize = async (manualJournalDTO: CreateManualJournalDto) => {
const tenant = await this.tenancyContext.getTenant(true);
// Validate the total credit should equals debit.
this.validator.valdiateCreditDebitTotalEquals(manualJournalDTO);
// Validate the contacts existance.
await this.validator.validateContactsExistance(manualJournalDTO);
// Validate entries accounts existance.
await this.validator.validateAccountsExistance(manualJournalDTO);
// Validate manual journal number require when auto-increment not enabled.
this.validator.validateJournalNoRequireWhenAutoNotEnabled(
manualJournalDTO.journalNumber,
);
// Validate manual journal uniquiness on the storage.
if (manualJournalDTO.journalNumber) {
await this.validator.validateManualJournalNoUnique(
manualJournalDTO.journalNumber,
);
}
// Validate accounts with contact type from the given config.
await this.validator.dynamicValidateAccountsWithContactType(
manualJournalDTO.entries,
);
// Validates the accounts currency with journal currency.
await this.validator.validateJournalCurrencyWithAccountsCurrency(
manualJournalDTO,
tenant.metadata.baseCurrency,
);
};
/**
* Make journal entries.
* @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO
* @param {ISystemUser} authorizedUser
*/
public makeJournalEntries = async (
manualJournalDTO: CreateManualJournalDto,
trx?: Knex.Transaction,
): Promise<ManualJournal> => {
// Authorize manual journal creating.
await this.authorize(manualJournalDTO);
// Transformes the next DTO to model.
const manualJournalObj =
await this.transformNewDTOToModel(manualJournalDTO);
// Creates a manual journal transactions with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onManualJournalCreating` event.
await this.eventPublisher.emitAsync(events.manualJournals.onCreating, {
manualJournalDTO,
trx,
} as IManualJournalCreatingPayload);
// Upsert the manual journal object.
const manualJournal = await this.manualJournalModel()
.query(trx)
.upsertGraph({
...manualJournalObj,
});
// Triggers `onManualJournalCreated` event.
await this.eventPublisher.emitAsync(events.manualJournals.onCreated, {
manualJournal,
manualJournalDTO,
trx,
} as IManualJournalEventCreatedPayload);
return manualJournal;
}, trx);
};
}

View File

@@ -0,0 +1,75 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
IManualJournalEventDeletedPayload,
IManualJournalDeletingPayload,
} from '../types/ManualJournals.types';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ManualJournal } from '../models/ManualJournal';
import { ManualJournalEntry } from '../models/ManualJournalEntry';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class DeleteManualJournalService {
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(ManualJournal.name)
private readonly manualJournalModel: TenantModelProxy<typeof ManualJournal>,
@Inject(ManualJournalEntry.name)
private readonly manualJournalEntryModel: TenantModelProxy<
typeof ManualJournalEntry
>,
) {}
/**
* Deletes the given manual journal
* @param {number} manualJournalId
* @return {Promise<void>}
*/
public deleteManualJournal = async (
manualJournalId: number,
): Promise<{
oldManualJournal: ManualJournal;
}> => {
// Validate the manual journal exists on the storage.
const oldManualJournal = await this.manualJournalModel()
.query()
.findById(manualJournalId)
.throwIfNotFound();
// Deletes the manual journal with associated transactions under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onManualJournalDeleting` event.
await this.eventPublisher.emitAsync(events.manualJournals.onDeleting, {
oldManualJournal,
trx,
} as IManualJournalDeletingPayload);
// Deletes the manual journal entries.
await this.manualJournalEntryModel()
.query(trx)
.where('manualJournalId', manualJournalId)
.delete();
// Deletes the manual journal transaction.
await this.manualJournalModel()
.query(trx)
.findById(manualJournalId)
.delete();
// Triggers `onManualJournalDeleted` event.
await this.eventPublisher.emitAsync(events.manualJournals.onDeleted, {
manualJournalId,
oldManualJournal,
trx,
} as IManualJournalEventDeletedPayload);
return { oldManualJournal };
});
};
}

View File

@@ -0,0 +1,141 @@
import { Knex } from 'knex';
import { omit, sumBy } from 'lodash';
import * as moment from 'moment';
import { Inject, Injectable } from '@nestjs/common';
import {
IManualJournalEventEditedPayload,
IManualJournalEditingPayload,
} from '../types/ManualJournals.types';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { CommandManualJournalValidators } from './CommandManualJournalValidators.service';
import { events } from '@/common/events/events';
import { ManualJournal } from '../models/ManualJournal';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { EditManualJournalDto } from '../dtos/ManualJournal.dto';
@Injectable()
export class EditManualJournal {
constructor(
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
private validator: CommandManualJournalValidators,
@Inject(ManualJournal.name)
private manualJournalModel: TenantModelProxy<typeof ManualJournal>,
) {}
/**
* Authorize the manual journal editing.
* @param {number} manualJournalId - Manual journal id.
* @param {IManualJournalDTO} manualJournalDTO - Manual journal DTO.
*/
private authorize = async (
manualJournalId: number,
manualJournalDTO: EditManualJournalDto,
) => {
// Validates the total credit and debit to be equals.
this.validator.valdiateCreditDebitTotalEquals(manualJournalDTO);
// Validate the contacts existance.
await this.validator.validateContactsExistance(manualJournalDTO);
// Validates entries accounts existance.
await this.validator.validateAccountsExistance(manualJournalDTO);
// Validates the manual journal number uniquiness.
if (manualJournalDTO.journalNumber) {
await this.validator.validateManualJournalNoUnique(
manualJournalDTO.journalNumber,
manualJournalId,
);
}
// Validate accounts with contact type from the given config.
await this.validator.dynamicValidateAccountsWithContactType(
manualJournalDTO.entries,
);
};
/**
* Transform the edit manual journal DTO to upsert graph operation.
* @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO.
* @param {IManualJournal} oldManualJournal
*/
private transformEditDTOToModel = (
manualJournalDTO: EditManualJournalDto,
oldManualJournal: ManualJournal,
) => {
const amount = sumBy(manualJournalDTO.entries, 'credit') || 0;
const date = moment(manualJournalDTO.date).format('YYYY-MM-DD');
return {
id: oldManualJournal.id,
...omit(manualJournalDTO, ['publish', 'attachments']),
...(manualJournalDTO.publish && !oldManualJournal.publishedAt
? { publishedAt: moment().toMySqlDateTime() }
: {}),
amount,
date,
};
};
/**
* Edits jouranl entries.
* @param {number} manualJournalId - Manual journal id.
* @param {IMakeJournalDTO} manualJournalDTO - Manual journal DTO.
*/
public async editJournalEntries(
manualJournalId: number,
manualJournalDTO: EditManualJournalDto,
): Promise<{
manualJournal: ManualJournal;
oldManualJournal: ManualJournal;
}> {
// Validates the manual journal existance on the storage.
const oldManualJournal = await this.manualJournalModel()
.query()
.findById(manualJournalId)
.throwIfNotFound();
// Authorize manual journal editing.
await this.authorize(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(async (trx: Knex.Transaction) => {
// Triggers `onManualJournalEditing` event.
await this.eventPublisher.emitAsync(events.manualJournals.onEditing, {
manualJournalDTO,
oldManualJournal,
trx,
} as IManualJournalEditingPayload);
// Upserts the manual journal graph to the storage.
await this.manualJournalModel()
.query(trx)
.upsertGraph({
...manualJournalObj,
});
// Retrieve the given manual journal with associated entries after modifications.
const manualJournal = await this.manualJournalModel()
.query(trx)
.findById(manualJournalId)
.withGraphFetched('entries');
// Triggers `onManualJournalEdited` event.
await this.eventPublisher.emitAsync(events.manualJournals.onEdited, {
manualJournal,
oldManualJournal,
manualJournalDTO,
trx,
} as IManualJournalEventEditedPayload);
return { manualJournal, oldManualJournal };
});
}
}

View File

@@ -0,0 +1,30 @@
// import { Inject, Service } from 'typedi';
// import { IManualJournalsFilter } from '@/interfaces';
// import { Exportable } from '../../Export/Exportable';
// import { ManualJournalsApplication } from '../ManualJournalsApplication';
// import { EXPORT_SIZE_LIMIT } from '../../Export/constants';
// @Service()
// export class ManualJournalsExportable extends Exportable {
// @Inject()
// private manualJournalsApplication: ManualJournalsApplication;
// /**
// * Retrieves the manual journals data to exportable sheet.
// * @param {number} tenantId
// * @returns
// */
// public exportable(tenantId: number, query: IManualJournalsFilter) {
// const parsedQuery = {
// sortOrder: 'desc',
// columnSortBy: 'created_at',
// ...query,
// page: 1,
// pageSize: EXPORT_SIZE_LIMIT,
// } as IManualJournalsFilter;
// return this.manualJournalsApplication
// .getManualJournals(tenantId, parsedQuery)
// .then((output) => output.manualJournals);
// }
// }

View File

@@ -0,0 +1,82 @@
import { Ledger } from '@/modules/Ledger/Ledger';
import { ManualJournal } from '../models/ManualJournal';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { ManualJournalEntry } from '../models/ManualJournalEntry';
export class ManualJournalGL {
manualJournal: ManualJournal;
constructor(manualJournal: ManualJournal) {
this.manualJournal = manualJournal;
}
/**
* Retrieves the ledger of the given manual journal.
* @param {ManualJournal} manualJournal - The manual journal.
* @returns {Ledger}
*/
public getManualJournalGLedger = () => {
const entries = this.getManualJournalGLEntries();
return new Ledger(entries);
};
/**
* Retrieves the common entry details of the manual journal
* @param {IManualJournal} manualJournal - The manual journal.
* @returns {Partial<ILedgerEntry>}
*/
public get manualJournalCommonEntry() {
return {
transactionNumber: this.manualJournal.journalNumber,
referenceNumber: this.manualJournal.reference,
createdAt: this.manualJournal.createdAt,
date: this.manualJournal.date,
currencyCode: this.manualJournal.currencyCode,
exchangeRate: this.manualJournal.exchangeRate,
transactionType: 'Journal',
transactionId: this.manualJournal.id,
userId: this.manualJournal.userId,
};
}
/**
* Retrieves the ledger entry of the given manual journal and
* its associated entry.
* @param {IManualJournal} manualJournal - The manual journal.
* @param {IManualJournalEntry} entry - The manual journal entry.
* @returns {ILedgerEntry}
*/
public getManualJournalEntry(entry: ManualJournalEntry): ILedgerEntry {
const commonEntry = this.manualJournalCommonEntry;
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,
};
}
/**
* Retrieves the ledger entries of the given manual journal.
* @param {IManualJournal} manualJournal - The manual journal.
* @returns {ILedgerEntry[]}
*/
public getManualJournalGLEntries = (): ILedgerEntry[] => {
return this.manualJournal.entries
.map((entry) => this.getManualJournalEntry(entry))
.flat();
};
}

View File

@@ -0,0 +1,73 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
import { ManualJournal } from '../models/ManualJournal';
import { ManualJournalGL } from './ManualJournalGL';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class ManualJournalGLEntries {
/**
* @param {typeof ManualJournal} manualJournalModel - The manual journal model.
* @param {LedgerStorageService} ledgerStorage - The ledger storage service.
*/
constructor(
@Inject(ManualJournal.name)
private readonly manualJournalModel: TenantModelProxy<typeof ManualJournal>,
private readonly ledgerStorage: LedgerStorageService,
) {}
/**
* Create manual journal GL entries.
* @param {number} manualJournalId - The manual journal ID.
* @param {Knex.Transaction} trx - The knex transaction.
*/
public createManualJournalGLEntries = async (
manualJournalId: number,
trx?: Knex.Transaction,
) => {
// Retrieves the given manual journal with associated entries.
const manualJournal = await this.manualJournalModel()
.query(trx)
.findById(manualJournalId)
.withGraphFetched('entries.account');
// Retrieves the ledger entries of the given manual journal.
const ledger = new ManualJournalGL(manualJournal).getManualJournalGLedger();
// Commits the given ledger on the storage.
await this.ledgerStorage.commit(ledger, trx);
};
/**
* Edits manual journal GL entries.
* @param {number} manualJournalId - The manual journal ID.
* @param {Knex.Transaction} trx - The knex transaction.
*/
public editManualJournalGLEntries = async (
manualJournalId: number,
trx?: Knex.Transaction,
) => {
// Reverts the manual journal GL entries.
await this.revertManualJournalGLEntries(manualJournalId, trx);
// Write the manual journal GL entries.
await this.createManualJournalGLEntries(manualJournalId, trx);
};
/**
* Deletes the manual journal GL entries.
* @param {number} manualJournalId - The manual journal ID.
* @param {Knex.Transaction} trx - The knex transaction.
*/
public revertManualJournalGLEntries = async (
manualJournalId: number,
trx?: Knex.Transaction,
): Promise<void> => {
return this.ledgerStorage.deleteByReference(
manualJournalId,
'Journal',
trx,
);
};
}

View File

@@ -0,0 +1,102 @@
import { OnEvent } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import {
IManualJournalEventCreatedPayload,
IManualJournalEventEditedPayload,
IManualJournalEventPublishedPayload,
IManualJournalEventDeletedPayload,
} from '../types/ManualJournals.types';
import { ManualJournalGLEntries } from './ManualJournalGLEntries';
import { AutoIncrementManualJournal } from './AutoIncrementManualJournal.service';
import { events } from '@/common/events/events';
@Injectable()
export class ManualJournalWriteGLSubscriber {
/**
* @param {ManualJournalGLEntries} manualJournalGLEntries - The manual journal GL entries service.
* @param {AutoIncrementManualJournal} manualJournalAutoIncrement - The manual journal auto increment service.
*/
constructor(
private manualJournalGLEntries: ManualJournalGLEntries,
private manualJournalAutoIncrement: AutoIncrementManualJournal,
) {}
/**
* Handle manual journal created event.
* @param {IManualJournalEventCreatedPayload} payload -
* @returns {Promise<void>}
*/
@OnEvent(events.manualJournals.onCreated)
public async handleWriteJournalEntriesOnCreated({
manualJournal,
trx,
}: IManualJournalEventCreatedPayload) {
// Ingore writing manual journal journal entries in case was not published.
if (!manualJournal.publishedAt) return;
await this.manualJournalGLEntries.createManualJournalGLEntries(
manualJournal.id,
trx,
);
}
/**
* Handles the manual journal next number increment once the journal be created.
* @param {IManualJournalEventCreatedPayload} payload -
* @return {Promise<void>}
*/
@OnEvent(events.manualJournals.onCreated)
public async handleJournalNumberIncrement({}: IManualJournalEventCreatedPayload) {
await this.manualJournalAutoIncrement.incrementNextJournalNumber();
}
/**
* Handle manual journal edited event.
* @param {IManualJournalEventEditedPayload}
* @return {Promise<void>}
*/
@OnEvent(events.manualJournals.onEdited)
public async handleRewriteJournalEntriesOnEdited({
manualJournal,
oldManualJournal,
trx,
}: IManualJournalEventEditedPayload) {
if (manualJournal.publishedAt) {
await this.manualJournalGLEntries.editManualJournalGLEntries(
manualJournal.id,
trx,
);
}
}
/**
* Handles writing journal entries once the manula journal publish.
* @param {IManualJournalEventPublishedPayload} payload -
* @return {Promise<void>}
*/
@OnEvent(events.manualJournals.onPublished)
public async handleWriteJournalEntriesOnPublished({
manualJournal,
trx,
}: IManualJournalEventPublishedPayload) {
await this.manualJournalGLEntries.createManualJournalGLEntries(
manualJournal.id,
trx,
);
}
/**
* Handle manual journal deleted event.
* @param {IManualJournalEventDeletedPayload} payload -
*/
@OnEvent(events.manualJournals.onDeleted)
public async handleRevertJournalEntries({
manualJournalId,
trx,
}: IManualJournalEventDeletedPayload) {
await this.manualJournalGLEntries.revertManualJournalGLEntries(
manualJournalId,
trx,
);
}
}

View File

@@ -0,0 +1,60 @@
// import { Inject } from 'typedi';
// import { Knex } from 'knex';
// import * as Yup from 'yup';
// import { Importable } from '../../Import/Importable';
// import { CreateManualJournalService } from './CreateManualJournal.service';
// import { IManualJournalDTO } from '@/interfaces';
// import { ImportableContext } from '../../Import/interfaces';
// import { ManualJournalsSampleData } from '../constants';
// export class ManualJournalImportable extends Importable {
// @Inject()
// private createManualJournalService: CreateManualJournalService;
// /**
// * Importing to account service.
// * @param {number} tenantId
// * @param {IAccountCreateDTO} createAccountDTO
// * @returns
// */
// public importable(
// tenantId: number,
// createJournalDTO: IManualJournalDTO,
// trx?: Knex.Transaction
// ) {
// return this.createManualJournalService.makeJournalEntries(
// tenantId,
// createJournalDTO,
// {},
// trx
// );
// }
// /**
// * Transformes the DTO before passing it to importable and validation.
// * @param {Record<string, any>} createDTO
// * @param {ImportableContext} context
// * @returns {Record<string, any>}
// */
// public transform(createDTO: Record<string, any>, context: ImportableContext) {
// return createDTO;
// }
// /**
// * Params validation schema.
// * @returns {ValidationSchema[]}
// */
// public paramsValidationSchema() {
// return Yup.object().shape({
// autoIncrement: Yup.boolean(),
// });
// }
// /**
// * Retrieves the sample data of manual journals that used to download sample sheet.
// * @returns {Record<string, any>}
// */
// public sampleData(): Record<string, any>[] {
// return ManualJournalsSampleData;
// }
// }

View File

@@ -0,0 +1,78 @@
import * as moment from 'moment';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
IManualJournalEventPublishedPayload,
IManualJournalPublishingPayload,
} from '../types/ManualJournals.types';
import { CommandManualJournalValidators } from './CommandManualJournalValidators.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ManualJournal } from '../models/ManualJournal';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class PublishManualJournal {
constructor(
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
private validator: CommandManualJournalValidators,
@Inject(ManualJournal.name)
private manualJournalModel: TenantModelProxy<typeof ManualJournal>,
) {}
/**
* Authorize the manual journal publishing.
* @param {number} manualJournalId - Manual journal id.
*/
private authorize = (oldManualJournal: ManualJournal) => {
// Validate the manual journal is not published.
this.validator.validateManualJournalIsNotPublished(oldManualJournal);
};
/**
* Publish the given manual journal.
* @param {number} manualJournalId - Manual journal id.
*/
public async publishManualJournal(manualJournalId: number): Promise<void> {
// Find the old manual journal or throw not found error.
const oldManualJournal = await this.manualJournalModel()
.query()
.findById(manualJournalId)
.throwIfNotFound();
// Authorize the manual journal publishing.
await this.authorize(oldManualJournal);
// Publishes the manual journal with associated transactions.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onManualJournalPublishing` event.
await this.eventPublisher.emitAsync(events.manualJournals.onPublishing, {
oldManualJournal,
trx,
} as IManualJournalPublishingPayload);
// Mark the given manual journal as published.
await this.manualJournalModel()
.query(trx)
.findById(manualJournalId)
.patch({
publishedAt: moment().toMySqlDateTime(),
});
// Retrieve the manual journal with enrties after modification.
const manualJournal = await this.manualJournalModel()
.query()
.findById(manualJournalId)
.withGraphFetched('entries');
// Triggers `onManualJournalPublishedBulk` event.
await this.eventPublisher.emitAsync(events.manualJournals.onPublished, {
manualJournal,
oldManualJournal,
trx,
} as IManualJournalEventPublishedPayload);
});
}
}

View File

@@ -0,0 +1,64 @@
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',
ACCOUNTS_IDS_NOT_FOUND: 'accounts_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 = [];
export const ManualJournalsSampleData = [
{
Date: '2024-02-02',
'Journal No': 'J-100022',
'Reference No.': 'REF-10000',
'Currency Code': '',
'Exchange Rate': '',
'Journal Type': '',
Description: 'Animi quasi qui itaque aut possimus illum est magnam enim.',
Credit: 1000,
Debit: 0,
Note: 'Qui reprehenderit voluptate.',
Account: 'Bank Account',
Contact: '',
Publish: 'T',
},
{
Date: '2024-02-02',
'Journal No': 'J-100022',
'Reference No.': 'REF-10000',
'Currency Code': '',
'Exchange Rate': '',
'Journal Type': '',
Description: 'In assumenda dicta autem non est corrupti non et.',
Credit: 0,
Debit: 1000,
Note: 'Omnis tempora qui fugiat neque dolor voluptatem aut repudiandae nihil.',
Account: 'Bank Account',
Contact: '',
Publish: 'T',
},
];

View File

@@ -0,0 +1,130 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsDate,
IsInt,
IsNumber,
IsOptional,
IsPositive,
IsString,
MaxLength,
Min,
ValidateNested,
} from 'class-validator';
export class ManualJournalEntryDto {
@ApiProperty({ description: 'Entry index' })
@IsInt()
index: number;
@ApiPropertyOptional({ description: 'Credit amount' })
@IsOptional()
@IsNumber()
@Min(0)
credit?: number;
@ApiPropertyOptional({ description: 'Debit amount' })
@IsOptional()
@IsNumber()
@Min(0)
debit?: number;
@ApiProperty({ description: 'Account ID' })
@IsInt()
accountId: number;
@ApiPropertyOptional({ description: 'Entry note' })
@IsOptional()
@IsString()
note?: string;
@ApiPropertyOptional({ description: 'Contact ID' })
@IsOptional()
@IsInt()
contactId?: number;
@ApiPropertyOptional({ description: 'Branch ID' })
@IsOptional()
@IsInt()
branchId?: number;
@ApiPropertyOptional({ description: 'Project ID' })
@IsOptional()
@IsInt()
projectId?: number;
}
class AttachmentDto {
@ApiProperty({ description: 'Attachment key' })
@IsString()
key: string;
}
export class CommandManualJournalDto {
@ApiProperty({ description: 'Journal date' })
@IsDate()
@Type(() => Date)
date: Date;
@ApiPropertyOptional({ description: 'Currency code' })
@IsOptional()
@IsString()
currencyCode?: string;
@ApiPropertyOptional({ description: 'Exchange rate' })
@IsOptional()
@IsNumber()
@IsPositive()
exchangeRate?: number;
@ApiPropertyOptional({ description: 'Journal number' })
@IsOptional()
@IsString()
@MaxLength(255)
journalNumber?: string;
@ApiPropertyOptional({ description: 'Journal type' })
@IsOptional()
@IsString()
@MaxLength(255)
journalType?: string;
@ApiPropertyOptional({ description: 'Reference' })
@IsOptional()
@IsString()
@MaxLength(255)
reference?: string;
@ApiPropertyOptional({ description: 'Description' })
@IsOptional()
@IsString()
description?: string;
@ApiPropertyOptional({ description: 'Branch ID' })
@IsOptional()
@IsInt()
branchId?: number;
@ApiPropertyOptional({ description: 'Publish status' })
@IsOptional()
@IsBoolean()
publish?: boolean;
@ApiProperty({ description: 'Journal entries', type: [ManualJournalEntryDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => ManualJournalEntryDto)
entries: ManualJournalEntryDto[];
@ApiPropertyOptional({ description: 'Attachments', type: [AttachmentDto] })
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AttachmentDto)
attachments?: AttachmentDto[];
}
export class CreateManualJournalDto extends CommandManualJournalDto {}
export class EditManualJournalDto extends CommandManualJournalDto {}

View File

@@ -0,0 +1,209 @@
import { Model, mixin } from 'objection';
// import TenantModel from 'models/TenantModel';
// import { formatNumber } from 'utils';
// import ModelSetting from './ModelSetting';
// import ManualJournalSettings from './ManualJournal.Settings';
// import CustomViewBaseModel from './CustomViewBaseModel';
// import { DEFAULT_VIEWS } from '@/services/ManualJournals/constants';
// import ModelSearchable from './ModelSearchable';
import { ManualJournalEntry } from './ManualJournalEntry';
import { Document } from '@/modules/ChromiumlyTenancy/models/Document';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
export class ManualJournal extends TenantBaseModel {
date: Date;
journalNumber: string;
journalType: string;
reference: string;
amount: number;
currencyCode: string;
exchangeRate: number | null;
publishedAt: Date | string | null;
description: string;
userId?: number;
createdAt?: Date;
updatedAt?: Date;
entries!: ManualJournalEntry[];
attachments!: Document[];
branchId?: number;
/**
* Table name.
*/
static get tableName() {
return 'manual_journals';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isPublished', 'amountFormatted'];
}
/**
* Retrieve the amount formatted value.
*/
// get amountFormatted() {
// return formatNumber(this.amount, { currencyCode: this.currencyCode });
// }
/**
* Detarmines whether the invoice is published.
* @return {boolean}
*/
get isPublished() {
return !!this.publishedAt;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Sort by status query.
*/
sortByStatus(query, order) {
query.orderByRaw(`PUBLISHED_AT IS NULL ${order}`);
},
/**
* Filter by draft status.
*/
filterByDraft(query) {
query.whereNull('publishedAt');
},
/**
* Filter by published status.
*/
filterByPublished(query) {
query.whereNotNull('publishedAt');
},
/**
* Filter by the given status.
*/
filterByStatus(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('filterByDraft');
break;
case 'published':
default:
query.modify('filterByPublished');
break;
}
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { AccountTransaction } = require('../../Accounts/models/AccountTransaction.model');
const { ManualJournalEntry } = require('./ManualJournalEntry');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
return {
entries: {
relation: Model.HasManyRelation,
modelClass: ManualJournalEntry,
join: {
from: 'manual_journals.id',
to: 'manual_journals_entries.manualJournalId',
},
filter(query) {
query.orderBy('index', 'ASC');
},
},
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction,
join: {
from: 'manual_journals.id',
to: 'accounts_transactions.referenceId',
},
filter: (query) => {
query.where('referenceType', 'Journal');
},
},
/**
* Manual journal may has many attached attachments.
*/
attachments: {
relation: Model.ManyToManyRelation,
modelClass: Document,
join: {
from: 'manual_journals.id',
through: {
from: 'document_links.modelId',
to: 'document_links.documentId',
},
to: 'documents.id',
},
filter(query) {
query.where('model_ref', 'ManualJournal');
},
},
/**
* Manual journal may belongs to matched bank transaction.
*/
// matchedBankTransaction: {
// relation: Model.BelongsToOneRelation,
// modelClass: MatchedBankTransaction,
// join: {
// from: 'manual_journals.id',
// to: 'matched_bank_transactions.referenceId',
// },
// filter(query) {
// query.where('reference_type', 'ManualJournal');
// },
// },
};
}
// static get meta() {
// return ManualJournalSettings;
// }
// /**
// * Retrieve the default custom views, roles and columns.
// */
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'journal_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,71 @@
import { Model } from 'objection';
import { BaseModel } from '@/models/Model';
import { Account } from '@/modules/Accounts/models/Account.model';
import { Contact } from '@/modules/Contacts/models/Contact';
import { Branch } from '@/modules/Branches/models/Branch.model';
export class ManualJournalEntry extends BaseModel {
index: number;
credit: number;
debit: number;
accountId: number;
note: string;
contactId?: number;
branchId!: number;
projectId?: number;
contact?: Contact;
account?: Account;
branch?: Branch;
/**
* Table name.
*/
static get tableName() {
return 'manual_journals_entries';
}
/**
* Model timestamps.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { Account } = require('../../Accounts/models/Account.model');
const { Contact } = require('../../Contacts/models/Contact');
const { Branch } = require('../../Branches/models/Branch.model');
return {
account: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'manual_journals_entries.accountId',
to: 'accounts.id',
},
},
contact: {
relation: Model.BelongsToOneRelation,
modelClass: Contact,
join: {
from: 'manual_journals_entries.contactId',
to: 'contacts.id',
},
},
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch,
join: {
from: 'manual_journals_entries.branchId',
to: 'branches.id',
},
},
};
}
}

View File

@@ -0,0 +1,37 @@
import { Inject, Injectable } from '@nestjs/common';
import { ManualJournal } from '../models/ManualJournal';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { ManualJournalTransfromer } from './ManualJournalTransformer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetManualJournal {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(ManualJournal.name)
private readonly manualJournalModel: TenantModelProxy<typeof ManualJournal>,
) {}
/**
* Retrieve manual journal details with associated journal transactions.
* @param {number} tenantId
* @param {number} manualJournalId
*/
public getManualJournal = async (manualJournalId: number) => {
const manualJournal = await this.manualJournalModel()
.query()
.findById(manualJournalId)
.withGraphFetched('entries.account')
.withGraphFetched('entries.contact')
.withGraphFetched('entries.branch')
.withGraphFetched('transactions')
.withGraphFetched('attachments')
.throwIfNotFound();
return this.transformer.transform(
manualJournal,
new ManualJournalTransfromer(),
);
};
}

View File

@@ -0,0 +1,68 @@
import * as R from 'ramda';
import { ManualJournalTransfromer } from './ManualJournalTransformer';
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
import { ManualJournal } from '../models/ManualJournal';
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { IManualJournalsFilter } from '../types/ManualJournals.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetManualJournals {
constructor(
private readonly dynamicListService: DynamicListService,
private readonly transformer: TransformerInjectable,
@Inject(ManualJournal.name)
private readonly manualJournalModel: TenantModelProxy<typeof ManualJournal>,
) {}
/**
* 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 {IManualJournalsFilter} filter -
*/
public getManualJournals = async (
filterDTO: IManualJournalsFilter,
): Promise<{
manualJournals: ManualJournal[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> => {
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic service.
const dynamicService = await this.dynamicListService.dynamicList(
this.manualJournalModel(),
filter,
);
const { results, pagination } = await this.manualJournalModel()
.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(
results,
new ManualJournalTransfromer(),
);
return {
manualJournals,
pagination,
filterMeta: dynamicService.getResponseMeta(),
};
};
}

View File

@@ -0,0 +1,66 @@
import { Transformer } from '@/modules/Transformer/Transformer';
import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer';
import { ManualJournal } from '../models/ManualJournal';
export class ManualJournalTransfromer extends Transformer {
/**
* Include these attributes to expense object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedDate',
'formattedPublishedAt',
'formattedCreatedAt',
'attachments',
];
};
/**
* Retrieve formatted journal amount.
* @param {IManualJournal} manualJournal
* @returns {string}
*/
protected formattedAmount = (manualJorunal: ManualJournal): string => {
return this.formatNumber(manualJorunal.amount, {
currencyCode: manualJorunal.currencyCode,
});
};
/**
* Retrieve formatted date.
* @param {ManualJournal} manualJournal
* @returns {string}
*/
protected formattedDate = (manualJorunal: ManualJournal): string => {
return this.formatDate(manualJorunal.date);
};
/**
* Retrieve formatted created at date.
* @param {ManualJournal} manualJournal
* @returns {string}
*/
protected formattedCreatedAt = (manualJorunal: ManualJournal): string => {
return this.formatDate(manualJorunal.createdAt);
};
/**
* Retrieve formatted published at date.
* @param {ManualJournal} manualJournal
* @returns {string}
*/
protected formattedPublishedAt = (manualJorunal: ManualJournal): string => {
return this.formatDate(manualJorunal.publishedAt);
};
/**
* Retrieves the manual journal attachments.
* @param {ManualJournal} manualJorunal
* @returns
*/
protected attachments = (manualJorunal: ManualJournal) => {
return this.item(manualJorunal.attachments, new AttachmentTransformer());
};
}

View File

@@ -0,0 +1,101 @@
import { Knex } from 'knex';
// import { IDynamicListFilterDTO } from './DynamicFilter';
// import { ISystemUser } from './User';
// import { IAccount } from './Account';
// import { AttachmentLinkDTO } from './Attachments';
import { ManualJournal } from '../models/ManualJournal';
import { AttachmentLinkDTO } from '@/modules/Attachments/Attachments.types';
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
export interface IManualJournalEntryDTO {
index: number;
credit: number;
debit: number;
accountId: number;
note: string;
contactId?: number;
branchId?: number
projectId?: number;
}
export interface IManualJournalDTO {
date: Date;
currencyCode?: string;
exchangeRate?: number;
journalNumber: string;
journalType: string;
reference?: string;
description?: string;
publish?: boolean;
branchId?: number;
entries: IManualJournalEntryDTO[];
attachments?: AttachmentLinkDTO[];
}
export interface IManualJournalsFilter extends IDynamicListFilter {
stringifiedFilterRoles?: string;
page: number;
pageSize: number;
}
export interface IManualJournalEventPublishedPayload {
// tenantId: number;
manualJournal: ManualJournal;
// manualJournalId: number;
oldManualJournal: ManualJournal;
trx: Knex.Transaction;
}
export interface IManualJournalPublishingPayload {
oldManualJournal: ManualJournal;
trx: Knex.Transaction;
// tenantId: number;
}
export interface IManualJournalEventDeletedPayload {
// tenantId: number;
manualJournalId: number;
oldManualJournal: ManualJournal;
trx?: Knex.Transaction;
}
export interface IManualJournalDeletingPayload {
// tenantId: number;
oldManualJournal: ManualJournal;
trx: Knex.Transaction;
}
export interface IManualJournalEventEditedPayload {
// tenantId: number;
manualJournal: ManualJournal;
oldManualJournal: ManualJournal;
manualJournalDTO: IManualJournalDTO;
trx: Knex.Transaction;
}
export interface IManualJournalEditingPayload {
// tenantId: number;
oldManualJournal: ManualJournal;
manualJournalDTO: IManualJournalDTO;
trx: Knex.Transaction;
}
export interface IManualJournalCreatingPayload {
// tenantId: number;
manualJournalDTO: IManualJournalDTO;
trx: Knex.Transaction;
}
export interface IManualJournalEventCreatedPayload {
// tenantId: number;
manualJournal: ManualJournal;
// manualJournalId: number;
manualJournalDTO: IManualJournalDTO;
trx: Knex.Transaction;
}
export enum ManualJournalAction {
Create = 'Create',
View = 'View',
Edit = 'Edit',
Delete = 'Delete',
}