mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
83
packages/server/src/modules/Expenses/Expenses.controller.ts
Normal file
83
packages/server/src/modules/Expenses/Expenses.controller.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ExpensesApplication } from './ExpensesApplication.service';
|
||||
import { IExpensesFilter } from './Expenses.types';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateExpenseDto, EditExpenseDto } from './dtos/Expense.dto';
|
||||
|
||||
@Controller('expenses')
|
||||
@ApiTags('expenses')
|
||||
export class ExpensesController {
|
||||
constructor(private readonly expensesApplication: ExpensesApplication) {}
|
||||
|
||||
/**
|
||||
* Create a new expense transaction.
|
||||
* @param {IExpenseCreateDTO} expenseDTO
|
||||
*/
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new expense transaction.' })
|
||||
public createExpense(@Body() expenseDTO: CreateExpenseDto) {
|
||||
return this.expensesApplication.createExpense(expenseDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the given expense transaction.
|
||||
* @param {number} expenseId
|
||||
* @param {IExpenseEditDTO} expenseDTO
|
||||
*/
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Edit the given expense transaction.' })
|
||||
public editExpense(
|
||||
@Param('id') expenseId: number,
|
||||
@Body() expenseDTO: EditExpenseDto,
|
||||
) {
|
||||
return this.expensesApplication.editExpense(expenseId, expenseDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given expense transaction.
|
||||
* @param {number} expenseId
|
||||
*/
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete the given expense transaction.' })
|
||||
public deleteExpense(@Param('id') expenseId: number) {
|
||||
return this.expensesApplication.deleteExpense(expenseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the given expense transaction.
|
||||
* @param {number} expenseId
|
||||
*/
|
||||
@Post(':id/publish')
|
||||
@ApiOperation({ summary: 'Publish the given expense transaction.' })
|
||||
public publishExpense(@Param('id') expenseId: number) {
|
||||
return this.expensesApplication.publishExpense(expenseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expense transaction details.
|
||||
*/
|
||||
@Get('')
|
||||
@ApiOperation({ summary: 'Get the expense transaction details.' })
|
||||
public getExpenses(@Query() filterDTO: IExpensesFilter) {
|
||||
return this.expensesApplication.getExpenses(filterDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expense transaction details.
|
||||
* @param {number} expenseId
|
||||
*/
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get the expense transaction details.' })
|
||||
public getExpense(@Param('id') expenseId: number) {
|
||||
return this.expensesApplication.getExpense(expenseId);
|
||||
}
|
||||
}
|
||||
42
packages/server/src/modules/Expenses/Expenses.module.ts
Normal file
42
packages/server/src/modules/Expenses/Expenses.module.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CreateExpense } from './commands/CreateExpense.service';
|
||||
import { DeleteExpense } from './commands/DeleteExpense.service';
|
||||
import { EditExpense } from './commands/EditExpense.service';
|
||||
import { PublishExpense } from './commands/PublishExpense.service';
|
||||
import { ExpensesController } from './Expenses.controller';
|
||||
import { ExpensesApplication } from './ExpensesApplication.service';
|
||||
import { GetExpenseService } from './queries/GetExpense.service';
|
||||
import { ExpenseDTOTransformer } from './commands/CommandExpenseDTO.transformer';
|
||||
import { CommandExpenseValidator } from './commands/CommandExpenseValidator.service';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { ExpensesWriteGLSubscriber } from './subscribers/ExpenseGLEntries.subscriber';
|
||||
import { ExpenseGLEntriesStorageService } from './subscribers/ExpenseGLEntriesStorage.sevice';
|
||||
import { ExpenseGLEntriesService } from './subscribers/ExpenseGLEntries.service';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
import { BranchesModule } from '../Branches/Branches.module';
|
||||
import { GetExpensesService } from './queries/GetExpenses.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
|
||||
@Module({
|
||||
imports: [LedgerModule, BranchesModule, DynamicListModule],
|
||||
controllers: [ExpensesController],
|
||||
exports: [CreateExpense],
|
||||
providers: [
|
||||
CreateExpense,
|
||||
ExpenseDTOTransformer,
|
||||
CommandExpenseValidator,
|
||||
EditExpense,
|
||||
DeleteExpense,
|
||||
PublishExpense,
|
||||
GetExpenseService,
|
||||
ExpensesApplication,
|
||||
TenancyContext,
|
||||
TransformerInjectable,
|
||||
ExpensesWriteGLSubscriber,
|
||||
ExpenseGLEntriesStorageService,
|
||||
ExpenseGLEntriesService,
|
||||
GetExpensesService,
|
||||
],
|
||||
})
|
||||
export class ExpensesModule {}
|
||||
79
packages/server/src/modules/Expenses/Expenses.types.ts
Normal file
79
packages/server/src/modules/Expenses/Expenses.types.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Expense } from './models/Expense.model';
|
||||
import { SystemUser } from '../System/models/SystemUser';
|
||||
import { IFilterRole } from '../DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
import { CreateExpenseDto, EditExpenseDto } from './dtos/Expense.dto';
|
||||
import { CreateExpense } from './commands/CreateExpense.service';
|
||||
|
||||
export interface IPaginationMeta {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface IExpensesFilter {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
filterRoles?: IFilterRole[];
|
||||
columnSortBy: string;
|
||||
sortOrder: string;
|
||||
viewSlug?: string;
|
||||
filterQuery?: (query: any) => void;
|
||||
}
|
||||
|
||||
export interface IExpenseCreatingPayload {
|
||||
trx: Knex.Transaction;
|
||||
expenseDTO: CreateExpenseDto;
|
||||
}
|
||||
|
||||
export interface IExpenseEventEditingPayload {
|
||||
oldExpense: Expense;
|
||||
expenseDTO: EditExpenseDto;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseCreatedPayload {
|
||||
expenseId: number;
|
||||
expense: Expense;
|
||||
expenseDTO: CreateExpenseDto;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseEventEditPayload {
|
||||
expenseId: number;
|
||||
expense: Expense;
|
||||
expenseDTO: EditExpenseDto;
|
||||
authorizedUser: SystemUser;
|
||||
oldExpense: Expense;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseEventDeletePayload {
|
||||
expenseId: number;
|
||||
authorizedUser: SystemUser;
|
||||
oldExpense: Expense;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseDeletingPayload {
|
||||
trx: Knex.Transaction;
|
||||
oldExpense: Expense;
|
||||
}
|
||||
export interface IExpenseEventPublishedPayload {
|
||||
expenseId: number;
|
||||
oldExpense: Expense;
|
||||
expense: Expense;
|
||||
authorizedUser: SystemUser;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpensePublishingPayload {
|
||||
trx: Knex.Transaction;
|
||||
oldExpense: Expense;
|
||||
}
|
||||
export enum ExpenseAction {
|
||||
Create = 'Create',
|
||||
Edit = 'Edit',
|
||||
Delete = 'Delete',
|
||||
View = 'View',
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateExpense } from './commands/CreateExpense.service';
|
||||
import { EditExpense } from './commands/EditExpense.service';
|
||||
import { DeleteExpense } from './commands/DeleteExpense.service';
|
||||
import { PublishExpense } from './commands/PublishExpense.service';
|
||||
import { GetExpenseService } from './queries/GetExpense.service';
|
||||
import { IExpensesFilter } from './interfaces/Expenses.interface';
|
||||
import { GetExpensesService } from './queries/GetExpenses.service';
|
||||
import { CreateExpenseDto, EditExpenseDto } from './dtos/Expense.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ExpensesApplication {
|
||||
constructor(
|
||||
private readonly createExpenseService: CreateExpense,
|
||||
private readonly editExpenseService: EditExpense,
|
||||
private readonly deleteExpenseService: DeleteExpense,
|
||||
private readonly publishExpenseService: PublishExpense,
|
||||
private readonly getExpenseService: GetExpenseService,
|
||||
private readonly getExpensesService: GetExpensesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new expense transaction.
|
||||
* @param {CreateExpenseDto} expenseDTO
|
||||
* @returns {Promise<Expense>}
|
||||
*/
|
||||
public createExpense(expenseDTO: CreateExpenseDto) {
|
||||
return this.createExpenseService.newExpense(expenseDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given expense transaction.
|
||||
* @param {number} expenseId - Expense id.
|
||||
* @param {EditExpenseDto} expenseDTO
|
||||
* @returns {Promise<Expense>}
|
||||
*/
|
||||
public editExpense(expenseId: number, expenseDTO: EditExpenseDto) {
|
||||
return this.editExpenseService.editExpense(expenseId, expenseDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given expense.
|
||||
* @param {number} expenseId - Expense id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteExpense(expenseId: number) {
|
||||
return this.deleteExpenseService.deleteExpense(expenseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the given expense.
|
||||
* @param {number} expenseId - Expense id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public publishExpense(expenseId: number) {
|
||||
return this.publishExpenseService.publishExpense(expenseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given expense details.
|
||||
* @param {number} expenseId -Expense id.
|
||||
* @return {Promise<Expense>}
|
||||
*/
|
||||
public getExpense(expenseId: number) {
|
||||
return this.getExpenseService.getExpense(expenseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve expenses paginated list.
|
||||
* @param {IExpensesFilter} expensesFilter
|
||||
*/
|
||||
public getExpenses(filterDTO: IExpensesFilter) {
|
||||
return this.getExpensesService.getExpensesList(filterDTO);
|
||||
}
|
||||
}
|
||||
34
packages/server/src/modules/Expenses/ExpensesExportable.ts
Normal file
34
packages/server/src/modules/Expenses/ExpensesExportable.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { Exportable } from '../Export/Exportable';
|
||||
// import { IExpensesFilter } from '@/interfaces';
|
||||
// import { ExpensesApplication } from './ExpensesApplication.service';
|
||||
// import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||
|
||||
// @Service()
|
||||
// export class ExpensesExportable extends Exportable {
|
||||
// @Inject()
|
||||
// private expensesApplication: ExpensesApplication;
|
||||
|
||||
// /**
|
||||
// * Retrieves the accounts data to exportable sheet.
|
||||
// * @param {number} tenantId
|
||||
// * @returns
|
||||
// */
|
||||
// public exportable(tenantId: number, query: IExpensesFilter) {
|
||||
// const filterQuery = (query) => {
|
||||
// query.withGraphFetched('branch');
|
||||
// };
|
||||
// const parsedQuery = {
|
||||
// sortOrder: 'desc',
|
||||
// columnSortBy: 'created_at',
|
||||
// ...query,
|
||||
// page: 1,
|
||||
// pageSize: EXPORT_SIZE_LIMIT,
|
||||
// filterQuery,
|
||||
// } as IExpensesFilter;
|
||||
|
||||
// return this.expensesApplication
|
||||
// .getExpenses(tenantId, parsedQuery)
|
||||
// .then((output) => output.expenses);
|
||||
// }
|
||||
// }
|
||||
46
packages/server/src/modules/Expenses/ExpensesImportable.ts
Normal file
46
packages/server/src/modules/Expenses/ExpensesImportable.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { Knex } from 'knex';
|
||||
// import { IExpenseCreateDTO } from '@/interfaces';
|
||||
// import { Importable } from '../Import/Importable';
|
||||
// import { CreateExpense } from './CRUD/CreateExpense.service';
|
||||
// import { ExpensesSampleData } from './constants';
|
||||
|
||||
// @Service()
|
||||
// export class ExpensesImportable extends Importable {
|
||||
// @Inject()
|
||||
// private createExpenseService: CreateExpense;
|
||||
|
||||
// /**
|
||||
// * Importing to account service.
|
||||
// * @param {number} tenantId
|
||||
// * @param {IAccountCreateDTO} createAccountDTO
|
||||
// * @returns
|
||||
// */
|
||||
// public importable(
|
||||
// tenantId: number,
|
||||
// createAccountDTO: IExpenseCreateDTO,
|
||||
// trx?: Knex.Transaction
|
||||
// ) {
|
||||
// return this.createExpenseService.newExpense(
|
||||
// tenantId,
|
||||
// createAccountDTO,
|
||||
// {},
|
||||
// trx
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Concurrrency controlling of the importing process.
|
||||
// * @returns {number}
|
||||
// */
|
||||
// public get concurrency() {
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Retrieves the sample data that used to download accounts sample sheet.
|
||||
// */
|
||||
// public sampleData(): any[] {
|
||||
// return ExpensesSampleData;
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import * as composeAsync from 'async/compose';
|
||||
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { CreateExpenseDto, EditExpenseDto } from '../dtos/Expense.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ExpenseDTOTransformer {
|
||||
/**
|
||||
* @param {BranchTransactionDTOTransformer} branchDTOTransform - Branch transaction DTO transformer.
|
||||
* @param {TenancyContext} tenancyContext - Tenancy context.
|
||||
*/
|
||||
constructor(
|
||||
private readonly branchDTOTransform: BranchTransactionDTOTransformer,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the expense landed cost amount.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @return {number}
|
||||
*/
|
||||
private getExpenseLandedCostAmount = (
|
||||
expenseDTO: CreateExpenseDto | EditExpenseDto,
|
||||
): number => {
|
||||
const landedCostEntries = expenseDTO.categories.filter((entry) => {
|
||||
return entry.landedCost === true;
|
||||
});
|
||||
return this.getExpenseCategoriesTotal(landedCostEntries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the given expense categories total.
|
||||
* @param {IExpenseCategory} categories
|
||||
* @returns {number}
|
||||
*/
|
||||
private getExpenseCategoriesTotal = (categories): number => {
|
||||
return sumBy(categories, 'amount');
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping expense DTO to model.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @return {IExpense}
|
||||
*/
|
||||
private async expenseDTOToModel(
|
||||
expenseDTO: CreateExpenseDto | EditExpenseDto,
|
||||
): Promise<Expense> {
|
||||
const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO);
|
||||
const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories);
|
||||
|
||||
const categories = R.compose(
|
||||
// Associate the default index to categories lines.
|
||||
assocItemEntriesDefaultIndex,
|
||||
)(expenseDTO.categories || []);
|
||||
|
||||
const initialDTO = {
|
||||
...omit(expenseDTO, ['publish', 'attachments']),
|
||||
categories,
|
||||
totalAmount,
|
||||
landedCostAmount,
|
||||
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),
|
||||
...(expenseDTO.publish
|
||||
? {
|
||||
publishedAt: moment().toMySqlDateTime(),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const asyncDto = await composeAsync(
|
||||
this.branchDTOTransform.transformDTO<Expense>,
|
||||
)(initialDTO);
|
||||
|
||||
return asyncDto as Expense;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the expense create DTO.
|
||||
* @param {IExpenseCreateDTO} expenseDTO
|
||||
* @returns {Promise<Expense>}
|
||||
*/
|
||||
public expenseCreateDTO = async (
|
||||
expenseDTO: CreateExpenseDto | EditExpenseDto,
|
||||
): Promise<Partial<Expense>> => {
|
||||
const initialDTO = await this.expenseDTOToModel(expenseDTO);
|
||||
const tenant = await this.tenancyContext.getTenant(true);
|
||||
|
||||
return {
|
||||
...initialDTO,
|
||||
currencyCode: expenseDTO.currencyCode || tenant?.metadata?.baseCurrency,
|
||||
exchangeRate: expenseDTO.exchangeRate || 1,
|
||||
// ...(user
|
||||
// ? {
|
||||
// userId: user.id,
|
||||
// }
|
||||
// : {}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the expense edit DTO.
|
||||
* @param {EditExpenseDto} expenseDTO
|
||||
* @returns {Promise<Expense>}
|
||||
*/
|
||||
public expenseEditDTO = async (
|
||||
expenseDTO: EditExpenseDto,
|
||||
): Promise<Expense> => {
|
||||
return this.expenseDTOToModel(expenseDTO);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { sumBy, difference } from 'lodash';
|
||||
import { ERRORS, SUPPORTED_EXPENSE_PAYMENT_ACCOUNT_TYPES } from '../constants';
|
||||
import { ACCOUNT_ROOT_TYPE } from '@/constants/accounts';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { CreateExpenseDto, EditExpenseDto } from '../dtos/Expense.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CommandExpenseValidator {
|
||||
/**
|
||||
* Validates expense categories not equals zero.
|
||||
* @param {IExpenseCreateDTO | IExpenseEditDTO} expenseDTO
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public validateCategoriesNotEqualZero = (
|
||||
expenseDTO: CreateExpenseDto | EditExpenseDto,
|
||||
) => {
|
||||
const totalAmount = sumBy(expenseDTO.categories, 'amount') || 0;
|
||||
|
||||
if (totalAmount <= 0) {
|
||||
throw new ServiceError(ERRORS.TOTAL_AMOUNT_EQUALS_ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve expense accounts or throw error in case one of the given accounts
|
||||
* not found not the storage.
|
||||
* @param {number} expenseAccountsIds
|
||||
* @throws {ServiceError}
|
||||
* @returns {Promise<IAccount[]>}
|
||||
*/
|
||||
public validateExpensesAccountsExistance(
|
||||
expenseAccounts: Account[],
|
||||
DTOAccountsIds: number[],
|
||||
) {
|
||||
const storedExpenseAccountsIds = expenseAccounts.map((a: Account) => a.id);
|
||||
const notStoredAccountsIds = difference(
|
||||
DTOAccountsIds,
|
||||
storedExpenseAccountsIds,
|
||||
);
|
||||
if (notStoredAccountsIds.length > 0) {
|
||||
throw new ServiceError(ERRORS.SOME_ACCOUNTS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate expenses accounts type.
|
||||
* @param {Account[]} expensesAccounts
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public validateExpensesAccountsType = (expensesAccounts: Account[]) => {
|
||||
const invalidExpenseAccounts: number[] = [];
|
||||
|
||||
expensesAccounts.forEach((expenseAccount) => {
|
||||
if (!expenseAccount.isRootType(ACCOUNT_ROOT_TYPE.EXPENSE)) {
|
||||
invalidExpenseAccounts.push(expenseAccount.id);
|
||||
}
|
||||
});
|
||||
if (invalidExpenseAccounts.length > 0) {
|
||||
throw new ServiceError(ERRORS.EXPENSES_ACCOUNT_HAS_INVALID_TYPE);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates payment account type in case has invalid type throws errors.
|
||||
* @param {Account} paymentAccount
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public validatePaymentAccountType = (paymentAccount: Account) => {
|
||||
if (
|
||||
!paymentAccount.isAccountType(SUPPORTED_EXPENSE_PAYMENT_ACCOUNT_TYPES)
|
||||
) {
|
||||
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_HAS_INVALID_TYPE);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the expense has not associated landed cost
|
||||
* references to the given expense.
|
||||
* @param {number} expenseId
|
||||
*/
|
||||
public async validateNoAssociatedLandedCost(expenseId: number) {
|
||||
// const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||
// const associatedLandedCosts = await BillLandedCost.query()
|
||||
// .where('fromTransactionType', 'Expense')
|
||||
// .where('fromTransactionId', expenseId);
|
||||
// if (associatedLandedCosts.length > 0) {
|
||||
// throw new ServiceError(ERRORS.EXPENSE_HAS_ASSOCIATED_LANDED_COST);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates expenses is not already published before.
|
||||
* @param {IExpense} expense
|
||||
*/
|
||||
public validateExpenseIsNotPublished(expense: Expense) {
|
||||
if (expense.publishedAt) {
|
||||
throw new ServiceError(ERRORS.EXPENSE_ALREADY_PUBLISHED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IExpenseCreatedPayload,
|
||||
IExpenseCreatingPayload,
|
||||
} from '../interfaces/Expenses.interface';
|
||||
import { CommandExpenseValidator } from './CommandExpenseValidator.service';
|
||||
import { ExpenseDTOTransformer } from './CommandExpenseDTO.transformer';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { Expense } from '@/modules/Expenses/models/Expense.model';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { CreateExpenseDto } from '../dtos/Expense.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CreateExpense {
|
||||
/**
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter.
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {CommandExpenseValidator} validator - Command expense validator.
|
||||
* @param {ExpenseDTOTransformer} transformDTO - Expense DTO transformer.
|
||||
* @param {typeof Account} accountModel - Account model.
|
||||
* @param {typeof Expense} expenseModel - Expense model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandExpenseValidator,
|
||||
private readonly transformDTO: ExpenseDTOTransformer,
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
|
||||
@Inject(Expense.name)
|
||||
private readonly expenseModel: TenantModelProxy<typeof Expense>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize before create a new expense transaction.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
*/
|
||||
private authorize = async (expenseDTO: CreateExpenseDto) => {
|
||||
// Validate payment account existance on the storage.
|
||||
const paymentAccount = await this.accountModel()
|
||||
.query()
|
||||
.findById(expenseDTO.paymentAccountId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Retrieves the DTO expense accounts ids.
|
||||
const DTOExpenseAccountsIds = expenseDTO.categories.map(
|
||||
(category) => category.expenseAccountId,
|
||||
);
|
||||
// Retrieves the expenses accounts.
|
||||
const expenseAccounts = await this.accountModel()
|
||||
.query()
|
||||
.whereIn('id', DTOExpenseAccountsIds);
|
||||
// Validate expense accounts exist on the storage.
|
||||
this.validator.validateExpensesAccountsExistance(
|
||||
expenseAccounts,
|
||||
DTOExpenseAccountsIds,
|
||||
);
|
||||
// Validate payment account type.
|
||||
this.validator.validatePaymentAccountType(paymentAccount);
|
||||
|
||||
// Validate expenses accounts type.
|
||||
this.validator.validateExpensesAccountsType(expenseAccounts);
|
||||
|
||||
// Validate the given expense categories not equal zero.
|
||||
this.validator.validateCategoriesNotEqualZero(expenseDTO);
|
||||
};
|
||||
|
||||
/**
|
||||
* Precedures.
|
||||
* ---------
|
||||
* 1. Validate payment account existance on the storage.
|
||||
* 2. Validate expense accounts exist on the storage.
|
||||
* 3. Validate payment account type.
|
||||
* 4. Validate expenses accounts type.
|
||||
* 5. Validate the expense payee contact id existance on storage.
|
||||
* 6. Validate the given expense categories not equal zero.
|
||||
* 7. Stores the expense to the storage.
|
||||
* ---------
|
||||
* @param {number} tenantId
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
*/
|
||||
public newExpense = async (
|
||||
expenseDTO: CreateExpenseDto,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<Expense> => {
|
||||
// Authorize before create a new expense.
|
||||
await this.authorize(expenseDTO);
|
||||
|
||||
// Save the expense to the storage.
|
||||
const expenseObj = await this.transformDTO.expenseCreateDTO(expenseDTO);
|
||||
|
||||
// Writes the expense transaction with associated transactions under
|
||||
// unit-of-work envirement.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onExpenseCreating` event.
|
||||
await this.eventEmitter.emitAsync(events.expenses.onCreating, {
|
||||
trx,
|
||||
expenseDTO,
|
||||
} as IExpenseCreatingPayload);
|
||||
|
||||
// Creates a new expense transaction graph.
|
||||
const expense = await this.expenseModel()
|
||||
.query(trx)
|
||||
.upsertGraph(expenseObj);
|
||||
// Triggers `onExpenseCreated` event.
|
||||
await this.eventEmitter.emitAsync(events.expenses.onCreated, {
|
||||
expenseId: expense.id,
|
||||
expenseDTO,
|
||||
expense,
|
||||
trx,
|
||||
} as IExpenseCreatedPayload);
|
||||
|
||||
return expense;
|
||||
}, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { CommandExpenseValidator } from './CommandExpenseValidator.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { ExpenseCategory } from '../models/ExpenseCategory.model';
|
||||
import { events } from '@/common/events/events';
|
||||
import {
|
||||
IExpenseEventDeletePayload,
|
||||
IExpenseDeletingPayload,
|
||||
} from '../interfaces/Expenses.interface';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteExpense {
|
||||
/**
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter.
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {CommandExpenseValidator} validator - Command expense validator.
|
||||
* @param {TenantModelProxy<typeof Expense>} expenseModel - Expense model.
|
||||
* @param {TenantModelProxy<typeof ExpenseCategory>} expenseCategoryModel - Expense category model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandExpenseValidator,
|
||||
|
||||
@Inject(Expense.name)
|
||||
private expenseModel: TenantModelProxy<typeof Expense>,
|
||||
|
||||
@Inject(ExpenseCategory.name)
|
||||
private expenseCategoryModel: TenantModelProxy<typeof ExpenseCategory>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Deletes the given expense.
|
||||
* @param {number} expenseId
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
public async deleteExpense(expenseId: number): Promise<void> {
|
||||
// Retrieves the expense transaction with associated entries or
|
||||
// throw not found error.
|
||||
const oldExpense = await this.expenseModel()
|
||||
.query()
|
||||
.findById(expenseId)
|
||||
.withGraphFetched('categories')
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validates the expense has no associated landed cost.
|
||||
await this.validator.validateNoAssociatedLandedCost(expenseId);
|
||||
|
||||
// Deletes expense transactions with associated transactions under
|
||||
// unit-of-work envirement.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onExpenseDeleting` event.
|
||||
await this.eventEmitter.emitAsync(events.expenses.onDeleting, {
|
||||
trx,
|
||||
oldExpense,
|
||||
} as IExpenseDeletingPayload);
|
||||
|
||||
// Deletes expense associated entries.
|
||||
await this.expenseCategoryModel()
|
||||
.query(trx)
|
||||
.where('expenseId', expenseId)
|
||||
.delete();
|
||||
|
||||
// Deletes expense transactions.
|
||||
await this.expenseModel().query(trx).findById(expenseId).delete();
|
||||
|
||||
// Triggers `onExpenseDeleted` event.
|
||||
await this.eventEmitter.emitAsync(events.expenses.onDeleted, {
|
||||
expenseId,
|
||||
oldExpense,
|
||||
trx,
|
||||
} as IExpenseEventDeletePayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IExpenseEventEditPayload,
|
||||
IExpenseEventEditingPayload,
|
||||
} from '../interfaces/Expenses.interface';
|
||||
import { CommandExpenseValidator } from './CommandExpenseValidator.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { ExpenseDTOTransformer } from './CommandExpenseDTO.transformer';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { EditExpenseDto } from '../dtos/Expense.dto';
|
||||
|
||||
@Injectable()
|
||||
export class EditExpense {
|
||||
/**
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter.
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {CommandExpenseValidator} validator - Command expense validator.
|
||||
* @param {ExpenseDTOTransformer} transformDTO - Expense DTO transformer.
|
||||
* @param {typeof Expense} expenseModel - Expense model.
|
||||
* @param {typeof Account} accountModel - Account model.
|
||||
*/
|
||||
constructor(
|
||||
private eventEmitter: EventEmitter2,
|
||||
private uow: UnitOfWork,
|
||||
private validator: CommandExpenseValidator,
|
||||
private transformDTO: ExpenseDTOTransformer,
|
||||
@Inject(Expense.name)
|
||||
private expenseModel: TenantModelProxy<typeof Expense>,
|
||||
|
||||
@Inject(Account.name)
|
||||
private accountModel: TenantModelProxy<typeof Account>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize the DTO before editing expense transaction.
|
||||
* @param {EditExpenseDto} expenseDTO
|
||||
*/
|
||||
public authorize = async (
|
||||
oldExpense: Expense,
|
||||
expenseDTO: EditExpenseDto,
|
||||
) => {
|
||||
// Validate payment account existance on the storage.
|
||||
const paymentAccount = await this.accountModel()
|
||||
.query()
|
||||
.findById(expenseDTO.paymentAccountId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Retrieves the DTO expense accounts ids.
|
||||
const DTOExpenseAccountsIds = expenseDTO.categories.map(
|
||||
(category) => category.expenseAccountId,
|
||||
);
|
||||
// Retrieves the expenses accounts.
|
||||
const expenseAccounts = await this.accountModel()
|
||||
.query()
|
||||
.whereIn('id', DTOExpenseAccountsIds);
|
||||
// Validate expense accounts exist on the storage.
|
||||
this.validator.validateExpensesAccountsExistance(
|
||||
expenseAccounts,
|
||||
DTOExpenseAccountsIds,
|
||||
);
|
||||
// Validate payment account type.
|
||||
await this.validator.validatePaymentAccountType(paymentAccount);
|
||||
|
||||
// Validate expenses accounts type.
|
||||
await this.validator.validateExpensesAccountsType(expenseAccounts);
|
||||
// Validate the given expense categories not equal zero.
|
||||
this.validator.validateCategoriesNotEqualZero(expenseDTO);
|
||||
|
||||
// Validate expense entries that have allocated landed cost cannot be deleted.
|
||||
// this.entriesService.validateLandedCostEntriesNotDeleted(
|
||||
// oldExpense.categories,
|
||||
// expenseDTO.categories,
|
||||
// );
|
||||
// // Validate expense entries that have allocated cost amount should be bigger than amount.
|
||||
// this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries(
|
||||
// oldExpense.categories,
|
||||
// expenseDTO.categories,
|
||||
// );
|
||||
};
|
||||
|
||||
/**
|
||||
* Precedures.
|
||||
* ---------
|
||||
* 1. Validate expense existance.
|
||||
* 2. Validate payment account existance on the storage.
|
||||
* 3. Validate expense accounts exist on the storage.
|
||||
* 4. Validate payment account type.
|
||||
* 5. Validate expenses accounts type.
|
||||
* 6. Validate the given expense categories not equal zero.
|
||||
* 7. Stores the expense to the storage.
|
||||
* ---------
|
||||
* @param {number} expenseId
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
public async editExpense(
|
||||
expenseId: number,
|
||||
expenseDTO: EditExpenseDto,
|
||||
): Promise<Expense> {
|
||||
// Retrieves the expense model or throw not found error.
|
||||
const oldExpense = await this.expenseModel()
|
||||
.query()
|
||||
.findById(expenseId)
|
||||
.withGraphFetched('categories')
|
||||
.throwIfNotFound();
|
||||
|
||||
// Authorize expense DTO before editing.
|
||||
await this.authorize(oldExpense, expenseDTO);
|
||||
|
||||
// Update the expense on the storage.
|
||||
const expenseObj = await this.transformDTO.expenseEditDTO(expenseDTO);
|
||||
|
||||
// Edits expense transactions and associated transactions under UOW envirement.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onExpenseEditing` event.
|
||||
await this.eventEmitter.emitAsync(events.expenses.onEditing, {
|
||||
oldExpense,
|
||||
expenseDTO,
|
||||
trx,
|
||||
} as IExpenseEventEditingPayload);
|
||||
|
||||
// Upsert the expense object with expense entries.
|
||||
const expense = await this.expenseModel()
|
||||
.query(trx)
|
||||
.upsertGraphAndFetch({
|
||||
id: expenseId,
|
||||
...expenseObj,
|
||||
});
|
||||
|
||||
// Triggers `onExpenseCreated` event.
|
||||
await this.eventEmitter.emitAsync(events.expenses.onEdited, {
|
||||
expenseId,
|
||||
expense,
|
||||
expenseDTO,
|
||||
oldExpense,
|
||||
trx,
|
||||
} as IExpenseEventEditPayload);
|
||||
|
||||
return expense;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IExpensePublishingPayload,
|
||||
IExpenseEventPublishedPayload,
|
||||
} from '../interfaces/Expenses.interface';
|
||||
import { CommandExpenseValidator } from './CommandExpenseValidator.service';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class PublishExpense {
|
||||
/**
|
||||
* @param {EventEmitter2} eventPublisher - Event emitter.
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {CommandExpenseValidator} validator - Command expense validator.
|
||||
* @param {typeof Expense} expenseModel - Expense model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandExpenseValidator,
|
||||
|
||||
@Inject(Expense.name)
|
||||
private readonly expenseModel: TenantModelProxy<typeof Expense>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Publish the given expense.
|
||||
* @param {number} expenseId
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async publishExpense(expenseId: number) {
|
||||
// Retrieves the old expense or throw not found error.
|
||||
const oldExpense = await this.expenseModel()
|
||||
.query()
|
||||
.findById(expenseId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate the expense whether is published before.
|
||||
this.validator.validateExpenseIsNotPublished(oldExpense);
|
||||
|
||||
// Publishes expense transactions with associated transactions
|
||||
// under unit-of-work envirement.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Trigggers `onExpensePublishing` event.
|
||||
await this.eventPublisher.emitAsync(events.expenses.onPublishing, {
|
||||
trx,
|
||||
oldExpense,
|
||||
} as IExpensePublishingPayload);
|
||||
|
||||
// Publish the given expense on the storage.
|
||||
await this.expenseModel().query().findById(expenseId).modify('publish');
|
||||
|
||||
// Retrieve the new expense after modification.
|
||||
const expense = await this.expenseModel()
|
||||
.query()
|
||||
.findById(expenseId)
|
||||
.withGraphFetched('categories');
|
||||
|
||||
// Triggers `onExpensePublished` event.
|
||||
await this.eventPublisher.emitAsync(events.expenses.onPublished, {
|
||||
expenseId,
|
||||
oldExpense,
|
||||
expense,
|
||||
trx,
|
||||
} as IExpenseEventPublishedPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
89
packages/server/src/modules/Expenses/constants.ts
Normal file
89
packages/server/src/modules/Expenses/constants.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { ACCOUNT_TYPE } from "@/constants/accounts";
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
name: 'Draft',
|
||||
slug: 'draft',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Published',
|
||||
slug: 'published',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'published',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
];
|
||||
|
||||
export const ERRORS = {
|
||||
EXPENSE_NOT_FOUND: 'expense_not_found',
|
||||
EXPENSES_NOT_FOUND: 'EXPENSES_NOT_FOUND',
|
||||
PAYMENT_ACCOUNT_NOT_FOUND: 'payment_account_not_found',
|
||||
SOME_ACCOUNTS_NOT_FOUND: 'some_expenses_not_found',
|
||||
TOTAL_AMOUNT_EQUALS_ZERO: 'total_amount_equals_zero',
|
||||
PAYMENT_ACCOUNT_HAS_INVALID_TYPE: 'payment_account_has_invalid_type',
|
||||
EXPENSES_ACCOUNT_HAS_INVALID_TYPE: 'expenses_account_has_invalid_type',
|
||||
EXPENSE_ALREADY_PUBLISHED: 'expense_already_published',
|
||||
EXPENSE_HAS_ASSOCIATED_LANDED_COST: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST',
|
||||
};
|
||||
|
||||
export const ExpensesSampleData = [
|
||||
{
|
||||
'Payment Date': '2024-03-01',
|
||||
'Reference No.': 'REF-1',
|
||||
'Payment Account': 'Petty Cash',
|
||||
Description: 'Vel et dolorem architecto veniam.',
|
||||
'Currency Code': '',
|
||||
'Exchange Rate': '',
|
||||
'Expense Account': 'Utilities Expense',
|
||||
Amount: 9000,
|
||||
'Line Description': 'Voluptates voluptas corporis vel.',
|
||||
Publish: 'T',
|
||||
},
|
||||
{
|
||||
'Payment Date': '2024-03-02',
|
||||
'Reference No.': 'REF-2',
|
||||
'Payment Account': 'Petty Cash',
|
||||
Description: 'Id est molestias.',
|
||||
'Currency Code': '',
|
||||
'Exchange Rate': '',
|
||||
'Expense Account': 'Utilities Expense',
|
||||
Amount: 9000,
|
||||
'Line Description': 'Eos voluptatem cumque et voluptate reiciendis.',
|
||||
Publish: 'T',
|
||||
},
|
||||
{
|
||||
'Payment Date': '2024-03-03',
|
||||
'Reference No.': 'REF-3',
|
||||
'Payment Account': 'Petty Cash',
|
||||
Description: 'Quam cupiditate at nihil dicta dignissimos non fugit illo.',
|
||||
'Currency Code': '',
|
||||
'Exchange Rate': '',
|
||||
'Expense Account': 'Utilities Expense',
|
||||
Amount: 9000,
|
||||
'Line Description':
|
||||
'Hic alias rerum sed commodi dolores sint animi perferendis.',
|
||||
Publish: 'T',
|
||||
},
|
||||
];
|
||||
|
||||
export const SUPPORTED_EXPENSE_PAYMENT_ACCOUNT_TYPES = [
|
||||
ACCOUNT_TYPE.CASH,
|
||||
ACCOUNT_TYPE.BANK,
|
||||
ACCOUNT_TYPE.CREDIT_CARD,
|
||||
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
|
||||
ACCOUNT_TYPE.NON_CURRENT_ASSET,
|
||||
ACCOUNT_TYPE.FIXED_ASSET,
|
||||
];
|
||||
166
packages/server/src/modules/Expenses/dtos/Expense.dto.ts
Normal file
166
packages/server/src/modules/Expenses/dtos/Expense.dto.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
class AttachmentDto {
|
||||
@IsString()
|
||||
key: string;
|
||||
}
|
||||
|
||||
export class ExpenseCategoryDto {
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
index: number;
|
||||
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
expenseAccountId: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
amount?: number;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
landedCost?: boolean;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
projectId?: number;
|
||||
}
|
||||
|
||||
export class CommandExpenseDto {
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The reference number of the expense',
|
||||
example: 'INV-123456',
|
||||
})
|
||||
referenceNo?: string;
|
||||
|
||||
@IsDate()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
description: 'The payment date of the expense',
|
||||
example: '2021-01-01',
|
||||
})
|
||||
paymentDate: Date;
|
||||
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
description: 'The payment account id of the expense',
|
||||
example: 1,
|
||||
})
|
||||
paymentAccountId: number;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(1000)
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The description of the expense',
|
||||
example: 'This is a description',
|
||||
})
|
||||
description?: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The exchange rate of the expense',
|
||||
example: 1,
|
||||
})
|
||||
exchangeRate?: number;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(3)
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The currency code of the expense',
|
||||
example: 'USD',
|
||||
})
|
||||
currencyCode?: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The exchange rate of the expense',
|
||||
example: 1,
|
||||
})
|
||||
exchange_rate?: number;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The publish status of the expense',
|
||||
example: true,
|
||||
})
|
||||
publish?: boolean;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The payee id of the expense',
|
||||
example: 1,
|
||||
})
|
||||
payeeId?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The branch id of the expense',
|
||||
example: 1,
|
||||
})
|
||||
branchId?: number;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ExpenseCategoryDto)
|
||||
@ApiProperty({
|
||||
description: 'The categories of the expense',
|
||||
example: [
|
||||
{
|
||||
index: 1,
|
||||
expenseAccountId: 1,
|
||||
amount: 100,
|
||||
description: 'This is a description',
|
||||
landedCost: true,
|
||||
projectId: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
categories: ExpenseCategoryDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AttachmentDto)
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'The attachments of the expense',
|
||||
example: [
|
||||
{
|
||||
key: '123456',
|
||||
},
|
||||
],
|
||||
})
|
||||
attachments?: AttachmentDto[];
|
||||
}
|
||||
|
||||
export class CreateExpenseDto extends CommandExpenseDto {}
|
||||
export class EditExpenseDto extends CommandExpenseDto {}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { IFilterRole } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
import { Knex } from 'knex';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { CreateExpenseDto, EditExpenseDto } from '../dtos/Expense.dto';
|
||||
|
||||
// import { AttachmentLinkDTO } from '../Attachments/Attachments';
|
||||
|
||||
export interface IPaginationMeta {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface IExpensesFilter {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
filterRoles?: IFilterRole[];
|
||||
columnSortBy: string;
|
||||
sortOrder: string;
|
||||
viewSlug?: string;
|
||||
filterQuery?: (query: any) => void;
|
||||
}
|
||||
|
||||
export interface IExpenseCategoryDTO {
|
||||
id?: number;
|
||||
expenseAccountId: number;
|
||||
index: number;
|
||||
amount: number;
|
||||
description?: string;
|
||||
expenseId: number;
|
||||
landedCost?: boolean;
|
||||
projectId?: number;
|
||||
}
|
||||
|
||||
export interface IExpenseCreatingPayload {
|
||||
expenseDTO: CreateExpenseDto;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseEventEditingPayload {
|
||||
oldExpense: Expense;
|
||||
expenseDTO: EditExpenseDto;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface IExpenseCreatedPayload {
|
||||
expenseId: number;
|
||||
expense: Expense;
|
||||
expenseDTO: CreateExpenseDto;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseEventEditPayload {
|
||||
expenseId: number;
|
||||
expense: Expense;
|
||||
expenseDTO: EditExpenseDto;
|
||||
oldExpense: Expense;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseEventDeletePayload {
|
||||
expenseId: number;
|
||||
oldExpense: Expense;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseDeletingPayload {
|
||||
trx: Knex.Transaction;
|
||||
oldExpense: Expense;
|
||||
}
|
||||
export interface IExpenseEventPublishedPayload {
|
||||
expenseId: number;
|
||||
oldExpense: Expense;
|
||||
expense: Expense;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpensePublishingPayload {
|
||||
trx: Knex.Transaction;
|
||||
oldExpense: Expense;
|
||||
tenantId: number;
|
||||
}
|
||||
export enum ExpenseAction {
|
||||
Create = 'Create',
|
||||
Edit = 'Edit',
|
||||
Delete = 'Delete',
|
||||
View = 'View',
|
||||
}
|
||||
303
packages/server/src/modules/Expenses/models/Expense.model.ts
Normal file
303
packages/server/src/modules/Expenses/models/Expense.model.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import { Model, raw } from 'objection';
|
||||
import * as moment from 'moment';
|
||||
import { ExpenseCategory } from './ExpenseCategory.model';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
export class Expense extends TenantBaseModel {
|
||||
totalAmount!: number;
|
||||
currencyCode!: string;
|
||||
exchangeRate!: number;
|
||||
description?: string;
|
||||
paymentAccountId!: number;
|
||||
peyeeId!: number;
|
||||
referenceNo!: string;
|
||||
publishedAt!: Date | null;
|
||||
userId!: number;
|
||||
paymentDate!: Date;
|
||||
payeeId!: number;
|
||||
landedCostAmount!: number;
|
||||
allocatedCostAmount!: number;
|
||||
invoicedAmount: number;
|
||||
branchId!: number;
|
||||
createdAt!: Date;
|
||||
|
||||
categories!: ExpenseCategory[];
|
||||
paymentAccount!: Account;
|
||||
attachments!: Document[];
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'expenses_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Account transaction reference type.
|
||||
*/
|
||||
static get referenceType() {
|
||||
return 'Expense';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'isPublished',
|
||||
'unallocatedCostAmount',
|
||||
'localAmount',
|
||||
'localLandedCostAmount',
|
||||
'localUnallocatedCostAmount',
|
||||
'localAllocatedCostAmount',
|
||||
'billableAmount',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the local amount of expense.
|
||||
* @returns {number}
|
||||
*/
|
||||
get localAmount() {
|
||||
return this.totalAmount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rertieves the local landed cost amount of expense.
|
||||
* @returns {number}
|
||||
*/
|
||||
get localLandedCostAmount() {
|
||||
return this.landedCostAmount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the local allocated cost amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get localAllocatedCostAmount() {
|
||||
return this.allocatedCostAmount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the unallocated cost amount.
|
||||
* @return {number}
|
||||
*/
|
||||
get unallocatedCostAmount() {
|
||||
return Math.max(this.totalAmount - this.allocatedCostAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the local unallocated cost amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get localUnallocatedCostAmount() {
|
||||
return this.unallocatedCostAmount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the expense is published.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isPublished() {
|
||||
return Boolean(this.publishedAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the calculated amount which have not been invoiced.
|
||||
*/
|
||||
get billableAmount() {
|
||||
return Math.max(this.totalAmount - this.invoicedAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
filterByDateRange(query, startDate, endDate) {
|
||||
if (startDate) {
|
||||
query.where('date', '>=', startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
query.where('date', '<=', endDate);
|
||||
}
|
||||
},
|
||||
filterByAmountRange(query, from, to) {
|
||||
if (from) {
|
||||
query.where('amount', '>=', from);
|
||||
}
|
||||
if (to) {
|
||||
query.where('amount', '<=', to);
|
||||
}
|
||||
},
|
||||
filterByExpenseAccount(query, accountId) {
|
||||
if (accountId) {
|
||||
query.where('expense_account_id', accountId);
|
||||
}
|
||||
},
|
||||
filterByPaymentAccount(query, accountId) {
|
||||
if (accountId) {
|
||||
query.where('payment_account_id', accountId);
|
||||
}
|
||||
},
|
||||
// viewRolesBuilder(query, conditionals, expression) {
|
||||
// viewRolesBuilder(conditionals, expression)(query);
|
||||
// },
|
||||
|
||||
filterByDraft(query) {
|
||||
query.where('published_at', null);
|
||||
},
|
||||
|
||||
filterByPublished(query) {
|
||||
query.whereNot('published_at', null);
|
||||
},
|
||||
|
||||
filterByStatus(query, status) {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
query.modify('filterByDraft');
|
||||
break;
|
||||
case 'published':
|
||||
default:
|
||||
query.modify('filterByPublished');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
publish(query) {
|
||||
query.update({
|
||||
publishedAt: moment().toMySqlDateTime(),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters the expenses have billable amount.
|
||||
*/
|
||||
billable(query) {
|
||||
query.where(raw('AMOUNT > INVOICED_AMOUNT'));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { Account } = require('../../Accounts/models/Account.model');
|
||||
const { ExpenseCategory } = require('./ExpenseCategory.model');
|
||||
const { Document } = require('../../ChromiumlyTenancy/models/Document');
|
||||
const { Branch } = require('../../Branches/models/Branch.model');
|
||||
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Expense transaction may belongs to a payment account.
|
||||
*/
|
||||
paymentAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account,
|
||||
join: {
|
||||
from: 'expenses_transactions.paymentAccountId',
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Expense transaction may has many expense categories.
|
||||
*/
|
||||
categories: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: ExpenseCategory,
|
||||
join: {
|
||||
from: 'expenses_transactions.id',
|
||||
to: 'expense_transaction_categories.expenseId',
|
||||
},
|
||||
filter: (query) => {
|
||||
query.orderBy('index', 'ASC');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Expense transction may belongs to a branch.
|
||||
*/
|
||||
branch: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Branch,
|
||||
join: {
|
||||
from: 'expenses_transactions.branchId',
|
||||
to: 'branches.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Expense transaction may has many attached attachments.
|
||||
*/
|
||||
attachments: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: Document,
|
||||
join: {
|
||||
from: 'expenses_transactions.id',
|
||||
through: {
|
||||
from: 'document_links.modelId',
|
||||
to: 'document_links.documentId',
|
||||
},
|
||||
to: 'documents.id',
|
||||
},
|
||||
filter(query) {
|
||||
query.where('model_ref', 'Expense');
|
||||
},
|
||||
},
|
||||
|
||||
// /**
|
||||
// * Expense may belongs to matched bank transaction.
|
||||
// */
|
||||
// matchedBankTransaction: {
|
||||
// relation: Model.HasManyRelation,
|
||||
// modelClass: MatchedBankTransaction,
|
||||
// join: {
|
||||
// from: 'expenses_transactions.id',
|
||||
// to: 'matched_bank_transactions.referenceId',
|
||||
// },
|
||||
// filter(query) {
|
||||
// query.where('reference_type', 'Expense');
|
||||
// },
|
||||
// },
|
||||
};
|
||||
}
|
||||
|
||||
// static get meta() {
|
||||
// return ExpenseSettings;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Retrieve the default custom views, roles and columns.
|
||||
// */
|
||||
// static get defaultViews() {
|
||||
// return DEFAULT_VIEWS;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Model search attributes.
|
||||
*/
|
||||
static get searchRoles() {
|
||||
return [
|
||||
{ fieldKey: 'reference_no', 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,50 @@
|
||||
import { Model } from 'objection';
|
||||
import { BaseModel } from '@/models/Model';
|
||||
|
||||
export class ExpenseCategory extends BaseModel {
|
||||
public amount!: number;
|
||||
public allocatedCostAmount!: number;
|
||||
public expenseAccountId!: number;
|
||||
public projectId!: number;
|
||||
public description!: string;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'expense_transaction_categories';
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['unallocatedCostAmount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remain unallocated landed cost.
|
||||
* @return {number}
|
||||
*/
|
||||
get unallocatedCostAmount() {
|
||||
return Math.max(this.amount - this.allocatedCostAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { Account } = require('../../Accounts/models/Account.model');
|
||||
|
||||
return {
|
||||
expenseAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account,
|
||||
join: {
|
||||
from: 'expense_transaction_categories.expenseAccountId',
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
import { ExpenseCategoryTransformer } from './ExpenseCategory.transformer';
|
||||
// import { AttachmentTransformer } from '@/services/Attachments/AttachmentTransformer';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer';
|
||||
|
||||
export class ExpenseTransfromer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to expense object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedAmount',
|
||||
'formattedLandedCostAmount',
|
||||
'formattedAllocatedCostAmount',
|
||||
'formattedDate',
|
||||
'formattedCreatedAt',
|
||||
'formattedPublishedAt',
|
||||
'categories',
|
||||
'attachments',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted expense amount.
|
||||
* @param {Expense} expense - Expense.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (expense: Expense): string => {
|
||||
return this.formatNumber(expense.totalAmount, {
|
||||
currencyCode: expense.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted expense landed cost amount.
|
||||
* @param {Expense} expense - Expense.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedLandedCostAmount = (expense: Expense): string => {
|
||||
return this.formatNumber(expense.landedCostAmount, {
|
||||
currencyCode: expense.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted allocated cost amount.
|
||||
* @param {Expense} expense - Expense.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAllocatedCostAmount = (expense: Expense): string => {
|
||||
return this.formatNumber(expense.allocatedCostAmount, {
|
||||
currencyCode: expense.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retriecve fromatted date.
|
||||
* @param {Expense} expense - Expense.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDate = (expense: Expense): string => {
|
||||
return this.formatDate(expense.paymentDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted created at date.
|
||||
* @param {Expense} expense - Expense.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCreatedAt = (expense: Expense): string => {
|
||||
return this.formatDate(expense.createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the transformed expense categories.
|
||||
* @param {Expense} expense - Expense.
|
||||
* @returns
|
||||
*/
|
||||
protected categories = (expense: Expense) => {
|
||||
return this.item(expense.categories, new ExpenseCategoryTransformer(), {
|
||||
currencyCode: expense.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the sale invoice attachments.
|
||||
* @param {Expense} expense - Expense.
|
||||
* @returns
|
||||
*/
|
||||
protected attachments = (expense: Expense) => {
|
||||
return this.item(expense.attachments, new AttachmentTransformer());
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted published at date.
|
||||
* @param {Expense} expense - Expense.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedPublishedAt = (expense: Expense): string => {
|
||||
return this.formatDate(expense.publishedAt);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
import { ExpenseCategory } from '../models/ExpenseCategory.model';
|
||||
|
||||
export class ExpenseCategoryTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to expense object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['amountFormatted'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted amount.
|
||||
* @param {ExpenseCategory} category
|
||||
* @returns {string}
|
||||
*/
|
||||
protected amountFormatted(category: ExpenseCategory) {
|
||||
return this.formatNumber(category.amount, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
money: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ExpenseTransfromer } from './Expense.transformer';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetExpenseService {
|
||||
constructor(
|
||||
private readonly transformerService: TransformerInjectable,
|
||||
|
||||
@Inject(Expense.name)
|
||||
private readonly expenseModel: TenantModelProxy<typeof Expense>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve expense details.
|
||||
* @param {number} expenseId
|
||||
* @return {Promise<IExpense>}
|
||||
*/
|
||||
public async getExpense(expenseId: number): Promise<Expense> {
|
||||
const expense = await this.expenseModel()
|
||||
.query()
|
||||
.findById(expenseId)
|
||||
.withGraphFetched('categories.expenseAccount')
|
||||
.withGraphFetched('paymentAccount')
|
||||
.withGraphFetched('branch')
|
||||
.withGraphFetched('attachments')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformerService.transform(expense, new ExpenseTransfromer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as R from 'ramda';
|
||||
import { ExpenseTransfromer } from './Expense.transformer';
|
||||
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IExpensesFilter, IPaginationMeta } from '../Expenses.types';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { IFilterMeta } from '@/interfaces/Model';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetExpensesService {
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
private readonly dynamicListService: DynamicListService,
|
||||
|
||||
@Inject(Expense.name)
|
||||
private readonly expense: TenantModelProxy<typeof Expense>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve expenses paginated list.
|
||||
* @param {IExpensesFilter} expensesFilter
|
||||
* @return {IExpense[]}
|
||||
*/
|
||||
public async getExpensesList(filterDTO: IExpensesFilter): Promise<{
|
||||
expenses: Expense[];
|
||||
pagination: IPaginationMeta;
|
||||
filterMeta: IFilterMeta;
|
||||
}> {
|
||||
// Parses list filter DTO.
|
||||
const filter = this.parseListFilterDTO(filterDTO);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicList = await this.dynamicListService.dynamicList(
|
||||
this.expense(),
|
||||
filter,
|
||||
);
|
||||
// Retrieves the paginated results.
|
||||
const { results, pagination } = await this.expense()
|
||||
.query()
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('paymentAccount');
|
||||
builder.withGraphFetched('categories.expenseAccount');
|
||||
|
||||
dynamicList.buildQuery()(builder);
|
||||
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
// Transformes the expenses models to POJO.
|
||||
const expenses = await this.transformer.transform(
|
||||
results,
|
||||
new ExpenseTransfromer(),
|
||||
);
|
||||
return {
|
||||
expenses,
|
||||
pagination,
|
||||
filterMeta: dynamicList.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses filter DTO of expenses list.
|
||||
* @param filterDTO -
|
||||
*/
|
||||
private parseListFilterDTO(filterDTO) {
|
||||
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
|
||||
}
|
||||
}
|
||||
106
packages/server/src/modules/Expenses/subscribers/ExpenseGL.ts
Normal file
106
packages/server/src/modules/Expenses/subscribers/ExpenseGL.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import * as R from 'ramda';
|
||||
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { AccountNormal } from '@/modules/Accounts/Accounts.types';
|
||||
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { ExpenseCategory } from '../models/ExpenseCategory.model';
|
||||
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
|
||||
export class ExpenseGL {
|
||||
private expense: Expense;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {Expense} expense - Expense.
|
||||
*/
|
||||
constructor(expense: Expense) {
|
||||
this.expense = expense;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL common entry.
|
||||
*/
|
||||
private getExpenseGLCommonEntry = () => {
|
||||
return {
|
||||
currencyCode: this.expense.currencyCode,
|
||||
exchangeRate: this.expense.exchangeRate,
|
||||
|
||||
transactionType: 'Expense',
|
||||
transactionId: this.expense.id,
|
||||
|
||||
date: this.expense.paymentDate,
|
||||
userId: this.expense.userId,
|
||||
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
|
||||
branchId: this.expense.branchId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL payment entry.
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getExpenseGLPaymentEntry = (): ILedgerEntry => {
|
||||
const commonEntry = this.getExpenseGLCommonEntry();
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: this.expense.localAmount,
|
||||
accountId: this.expense.paymentAccountId,
|
||||
accountNormal:
|
||||
this.expense?.paymentAccount?.accountNormal === 'debit'
|
||||
? AccountNormal.DEBIT
|
||||
: AccountNormal.CREDIT,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL category entry.
|
||||
* @param {ExpenseCategory} category - Expense category.
|
||||
* @param {number} index
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getExpenseGLCategoryEntry = R.curry(
|
||||
(category: ExpenseCategory, index: number): ILedgerEntry => {
|
||||
const commonEntry = this.getExpenseGLCommonEntry();
|
||||
const localAmount = category.amount * this.expense.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
accountId: category.expenseAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
debit: localAmount,
|
||||
note: category.description,
|
||||
index: index + 2,
|
||||
projectId: category.projectId,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL entries.
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
public getExpenseGLEntries = (): ILedgerEntry[] => {
|
||||
const getCategoryEntry = this.getExpenseGLCategoryEntry();
|
||||
|
||||
const paymentEntry = this.getExpenseGLPaymentEntry();
|
||||
const categoryEntries = this.expense.categories.map((category, index) =>
|
||||
getCategoryEntry(category, index),
|
||||
);
|
||||
return [paymentEntry, ...categoryEntries];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given expense ledger.
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public getExpenseLedger = (): ILedger => {
|
||||
const entries = this.getExpenseGLEntries();
|
||||
|
||||
return new Ledger(entries);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Knex } from 'knex';
|
||||
import { ExpenseGL } from './ExpenseGL';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class ExpenseGLEntriesService {
|
||||
/**
|
||||
* @param {TenantModelProxy<typeof Expense>} expense - Expense model.
|
||||
*/
|
||||
constructor(
|
||||
@Inject(Expense.name)
|
||||
private readonly expense: TenantModelProxy<typeof Expense>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the expense G/L of the given id.
|
||||
* @param {number} expenseId
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @returns {Promise<ILedger>}
|
||||
*/
|
||||
public getExpenseLedgerById = async (
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<ILedger> => {
|
||||
const expense = await this.expense()
|
||||
.query(trx)
|
||||
.findById(expenseId)
|
||||
.withGraphFetched('categories')
|
||||
.withGraphFetched('paymentAccount')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.getExpenseLedger(expense);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given expense ledger.
|
||||
* @param {Expense} expense - Expense model.
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public getExpenseLedger = (expense: Expense): ILedger => {
|
||||
const expenseGL = new ExpenseGL(expense);
|
||||
|
||||
return expenseGL.getExpenseLedger();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
IExpenseCreatedPayload,
|
||||
IExpenseEventDeletePayload,
|
||||
IExpenseEventEditPayload,
|
||||
IExpenseEventPublishedPayload,
|
||||
} from '../Expenses.types';
|
||||
import { ExpenseGLEntriesStorageService } from './ExpenseGLEntriesStorage.sevice';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class ExpensesWriteGLSubscriber {
|
||||
/**
|
||||
* @param {ExpenseGLEntriesStorageService} expenseGLEntries - Expense GL entries storage service.
|
||||
*/
|
||||
constructor(
|
||||
private readonly expenseGLEntries: ExpenseGLEntriesStorageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handles the writing journal entries once the expense created.
|
||||
* @param {IExpenseCreatedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.expenses.onCreated)
|
||||
public async handleWriteGLEntriesOnceCreated({
|
||||
expense,
|
||||
trx,
|
||||
}: IExpenseCreatedPayload) {
|
||||
// In case expense published, write journal entries.
|
||||
if (!expense.publishedAt) return;
|
||||
|
||||
await this.expenseGLEntries.writeExpenseGLEntries(expense.id, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle writing expense journal entries once the expense edited.
|
||||
* @param {IExpenseEventEditPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.expenses.onEdited)
|
||||
public async handleRewriteGLEntriesOnceEdited({
|
||||
expenseId,
|
||||
expense,
|
||||
authorizedUser,
|
||||
trx,
|
||||
}: IExpenseEventEditPayload) {
|
||||
// Cannot continue if the expense is not published.
|
||||
if (!expense.publishedAt) return;
|
||||
|
||||
await this.expenseGLEntries.rewriteExpenseGLEntries(expense.id, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts expense journal entries once the expense deleted.
|
||||
* @param {IExpenseEventDeletePayload} payload -
|
||||
*/
|
||||
@OnEvent(events.expenses.onDeleted)
|
||||
public async handleRevertGLEntriesOnceDeleted({
|
||||
expenseId,
|
||||
trx,
|
||||
}: IExpenseEventDeletePayload) {
|
||||
await this.expenseGLEntries.revertExpenseGLEntries(expenseId, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles writing expense journal once the expense publish.
|
||||
* @param {IExpenseEventPublishedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.expenses.onPublished)
|
||||
public async handleWriteGLEntriesOncePublished({
|
||||
expense,
|
||||
trx,
|
||||
}: IExpenseEventPublishedPayload) {
|
||||
// In case expense published, write journal entries.
|
||||
if (!expense.publishedAt) return;
|
||||
|
||||
await this.expenseGLEntries.rewriteExpenseGLEntries(expense.id, trx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpenseGLEntriesService } from './ExpenseGLEntries.service';
|
||||
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
|
||||
|
||||
@Injectable()
|
||||
export class ExpenseGLEntriesStorageService {
|
||||
/**
|
||||
* @param {ExpenseGLEntriesService} expenseGLEntries - Expense GL entries service.
|
||||
* @param {LedgerStorageService} ledgerStorage - Ledger storage service.
|
||||
*/
|
||||
constructor(
|
||||
private readonly expenseGLEntries: ExpenseGLEntriesService,
|
||||
private readonly ledgerStorage: LedgerStorageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Writes the expense GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public writeExpenseGLEntries = async (
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
// Retrieves the given expense ledger.
|
||||
const expenseLedger = await this.expenseGLEntries.getExpenseLedgerById(
|
||||
expenseId,
|
||||
trx,
|
||||
);
|
||||
// Commits the expense ledger entries.
|
||||
await this.ledgerStorage.commit(expenseLedger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the given expense GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public revertExpenseGLEntries = async (
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
await this.ledgerStorage.deleteByReference(
|
||||
expenseId,
|
||||
'Expense',
|
||||
trx,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the expense GL entries.
|
||||
* @param {number} expenseId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public rewriteExpenseGLEntries = async (
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
// Reverts the expense GL entries.
|
||||
await this.revertExpenseGLEntries(expenseId, trx);
|
||||
|
||||
// Writes the expense GL entries.
|
||||
await this.writeExpenseGLEntries(expenseId, trx);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user