mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
// };
|
||||
}
|
||||
@@ -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'
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
64
packages/server/src/modules/ManualJournals/constants.ts
Normal file
64
packages/server/src/modules/ManualJournals/constants.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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());
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
Reference in New Issue
Block a user