mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
refactor: migrate item categories module to nestjs
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Transformer } from '../Transformer/Transformer';
|
||||
import { AccountModel } from './models/Account.model';
|
||||
import { Account } from './models/Account.model';
|
||||
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
|
||||
import { assocDepthLevelToObjectTree } from '@/utils/assoc-depth-level-to-object-tree';
|
||||
import { nestedArrayToFlatten } from '@/utils/nested-array-to-flatten';
|
||||
@@ -35,7 +35,7 @@ export class AccountTransformer extends Transformer {
|
||||
* @param {IAccount} account -
|
||||
* @returns {string}
|
||||
*/
|
||||
public flattenName = (account: AccountModel): string => {
|
||||
public flattenName = (account: Account): string => {
|
||||
const parentDependantsIds = this.options.accountsGraph.dependantsOf(
|
||||
account.id,
|
||||
);
|
||||
@@ -51,7 +51,7 @@ export class AccountTransformer extends Transformer {
|
||||
* @param {IAccount} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (account: AccountModel): string => {
|
||||
protected formattedAmount = (account: Account): string => {
|
||||
return this.formatNumber(account.amount, {
|
||||
currencyCode: account.currencyCode,
|
||||
});
|
||||
@@ -59,10 +59,10 @@ export class AccountTransformer extends Transformer {
|
||||
|
||||
/**
|
||||
* Retrieves the formatted bank balance.
|
||||
* @param {AccountModel} account
|
||||
* @param {Account} account
|
||||
* @returns {string}
|
||||
*/
|
||||
protected bankBalanceFormatted = (account: AccountModel): string => {
|
||||
protected bankBalanceFormatted = (account: Account): string => {
|
||||
return this.formatNumber(account.bankBalance, {
|
||||
currencyCode: account.currencyCode,
|
||||
});
|
||||
@@ -73,7 +73,7 @@ export class AccountTransformer extends Transformer {
|
||||
* @param {IAccount} account
|
||||
* @returns {string}
|
||||
*/
|
||||
protected lastFeedsUpdatedAtFormatted = (account: AccountModel): string => {
|
||||
protected lastFeedsUpdatedAtFormatted = (account: Account): string => {
|
||||
return account.lastFeedsUpdatedAt
|
||||
? this.formatDate(account.lastFeedsUpdatedAt)
|
||||
: '';
|
||||
@@ -84,7 +84,7 @@ export class AccountTransformer extends Transformer {
|
||||
* @param account
|
||||
* @returns {boolean}
|
||||
*/
|
||||
protected isFeedsPaused = (account: AccountModel): boolean => {
|
||||
protected isFeedsPaused = (account: Account): boolean => {
|
||||
// return account.plaidItem?.isPaused || false;
|
||||
|
||||
return false;
|
||||
@@ -94,7 +94,7 @@ export class AccountTransformer extends Transformer {
|
||||
* Retrieves formatted account type label.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected accountTypeLabel = (account: AccountModel): string => {
|
||||
protected accountTypeLabel = (account: Account): string => {
|
||||
return this.context.i18n.t(account.accountTypeLabel);
|
||||
};
|
||||
|
||||
@@ -102,7 +102,7 @@ export class AccountTransformer extends Transformer {
|
||||
* Retrieves formatted account normal.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected accountNormalFormatted = (account: AccountModel): string => {
|
||||
protected accountNormalFormatted = (account: Account): string => {
|
||||
return this.context.i18n.t(account.accountNormalFormatted);
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ export class AccountTransformer extends Transformer {
|
||||
* @param {IAccount[]}
|
||||
* @returns {IAccount[]}
|
||||
*/
|
||||
protected postCollectionTransform = (accounts: AccountModel[]) => {
|
||||
protected postCollectionTransform = (accounts: Account[]) => {
|
||||
// Transfom the flatten to accounts tree.
|
||||
const transformed = flatToNestedArray(accounts, {
|
||||
id: 'id',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Knex } from 'knex';
|
||||
import { AccountModel } from './models/Account.model';
|
||||
import { Account } from './models/Account.model';
|
||||
// import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
|
||||
|
||||
export enum AccountNormal {
|
||||
@@ -42,26 +42,26 @@ export interface IAccountEventCreatingPayload {
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface IAccountEventCreatedPayload {
|
||||
account: AccountModel;
|
||||
account: Account;
|
||||
accountId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IAccountEventEditedPayload {
|
||||
account: AccountModel;
|
||||
oldAccount: AccountModel;
|
||||
account: Account;
|
||||
oldAccount: Account;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IAccountEventDeletedPayload {
|
||||
accountId: number;
|
||||
oldAccount: AccountModel;
|
||||
oldAccount: Account;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IAccountEventDeletePayload {
|
||||
trx: Knex.Transaction;
|
||||
oldAccount: AccountModel;
|
||||
oldAccount: Account;
|
||||
}
|
||||
|
||||
export interface IAccountEventActivatedPayload {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CreateAccountService } from './CreateAccount.service';
|
||||
import { DeleteAccount } from './DeleteAccount.service';
|
||||
import { EditAccount } from './EditAccount.service';
|
||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
import { AccountModel } from './models/Account.model';
|
||||
import { Account } from './models/Account.model';
|
||||
import { EditAccountDTO } from './EditAccount.dto';
|
||||
import { GetAccount } from './GetAccount.service';
|
||||
import { ActivateAccount } from './ActivateAccount.service';
|
||||
@@ -37,7 +37,7 @@ export class AccountsApplication {
|
||||
public createAccount = (
|
||||
accountDTO: CreateAccountDTO,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<AccountModel> => {
|
||||
): Promise<Account> => {
|
||||
return this.createAccountService.createAccount(accountDTO, trx);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { IAccountEventActivatedPayload } from './Accounts.types';
|
||||
import { AccountModel } from './models/Account.model';
|
||||
import { Account } from './models/Account.model';
|
||||
import { AccountRepository } from './repositories/Account.repository';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
@@ -13,8 +13,8 @@ export class ActivateAccount {
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
|
||||
@Inject(AccountModel.name)
|
||||
private readonly accountModel: typeof AccountModel,
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: typeof Account,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
// import AccountTypesUtils from '@/lib/AccountTypes';
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
import { ERRORS, MAX_ACCOUNTS_CHART_DEPTH } from './constants';
|
||||
import { AccountModel } from './models/Account.model';
|
||||
import { Account } from './models/Account.model';
|
||||
import { AccountRepository } from './repositories/Account.repository';
|
||||
import { AccountTypesUtils } from './utils/AccountType.utils';
|
||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
@@ -13,16 +13,16 @@ import { EditAccountDTO } from './EditAccount.dto';
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class CommandAccountValidators {
|
||||
constructor(
|
||||
@Inject(AccountModel.name)
|
||||
private readonly accountModel: typeof AccountModel,
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: typeof Account,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Throws error if the account was prefined.
|
||||
* @param {AccountModel} account
|
||||
* @param {Account} account
|
||||
*/
|
||||
public throwErrorIfAccountPredefined(account: AccountModel) {
|
||||
public throwErrorIfAccountPredefined(account: Account) {
|
||||
if (account.predefined) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_PREDEFINED);
|
||||
}
|
||||
@@ -31,12 +31,12 @@ export class CommandAccountValidators {
|
||||
/**
|
||||
* Diff account type between new and old account, throw service error
|
||||
* if they have different account type.
|
||||
* @param {AccountModel|CreateAccountDTO|EditAccountDTO} oldAccount
|
||||
* @param {AccountModel|CreateAccountDTO|EditAccountDTO} newAccount
|
||||
* @param {Account|CreateAccountDTO|EditAccountDTO} oldAccount
|
||||
* @param {Account|CreateAccountDTO|EditAccountDTO} newAccount
|
||||
*/
|
||||
public async isAccountTypeChangedOrThrowError(
|
||||
oldAccount: AccountModel | CreateAccountDTO | EditAccountDTO,
|
||||
newAccount: AccountModel | CreateAccountDTO | EditAccountDTO,
|
||||
oldAccount: Account | CreateAccountDTO | EditAccountDTO,
|
||||
newAccount: Account | CreateAccountDTO | EditAccountDTO,
|
||||
) {
|
||||
if (oldAccount.accountType !== newAccount.accountType) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE);
|
||||
@@ -155,13 +155,13 @@ export class CommandAccountValidators {
|
||||
* Validates the account DTO currency code whether equals the currency code of
|
||||
* parent account.
|
||||
* @param {CreateAccountDTO | EditAccountDTO} accountDTO
|
||||
* @param {AccountModel} parentAccount
|
||||
* @param {Account} parentAccount
|
||||
* @param {string} baseCurrency -
|
||||
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
|
||||
*/
|
||||
public validateCurrentSameParentAccount = (
|
||||
accountDTO: CreateAccountDTO | EditAccountDTO,
|
||||
parentAccount: AccountModel,
|
||||
parentAccount: Account,
|
||||
baseCurrency: string,
|
||||
) => {
|
||||
// If the account DTO currency not assigned and the parent account has no base currency.
|
||||
@@ -187,7 +187,7 @@ export class CommandAccountValidators {
|
||||
*/
|
||||
public throwErrorIfParentHasDiffType(
|
||||
accountDTO: CreateAccountDTO | EditAccountDTO,
|
||||
parentAccount: AccountModel,
|
||||
parentAccount: Account,
|
||||
) {
|
||||
if (accountDTO.accountType !== parentAccount.accountType) {
|
||||
throw new ServiceError(ERRORS.PARENT_ACCOUNT_HAS_DIFFERENT_TYPE);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
CreateAccountParams,
|
||||
} from './Accounts.types';
|
||||
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
||||
import { AccountModel } from './models/Account.model';
|
||||
import { Account } from './models/Account.model';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { events } from '@/common/events/events';
|
||||
@@ -20,8 +20,8 @@ import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
@Injectable()
|
||||
export class CreateAccountService {
|
||||
constructor(
|
||||
@Inject(AccountModel.name)
|
||||
private readonly accountModel: typeof AccountModel,
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: typeof Account,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandAccountValidators,
|
||||
@@ -102,7 +102,7 @@ export class CreateAccountService {
|
||||
accountDTO: CreateAccountDTO,
|
||||
trx?: Knex.Transaction,
|
||||
params: CreateAccountParams = { ignoreUniqueName: false },
|
||||
): Promise<AccountModel> => {
|
||||
): Promise<Account> => {
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenant = await this.tenancyContext.getTenant(true);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
// import { IAccountEventDeletedPayload } from '@/interfaces';
|
||||
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
||||
import { AccountModel } from './models/Account.model';
|
||||
import { Account } from './models/Account.model';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
@@ -11,7 +11,7 @@ import { IAccountEventDeletedPayload } from './Accounts.types';
|
||||
@Injectable()
|
||||
export class DeleteAccount {
|
||||
constructor(
|
||||
@Inject(AccountModel.name) private accountModel: typeof AccountModel,
|
||||
@Inject(Account.name) private accountModel: typeof Account,
|
||||
private eventEmitter: EventEmitter2,
|
||||
private uow: UnitOfWork,
|
||||
private validator: CommandAccountValidators,
|
||||
@@ -21,7 +21,7 @@ export class DeleteAccount {
|
||||
* Authorize account delete.
|
||||
* @param {number} accountId - Account id.
|
||||
*/
|
||||
private authorize = async (accountId: number, oldAccount: AccountModel) => {
|
||||
private authorize = async (accountId: number, oldAccount: Account) => {
|
||||
// Throw error if the account was predefined.
|
||||
this.validator.throwErrorIfAccountPredefined(oldAccount);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
||||
import { AccountModel } from './models/Account.model';
|
||||
import { Account } from './models/Account.model';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
@@ -14,8 +14,8 @@ export class EditAccount {
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandAccountValidators,
|
||||
|
||||
@Inject(AccountModel.name)
|
||||
private readonly accountModel: typeof AccountModel,
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: typeof Account,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -27,7 +27,7 @@ export class EditAccount {
|
||||
private authorize = async (
|
||||
accountId: number,
|
||||
accountDTO: EditAccountDTO,
|
||||
oldAccount: AccountModel,
|
||||
oldAccount: Account,
|
||||
) => {
|
||||
// Validate account name uniquiness.
|
||||
await this.validator.validateAccountNameUniquiness(
|
||||
@@ -64,7 +64,7 @@ export class EditAccount {
|
||||
public async editAccount(
|
||||
accountId: number,
|
||||
accountDTO: EditAccountDTO,
|
||||
): Promise<AccountModel> {
|
||||
): Promise<Account> {
|
||||
// Retrieve the old account or throw not found service error.
|
||||
const oldAccount = await this.accountModel
|
||||
.query()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { AccountTransformer } from './Account.transformer';
|
||||
import { AccountModel } from './models/Account.model';
|
||||
import { Account } from './models/Account.model';
|
||||
import { AccountRepository } from './repositories/Account.repository';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
@@ -9,8 +9,8 @@ import { events } from '@/common/events/events';
|
||||
@Injectable()
|
||||
export class GetAccount {
|
||||
constructor(
|
||||
@Inject(AccountModel.name)
|
||||
private readonly accountModel: typeof AccountModel,
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: typeof Account,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from './Accounts.types';
|
||||
import { AccountTransactionTransformer } from './AccountTransaction.transformer';
|
||||
import { AccountTransaction } from './models/AccountTransaction.model';
|
||||
import { AccountModel } from './models/Account.model';
|
||||
import { Account } from './models/Account.model';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
|
||||
@@ -16,8 +16,8 @@ export class GetAccountTransactionsService {
|
||||
@Inject(AccountTransaction.name)
|
||||
private readonly accountTransaction: typeof AccountTransaction,
|
||||
|
||||
@Inject(AccountModel.name)
|
||||
private readonly account: typeof AccountModel,
|
||||
@Inject(Account.name)
|
||||
private readonly account: typeof Account,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// import { DynamicListService } from '../DynamicListing/DynamicListService';
|
||||
// import { AccountTransformer } from './Account.transformer';
|
||||
// import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
// import { AccountModel } from './models/Account.model';
|
||||
// import { Account } from './models/Account.model';
|
||||
// import { AccountRepository } from './repositories/Account.repository';
|
||||
|
||||
// @Injectable()
|
||||
@@ -16,7 +16,7 @@
|
||||
// constructor(
|
||||
// private readonly dynamicListService: DynamicListService,
|
||||
// private readonly transformerService: TransformerInjectable,
|
||||
// private readonly accountModel: typeof AccountModel,
|
||||
// private readonly accountModel: typeof Account,
|
||||
// private readonly accountRepository: AccountRepository,
|
||||
// ) {}
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@ import { Model } from 'objection';
|
||||
// import { flatToNestedArray } from 'utils';
|
||||
|
||||
// @ts-expect-error
|
||||
// export class AccountModel extends mixin(TenantModel, [
|
||||
// export class Account extends mixin(TenantModel, [
|
||||
// ModelSettings,
|
||||
// CustomViewBaseModel,
|
||||
// SearchableModel,
|
||||
// ]) {
|
||||
|
||||
export class AccountModel extends TenantModel {
|
||||
export class Account extends TenantModel {
|
||||
name: string;
|
||||
slug: string;
|
||||
code: string;
|
||||
@@ -126,7 +126,7 @@ export class AccountModel extends TenantModel {
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
const TABLE_NAME = AccountModel.tableName;
|
||||
const TABLE_NAME = Account.tableName;
|
||||
|
||||
return {
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Knex } from 'knex';
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { TenantRepository } from '@/common/repository/TenantRepository';
|
||||
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
|
||||
import { AccountModel } from '../models/Account.model';
|
||||
import { Account } from '../models/Account.model';
|
||||
// import { TenantMetadata } from '@/modules/System/models/TenantMetadataModel';
|
||||
// import { IAccount } from '../Accounts.types';
|
||||
// import {
|
||||
@@ -20,8 +20,8 @@ export class AccountRepository extends TenantRepository {
|
||||
/**
|
||||
* Gets the repository's model.
|
||||
*/
|
||||
get model(): typeof AccountModel {
|
||||
return AccountModel.bindKnex(this.tenantDBKnex);
|
||||
get model(): typeof Account {
|
||||
return Account.bindKnex(this.tenantDBKnex);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,7 @@ import { TenancyGlobalMiddleware } from '../Tenancy/TenancyGlobal.middleware';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { TransformerModule } from '../Transformer/Transformer.module';
|
||||
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||
import { ExpensesModule } from '../Expenses/Expenses.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -86,7 +87,8 @@ import { AccountsModule } from '../Accounts/Accounts.module';
|
||||
TenancyDatabaseModule,
|
||||
TenancyModelsModule,
|
||||
ItemsModule,
|
||||
AccountsModule
|
||||
AccountsModule,
|
||||
ExpensesModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
} from '@nestjs/common';
|
||||
import { ExpensesApplication } from './ExpensesApplication.service';
|
||||
import {
|
||||
IExpenseCreateDTO,
|
||||
IExpenseEditDTO,
|
||||
} from './interfaces/Expenses.interface';
|
||||
|
||||
@Controller('expenses')
|
||||
export class ExpensesController {
|
||||
constructor(private readonly expensesApplication: ExpensesApplication) {}
|
||||
|
||||
/**
|
||||
* Create a new expense transaction.
|
||||
* @param {IExpenseCreateDTO} expenseDTO
|
||||
*/
|
||||
@Post()
|
||||
public createExpense(@Body() expenseDTO: IExpenseCreateDTO) {
|
||||
return this.expensesApplication.createExpense(expenseDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the given expense transaction.
|
||||
* @param {number} expenseId
|
||||
* @param {IExpenseEditDTO} expenseDTO
|
||||
*/
|
||||
@Put(':id')
|
||||
public editExpense(
|
||||
@Param('id') expenseId: number,
|
||||
@Body() expenseDTO: IExpenseEditDTO,
|
||||
) {
|
||||
return this.expensesApplication.editExpense(expenseId, expenseDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given expense transaction.
|
||||
* @param {number} expenseId
|
||||
*/
|
||||
@Delete(':id')
|
||||
public deleteExpense(@Param('id') expenseId: number) {
|
||||
return this.expensesApplication.deleteExpense(expenseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the given expense transaction.
|
||||
* @param {number} expenseId
|
||||
*/
|
||||
@Post(':id/publish')
|
||||
public publishExpense(@Param('id') expenseId: number) {
|
||||
return this.expensesApplication.publishExpense(expenseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expense transaction details.
|
||||
* @param {number} expenseId
|
||||
*/
|
||||
@Get(':id')
|
||||
public getExpense(@Param('id') expenseId: number) {
|
||||
return this.expensesApplication.getExpense(expenseId);
|
||||
}
|
||||
}
|
||||
22
packages/server-nest/src/modules/Expenses/Expenses.module.ts
Normal file
22
packages/server-nest/src/modules/Expenses/Expenses.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [ExpensesController],
|
||||
providers: [
|
||||
CreateExpense,
|
||||
EditExpense,
|
||||
DeleteExpense,
|
||||
PublishExpense,
|
||||
GetExpenseService,
|
||||
ExpensesApplication,
|
||||
],
|
||||
})
|
||||
export class ExpensesModule {}
|
||||
@@ -0,0 +1,77 @@
|
||||
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 {
|
||||
IExpenseCreateDTO,
|
||||
IExpenseEditDTO,
|
||||
} from './interfaces/Expenses.interface';
|
||||
|
||||
@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: GetExpenseService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new expense transaction.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @returns {Promise<Expense>}
|
||||
*/
|
||||
public createExpense = (expenseDTO: IExpenseCreateDTO) => {
|
||||
return this.createExpenseService.newExpense(expenseDTO);
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits the given expense transaction.
|
||||
* @param {number} expenseId - Expense id.
|
||||
* @param {IExpenseEditDTO} expenseDTO
|
||||
* @returns {Promise<Expense>}
|
||||
*/
|
||||
public editExpense = (expenseId: number, expenseDTO: IExpenseEditDTO) => {
|
||||
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 {number} tenantId
|
||||
// * @param {IExpensesFilter} expensesFilter
|
||||
// */
|
||||
// public getExpenses = (tenantId: number, filterDTO: IExpensesFilter) => {
|
||||
// return this.getExpensesService.getExpensesList(tenantId, filterDTO);
|
||||
// };
|
||||
}
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
@@ -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,109 @@
|
||||
import { sumBy, difference } from 'lodash';
|
||||
import { ERRORS, SUPPORTED_EXPENSE_PAYMENT_ACCOUNT_TYPES } from '../constants';
|
||||
import {
|
||||
IExpenseCreateDTO,
|
||||
IExpenseEditDTO,
|
||||
} from '../interfaces/Expenses.interface';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class CommandExpenseValidator {
|
||||
/**
|
||||
* Validates expense categories not equals zero.
|
||||
* @param {IExpenseCreateDTO | IExpenseEditDTO} expenseDTO
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public validateCategoriesNotEqualZero = (
|
||||
expenseDTO: IExpenseCreateDTO | IExpenseEditDTO,
|
||||
) => {
|
||||
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} tenantId
|
||||
* @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} tenantId
|
||||
* @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,113 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IExpenseCreateDTO,
|
||||
IExpenseCreatedPayload,
|
||||
IExpenseCreatingPayload,
|
||||
} from '../interfaces/Expenses.interface';
|
||||
import { CommandExpenseValidator } from './CommandExpenseValidator.service';
|
||||
import { ExpenseDTOTransformer } from './ExpenseDTOTransformer';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class CreateExpense {
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandExpenseValidator,
|
||||
private readonly transformDTO: ExpenseDTOTransformer,
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: typeof Account,
|
||||
|
||||
@Inject(Expense.name)
|
||||
private readonly expenseModel: typeof Expense,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize before create a new expense transaction.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
*/
|
||||
private authorize = async (expenseDTO: IExpenseCreateDTO) => {
|
||||
// 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: IExpenseCreateDTO,
|
||||
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,70 @@
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteExpense {
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandExpenseValidator,
|
||||
@Inject(Expense.name)
|
||||
private expenseModel: typeof Expense,
|
||||
|
||||
@Inject(ExpenseCategory.name)
|
||||
private expenseCategoryModel: 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,139 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IExpenseEventEditPayload,
|
||||
IExpenseEventEditingPayload,
|
||||
IExpenseEditDTO,
|
||||
} from '../interfaces/Expenses.interface';
|
||||
import { CommandExpenseValidator } from './CommandExpenseValidator.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { ExpenseDTOTransformer } from './ExpenseDTOTransformer';
|
||||
// import { EntriesService } from '@/services/Entries';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class EditExpense {
|
||||
constructor(
|
||||
private eventEmitter: EventEmitter2,
|
||||
private uow: UnitOfWork,
|
||||
private validator: CommandExpenseValidator,
|
||||
private transformDTO: ExpenseDTOTransformer,
|
||||
// private entriesService: EntriesService,
|
||||
@Inject(Expense.name)
|
||||
private expenseModel: typeof Expense,
|
||||
@Inject(Account.name)
|
||||
private accountModel: typeof Account,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize the DTO before editing expense transaction.
|
||||
* @param {IExpenseEditDTO} expenseDTO
|
||||
*/
|
||||
public authorize = async (
|
||||
oldExpense: Expense,
|
||||
expenseDTO: IExpenseEditDTO,
|
||||
) => {
|
||||
// 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: IExpenseEditDTO,
|
||||
): 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,117 @@
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IExpenseCreateDTO,
|
||||
IExpenseCommonDTO,
|
||||
IExpenseEditDTO,
|
||||
} from '../interfaces/Expenses.interface';
|
||||
// import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
// import { TenantMetadata } from '@/system/models';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
|
||||
@Injectable()
|
||||
export class ExpenseDTOTransformer {
|
||||
constructor(
|
||||
// private readonly branchDTOTransform: BranchTransactionDTOTransform;
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the expense landed cost amount.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @return {number}
|
||||
*/
|
||||
private getExpenseLandedCostAmount = (
|
||||
expenseDTO: IExpenseCreateDTO | IExpenseEditDTO,
|
||||
): 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 expenseDTOToModel(
|
||||
expenseDTO: IExpenseCreateDTO | IExpenseEditDTO,
|
||||
// user?: ISystemUser
|
||||
): 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(),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
return initialDTO;
|
||||
// return R.compose(this.branchDTOTransform.transformDTO<IExpense>(tenantId))(
|
||||
// initialDTO
|
||||
// );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the expense create DTO.
|
||||
* @param {IExpenseCreateDTO} expenseDTO
|
||||
* @returns {Promise<Expense>}
|
||||
*/
|
||||
public expenseCreateDTO = async (
|
||||
expenseDTO: IExpenseCreateDTO,
|
||||
): Promise<Expense> => {
|
||||
const initialDTO = 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 {number} tenantId
|
||||
* @param {IExpenseEditDTO} expenseDTO
|
||||
* @param {ISystemUser} user
|
||||
* @returns {IExpense}
|
||||
*/
|
||||
public expenseEditDTO = async (
|
||||
expenseDTO: IExpenseEditDTO,
|
||||
): Promise<Expense> => {
|
||||
return this.expenseDTOToModel(expenseDTO);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class PublishExpense {
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandExpenseValidator,
|
||||
|
||||
@Inject(Expense.name)
|
||||
private readonly expenseModel: 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-nest/src/modules/Expenses/constants.ts
Normal file
89
packages/server-nest/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,
|
||||
];
|
||||
@@ -0,0 +1,117 @@
|
||||
import { IFilterRole } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
import { Knex } from 'knex';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
|
||||
// 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 IExpenseCommonDTO {
|
||||
currencyCode: string;
|
||||
exchangeRate?: number;
|
||||
description?: string;
|
||||
paymentAccountId: number;
|
||||
peyeeId?: number;
|
||||
referenceNo?: string;
|
||||
publish: boolean;
|
||||
userId: number;
|
||||
paymentDate: Date;
|
||||
payeeId: number;
|
||||
categories: IExpenseCategoryDTO[];
|
||||
|
||||
branchId?: number;
|
||||
// attachments?: AttachmentLinkDTO[];
|
||||
}
|
||||
|
||||
export interface IExpenseCreateDTO extends IExpenseCommonDTO {}
|
||||
export interface IExpenseEditDTO extends IExpenseCommonDTO {}
|
||||
|
||||
export interface IExpenseCategoryDTO {
|
||||
id?: number;
|
||||
expenseAccountId: number;
|
||||
index: number;
|
||||
amount: number;
|
||||
description?: string;
|
||||
expenseId: number;
|
||||
landedCost?: boolean;
|
||||
projectId?: number;
|
||||
}
|
||||
|
||||
export interface IExpenseCreatingPayload {
|
||||
trx: Knex.Transaction;
|
||||
tenantId: number;
|
||||
expenseDTO: IExpenseCreateDTO;
|
||||
}
|
||||
|
||||
export interface IExpenseEventEditingPayload {
|
||||
tenantId: number;
|
||||
oldExpense: Expense;
|
||||
expenseDTO: IExpenseEditDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface IExpenseCreatedPayload {
|
||||
tenantId: number;
|
||||
expenseId: number;
|
||||
// authorizedUser: ISystemUser;
|
||||
expense: Expense;
|
||||
expenseDTO: IExpenseCreateDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseEventEditPayload {
|
||||
tenantId: number;
|
||||
expenseId: number;
|
||||
expense: Expense;
|
||||
expenseDTO: IExpenseEditDTO;
|
||||
// authorizedUser: ISystemUser;
|
||||
oldExpense: Expense;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseEventDeletePayload {
|
||||
tenantId: number;
|
||||
expenseId: number;
|
||||
// authorizedUser: ISystemUser;
|
||||
oldExpense: Expense;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpenseDeletingPayload {
|
||||
trx: Knex.Transaction;
|
||||
tenantId: number;
|
||||
oldExpense: Expense;
|
||||
}
|
||||
export interface IExpenseEventPublishedPayload {
|
||||
tenantId: number;
|
||||
expenseId: number;
|
||||
oldExpense: Expense;
|
||||
expense: Expense;
|
||||
// authorizedUser: ISystemUser;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IExpensePublishingPayload {
|
||||
trx: Knex.Transaction;
|
||||
oldExpense: Expense;
|
||||
tenantId: number;
|
||||
}
|
||||
export enum ExpenseAction {
|
||||
Create = 'Create',
|
||||
Edit = 'Edit',
|
||||
Delete = 'Delete',
|
||||
View = 'View',
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import { Model, mixin, raw } from 'objection';
|
||||
// import TenantModel from 'models/TenantModel';
|
||||
// import { viewRolesBuilder } from '@/lib/ViewRolesBuilder';
|
||||
// import ModelSetting from './ModelSetting';
|
||||
// import ExpenseSettings from './Expense.Settings';
|
||||
// import CustomViewBaseModel from './CustomViewBaseModel';
|
||||
// import { DEFAULT_VIEWS } from '@/services/Expenses/constants';
|
||||
// import ModelSearchable from './ModelSearchable';
|
||||
import moment from 'moment';
|
||||
import { BaseModel } from '@/models/Model';
|
||||
|
||||
export class Expense extends BaseModel {
|
||||
// ModelSetting,
|
||||
// CustomViewBaseModel,
|
||||
// ModelSearchable,
|
||||
// ]) {
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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('models/Account');
|
||||
const { ExpenseCategory } = require('./ExpenseCategory.model');
|
||||
// const Document = require('models/Document');
|
||||
// const Branch = require('models/Branch');
|
||||
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
|
||||
|
||||
return {
|
||||
// paymentAccount: {
|
||||
// relation: Model.BelongsToOneRelation,
|
||||
// modelClass: Account.default,
|
||||
// join: {
|
||||
// from: 'expenses_transactions.paymentAccountId',
|
||||
// to: 'accounts.id',
|
||||
// },
|
||||
// },
|
||||
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.default,
|
||||
// join: {
|
||||
// from: 'expenses_transactions.branchId',
|
||||
// to: 'branches.id',
|
||||
// },
|
||||
// },
|
||||
|
||||
// /**
|
||||
// * Expense transaction may has many attached attachments.
|
||||
// */
|
||||
// attachments: {
|
||||
// relation: Model.ManyToManyRelation,
|
||||
// modelClass: Document.default,
|
||||
// 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,47 @@
|
||||
import { Model } from 'objection';
|
||||
import { BaseModel } from '@/models/Model';
|
||||
|
||||
export default class ExpenseCategory extends BaseModel {
|
||||
amount!: number;
|
||||
allocatedCostAmount!: number;
|
||||
|
||||
/**
|
||||
* 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('models/Account');
|
||||
|
||||
return {
|
||||
expenseAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account.default,
|
||||
join: {
|
||||
from: 'expense_transaction_categories.expenseAccountId',
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
import { ExpenseCategoryTransformer } from './ExpenseCategory.transformer';
|
||||
// import { AttachmentTransformer } from '@/services/Attachments/AttachmentTransformer';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
|
||||
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 {IExpense} expense
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (expense: Expense): string => {
|
||||
return this.formatNumber(expense.totalAmount, {
|
||||
currencyCode: expense.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted expense landed cost amount.
|
||||
* @param {IExpense} expense
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedLandedCostAmount = (expense: Expense): string => {
|
||||
return this.formatNumber(expense.landedCostAmount, {
|
||||
currencyCode: expense.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted allocated cost amount.
|
||||
* @param {IExpense} expense
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAllocatedCostAmount = (expense: Expense): string => {
|
||||
return this.formatNumber(expense.allocatedCostAmount, {
|
||||
currencyCode: expense.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retriecve fromatted date.
|
||||
* @param {IExpense} expense
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDate = (expense: Expense): string => {
|
||||
return this.formatDate(expense.paymentDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted created at date.
|
||||
* @param {IExpense} expense
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCreatedAt = (expense: Expense): string => {
|
||||
return this.formatDate(expense.createdAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the transformed expense categories.
|
||||
* @param {IExpense} expense
|
||||
* @returns {}
|
||||
*/
|
||||
protected categories = (expense: Expense) => {
|
||||
// return this.item(expense.categories, new ExpenseCategoryTransformer(), {
|
||||
// currencyCode: expense.currencyCode,
|
||||
// });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the sale invoice attachments.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns
|
||||
*/
|
||||
protected attachments = (expense: Expense) => {
|
||||
// return this.item(expense.attachments, new AttachmentTransformer());
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted published at date.
|
||||
* @param {IExpense} 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,32 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ExpenseTransfromer } from './Expense.transformer';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { Expense } from '../models/Expense.model';
|
||||
|
||||
@Injectable()
|
||||
export class GetExpenseService {
|
||||
constructor(
|
||||
private readonly transformerService: TransformerInjectable,
|
||||
|
||||
@Inject(Expense.name)
|
||||
private readonly expenseModel: 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,81 @@
|
||||
// import { Service, Inject } from 'typedi';
|
||||
// import * as R from 'ramda';
|
||||
// import {
|
||||
// IExpensesFilter,
|
||||
// IExpense,
|
||||
// IPaginationMeta,
|
||||
// IFilterMeta,
|
||||
// } from '@/interfaces';
|
||||
// import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
// import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
// import { ExpenseTransfromer } from './Expense.transformer';
|
||||
// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
// @Service()
|
||||
// export class GetExpenses {
|
||||
// @Inject()
|
||||
// private dynamicListService: DynamicListingService;
|
||||
|
||||
// @Inject()
|
||||
// private tenancy: HasTenancyService;
|
||||
|
||||
// @Inject()
|
||||
// private transformer: TransformerInjectable;
|
||||
|
||||
// /**
|
||||
// * Retrieve expenses paginated list.
|
||||
// * @param {number} tenantId
|
||||
// * @param {IExpensesFilter} expensesFilter
|
||||
// * @return {IExpense[]}
|
||||
// */
|
||||
// public getExpensesList = async (
|
||||
// tenantId: number,
|
||||
// filterDTO: IExpensesFilter
|
||||
// ): Promise<{
|
||||
// expenses: IExpense[];
|
||||
// pagination: IPaginationMeta;
|
||||
// filterMeta: IFilterMeta;
|
||||
// }> => {
|
||||
// const { Expense } = this.tenancy.models(tenantId);
|
||||
|
||||
// // Parses list filter DTO.
|
||||
// const filter = this.parseListFilterDTO(filterDTO);
|
||||
|
||||
// // Dynamic list service.
|
||||
// const dynamicList = await this.dynamicListService.dynamicList(
|
||||
// tenantId,
|
||||
// Expense,
|
||||
// filter
|
||||
// );
|
||||
// // Retrieves the paginated results.
|
||||
// const { results, pagination } = await 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(
|
||||
// tenantId,
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,113 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
AccountNormal,
|
||||
IExpenseCategory,
|
||||
ILedger,
|
||||
ILedgerEntry,
|
||||
} from '@/interfaces';
|
||||
import Ledger from '../Accounting/Ledger';
|
||||
|
||||
export class ExpenseGL {
|
||||
private expense: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(expense: any) {
|
||||
this.expense = expense;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expense GL common entry.
|
||||
* @param {IExpense} expense
|
||||
* @returns {Partial<ILedgerEntry>}
|
||||
*/
|
||||
private getExpenseGLCommonEntry = (): Partial<ILedgerEntry> => {
|
||||
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.
|
||||
* @param {IExpense} expense
|
||||
* @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 {IExpense} expense -
|
||||
* @param {IExpenseCategory} expenseCategory -
|
||||
* @param {number} index
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getExpenseGLCategoryEntry = R.curry(
|
||||
(category: IExpenseCategory, 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.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
public getExpenseGLEntries = (): ILedgerEntry[] => {
|
||||
const getCategoryEntry = this.getExpenseGLCategoryEntry();
|
||||
|
||||
const paymentEntry = this.getExpenseGLPaymentEntry();
|
||||
const categoryEntries = this.expense.categories.map(getCategoryEntry);
|
||||
|
||||
return [paymentEntry, ...categoryEntries];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given expense ledger.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public getExpenseLedger = (): ILedger => {
|
||||
const entries = this.getExpenseGLEntries();
|
||||
|
||||
console.log(entries, 'entries');
|
||||
|
||||
return new Ledger(entries);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IExpense, ILedger } from '@/interfaces';
|
||||
import { ExpenseGL } from './ExpenseGL';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class ExpenseGLEntries {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieves the expense G/L of the given id.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<ILedger>}
|
||||
*/
|
||||
public getExpenseLedgerById = async (
|
||||
tenantId: number,
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<ILedger> => {
|
||||
const { Expense } = await this.tenancy.models(tenantId);
|
||||
|
||||
const expense = await Expense.query(trx)
|
||||
.findById(expenseId)
|
||||
.withGraphFetched('categories')
|
||||
.withGraphFetched('paymentAccount')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.getExpenseLedger(expense);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given expense ledger.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public getExpenseLedger = (expense: IExpense): ILedger => {
|
||||
const expenseGL = new ExpenseGL(expense);
|
||||
|
||||
return expenseGL.getExpenseLedger();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ExpenseGLEntries } from './ExpenseGLEntriesService';
|
||||
|
||||
@Service()
|
||||
export class ExpenseGLEntriesStorage {
|
||||
@Inject()
|
||||
private expenseGLEntries: ExpenseGLEntries;
|
||||
|
||||
@Inject()
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
/**
|
||||
* Writes the expense GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public writeExpenseGLEntries = async (
|
||||
tenantId: number,
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
// Retrieves the given expense ledger.
|
||||
const expenseLedger = await this.expenseGLEntries.getExpenseLedgerById(
|
||||
tenantId,
|
||||
expenseId,
|
||||
trx
|
||||
);
|
||||
// Commits the expense ledger entries.
|
||||
await this.ledgerStorage.commit(tenantId, expenseLedger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the given expense GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public revertExpenseGLEntries = async (
|
||||
tenantId: number,
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
await this.ledgerStorage.deleteByReference(
|
||||
tenantId,
|
||||
expenseId,
|
||||
'Expense',
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the expense GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public rewriteExpenseGLEntries = async (
|
||||
tenantId: number,
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
// Reverts the expense GL entries.
|
||||
await this.revertExpenseGLEntries(tenantId, expenseId, trx);
|
||||
|
||||
// Writes the expense GL entries.
|
||||
await this.writeExpenseGLEntries(tenantId, expenseId, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
IExpenseCreatedPayload,
|
||||
IExpenseEventDeletePayload,
|
||||
IExpenseEventEditPayload,
|
||||
IExpenseEventPublishedPayload,
|
||||
} from '@/interfaces';
|
||||
import { ExpenseGLEntriesStorage } from './ExpenseGLEntriesStorage';
|
||||
|
||||
@Service()
|
||||
export class ExpensesWriteGLSubscriber {
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
private expenseGLEntries: ExpenseGLEntriesStorage;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
* @param bus
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.expenses.onCreated,
|
||||
this.handleWriteGLEntriesOnceCreated
|
||||
);
|
||||
bus.subscribe(
|
||||
events.expenses.onEdited,
|
||||
this.handleRewriteGLEntriesOnceEdited
|
||||
);
|
||||
bus.subscribe(
|
||||
events.expenses.onDeleted,
|
||||
this.handleRevertGLEntriesOnceDeleted
|
||||
);
|
||||
bus.subscribe(
|
||||
events.expenses.onPublished,
|
||||
this.handleWriteGLEntriesOncePublished
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the writing journal entries once the expense created.
|
||||
* @param {IExpenseCreatedPayload} payload -
|
||||
*/
|
||||
public handleWriteGLEntriesOnceCreated = async ({
|
||||
expense,
|
||||
tenantId,
|
||||
trx,
|
||||
}: IExpenseCreatedPayload) => {
|
||||
// In case expense published, write journal entries.
|
||||
if (!expense.publishedAt) return;
|
||||
|
||||
await this.expenseGLEntries.writeExpenseGLEntries(
|
||||
tenantId,
|
||||
expense.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle writing expense journal entries once the expense edited.
|
||||
* @param {IExpenseEventEditPayload} payload -
|
||||
*/
|
||||
public handleRewriteGLEntriesOnceEdited = async ({
|
||||
expenseId,
|
||||
tenantId,
|
||||
expense,
|
||||
authorizedUser,
|
||||
trx,
|
||||
}: IExpenseEventEditPayload) => {
|
||||
// Cannot continue if the expense is not published.
|
||||
if (!expense.publishedAt) return;
|
||||
|
||||
await this.expenseGLEntries.rewriteExpenseGLEntries(
|
||||
tenantId,
|
||||
expense.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts expense journal entries once the expense deleted.
|
||||
* @param {IExpenseEventDeletePayload} payload -
|
||||
*/
|
||||
public handleRevertGLEntriesOnceDeleted = async ({
|
||||
expenseId,
|
||||
tenantId,
|
||||
trx,
|
||||
}: IExpenseEventDeletePayload) => {
|
||||
await this.expenseGLEntries.revertExpenseGLEntries(
|
||||
tenantId,
|
||||
expenseId,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles writing expense journal once the expense publish.
|
||||
* @param {IExpenseEventPublishedPayload} payload -
|
||||
*/
|
||||
public handleWriteGLEntriesOncePublished = async ({
|
||||
tenantId,
|
||||
expense,
|
||||
trx,
|
||||
}: IExpenseEventPublishedPayload) => {
|
||||
// In case expense published, write journal entries.
|
||||
if (!expense.publishedAt) return;
|
||||
|
||||
await this.expenseGLEntries.rewriteExpenseGLEntries(
|
||||
tenantId,
|
||||
expense.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { Exportable } from '../Export/Exportable';
|
||||
// import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
|
||||
// import ItemCategoriesService from './ItemCategoriesService';
|
||||
|
||||
// @Service()
|
||||
// export class ItemCategoriesExportable extends Exportable {
|
||||
// @Inject()
|
||||
// private itemCategoriesApplication: ItemCategoriesService;
|
||||
|
||||
// /**
|
||||
// * Retrieves the accounts data to exportable sheet.
|
||||
// * @param {number} tenantId
|
||||
// * @returns
|
||||
// */
|
||||
// public exportable(tenantId: number, query: IAccountsFilter) {
|
||||
// const parsedQuery = {
|
||||
// sortOrder: 'desc',
|
||||
// columnSortBy: 'created_at',
|
||||
// inactiveMode: false,
|
||||
// ...query,
|
||||
// structure: IAccountsStructureType.Flat,
|
||||
// } as IAccountsFilter;
|
||||
|
||||
// return this.itemCategoriesApplication
|
||||
// .getItemCategoriesList(tenantId, parsedQuery, {})
|
||||
// .then((output) => output.itemCategories);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,38 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import ItemCategoriesService from './ItemCategoriesService';
|
||||
// import { Importable } from '../Import/Importable';
|
||||
// import { Knex } from 'knex';
|
||||
// import { IItemCategoryOTD } from '@/interfaces';
|
||||
// import { ItemCategoriesSampleData } from './constants';
|
||||
|
||||
// @Service()
|
||||
// export class ItemCategoriesImportable extends Importable {
|
||||
// @Inject()
|
||||
// private itemCategoriesService: ItemCategoriesService;
|
||||
|
||||
// /**
|
||||
// * Importing to create new item category service.
|
||||
// * @param {number} tenantId
|
||||
// * @param {any} createDTO
|
||||
// * @param {Knex.Transaction} trx
|
||||
// */
|
||||
// public async importable(
|
||||
// tenantId: number,
|
||||
// createDTO: IItemCategoryOTD,
|
||||
// trx?: Knex.Transaction
|
||||
// ) {
|
||||
// await this.itemCategoriesService.newItemCategory(
|
||||
// tenantId,
|
||||
// createDTO,
|
||||
// {},
|
||||
// trx
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Item categories sample data used to download sample sheet file.
|
||||
// */
|
||||
// public sampleData(): any[] {
|
||||
// return ItemCategoriesSampleData;
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,64 @@
|
||||
import { IItemCategoryOTD } from './ItemCategory.interfaces';
|
||||
import { CreateItemCategoryService } from './commands/CreateItemCategory.service';
|
||||
import { DeleteItemCategoryService } from './commands/DeleteItemCategory.service';
|
||||
import { EditItemCategoryService } from './commands/EditItemCategory.service';
|
||||
import { GetItemCategoryService } from './queries/GetItemCategory.service';
|
||||
|
||||
export class ItemCategoryApplication {
|
||||
constructor(
|
||||
private readonly createItemCategoryService: CreateItemCategoryService,
|
||||
private readonly editItemCategoryService: EditItemCategoryService,
|
||||
private readonly getItemCategoryService: GetItemCategoryService,
|
||||
private readonly deleteItemCategoryService: DeleteItemCategoryService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new item category.
|
||||
* @param {number} tenantId - The tenant id.
|
||||
* @param {IItemCategoryOTD} itemCategoryDTO - The item category data.
|
||||
* @returns {Promise<ItemCategory>} The created item category.
|
||||
*/
|
||||
public createItemCategory(
|
||||
tenantId: number,
|
||||
itemCategoryDTO: IItemCategoryOTD,
|
||||
) {
|
||||
return this.createItemCategoryService.newItemCategory(
|
||||
tenantId,
|
||||
itemCategoryDTO,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing item category.
|
||||
* @param {number} itemCategoryId - The item category id to update.
|
||||
* @param {IItemCategoryOTD} itemCategoryDTO - The updated item category data.
|
||||
* @returns {Promise<ItemCategory>} The updated item category.
|
||||
*/
|
||||
public editItemCategory(
|
||||
itemCategoryId: number,
|
||||
itemCategoryDTO: IItemCategoryOTD,
|
||||
) {
|
||||
return this.editItemCategoryService.editItemCategory(
|
||||
itemCategoryId,
|
||||
itemCategoryDTO,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an item category by id.
|
||||
* @param {number} itemCategoryId - The item category id to retrieve.
|
||||
* @returns {Promise<ItemCategory>} The requested item category.
|
||||
*/
|
||||
public getItemCategory(itemCategoryId: number) {
|
||||
return this.getItemCategoryService.getItemCategory(itemCategoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an item category.
|
||||
* @param {number} itemCategoryId - The item category id to delete.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteItemCategory(itemCategoryId: number) {
|
||||
return this.deleteItemCategoryService.deleteItemCategory(itemCategoryId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
} from '@nestjs/common';
|
||||
import { ItemCategoryApplication } from './ItemCategory.application';
|
||||
import { IItemCategoryOTD } from './ItemCategory.interfaces';
|
||||
|
||||
@Controller('item-categories')
|
||||
export class ItemCategoryController {
|
||||
constructor(
|
||||
private readonly itemCategoryApplication: ItemCategoryApplication,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
async createItemCategory(
|
||||
@Body('tenantId') tenantId: number,
|
||||
@Body() itemCategoryDTO: IItemCategoryOTD,
|
||||
) {
|
||||
return this.itemCategoryApplication.createItemCategory(
|
||||
tenantId,
|
||||
itemCategoryDTO,
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async editItemCategory(
|
||||
@Param('id') id: number,
|
||||
@Body() itemCategoryDTO: IItemCategoryOTD,
|
||||
) {
|
||||
return this.itemCategoryApplication.editItemCategory(id, itemCategoryDTO);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getItemCategory(@Param('id') id: number) {
|
||||
return this.itemCategoryApplication.getItemCategory(id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async deleteItemCategory(@Param('id') id: number) {
|
||||
return this.itemCategoryApplication.deleteItemCategory(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Knex } from 'knex';
|
||||
// import { IDynamicListFilterDTO } from './DynamicFilter';
|
||||
// import { ISystemUser } from './User';
|
||||
import { ItemCategory } from './models/ItemCategory.model';
|
||||
|
||||
|
||||
export interface IItemCategoryOTD {
|
||||
name: string;
|
||||
|
||||
description?: string;
|
||||
userId: number;
|
||||
|
||||
costAccountId?: number;
|
||||
sellAccountId?: number;
|
||||
inventoryAccountId?: number;
|
||||
|
||||
costMethod?: string;
|
||||
}
|
||||
|
||||
// export interface IItemCategoriesFilter extends IDynamicListFilterDTO {
|
||||
// stringifiedFilterRoles?: string;
|
||||
// }
|
||||
|
||||
export interface IItemCategoryCreatedPayload {
|
||||
itemCategory: ItemCategory;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IItemCategoryEditedPayload {
|
||||
oldItemCategory: ItemCategory;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IItemCategoryDeletedPayload {
|
||||
itemCategoryId: number;
|
||||
oldItemCategory: ItemCategory;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||
import { CreateItemCategoryService } from './commands/CreateItemCategory.service';
|
||||
import { DeleteItemCategoryService } from './commands/DeleteItemCategory.service';
|
||||
import { EditItemCategoryService } from './commands/EditItemCategory.service';
|
||||
import { GetItemCategoryService } from './queries/GetItemCategory.service';
|
||||
import { ItemCategoryApplication } from './ItemCategory.application';
|
||||
import { ItemCategoryController } from './ItemCategory.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TenancyDatabaseModule],
|
||||
controllers: [ItemCategoryController],
|
||||
providers: [
|
||||
CreateItemCategoryService,
|
||||
EditItemCategoryService,
|
||||
GetItemCategoryService,
|
||||
DeleteItemCategoryService,
|
||||
ItemCategoryApplication,
|
||||
],
|
||||
})
|
||||
export class ItemCategoryModule {}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { ItemCategory } from '../models/ItemCategory.model';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { ERRORS } from '../constants';
|
||||
import { ACCOUNT_ROOT_TYPE, ACCOUNT_TYPE } from '@/constants/accounts';
|
||||
|
||||
export class CommandItemCategoryValidatorService {
|
||||
constructor(
|
||||
@Inject(ItemCategory.name)
|
||||
private readonly itemCategoryModel: typeof ItemCategory,
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: typeof Account,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the category name uniquiness.
|
||||
* @param {string} categoryName - Category name.
|
||||
* @param {number} notAccountId - Ignore the account id.
|
||||
*/
|
||||
public async validateCategoryNameUniquiness(
|
||||
categoryName: string,
|
||||
notCategoryId?: number,
|
||||
) {
|
||||
const foundItemCategory = await this.itemCategoryModel
|
||||
.query()
|
||||
.findOne('name', categoryName)
|
||||
.onBuild((query) => {
|
||||
if (notCategoryId) {
|
||||
query.whereNot('id', notCategoryId);
|
||||
}
|
||||
});
|
||||
|
||||
if (foundItemCategory) {
|
||||
throw new ServiceError(
|
||||
ERRORS.CATEGORY_NAME_EXISTS,
|
||||
'The item category name is already exist.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates sell account existance and type.
|
||||
* @param {number} sellAccountId - Sell account id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async validateSellAccount(sellAccountId: number) {
|
||||
const foundAccount = await this.accountModel
|
||||
.query()
|
||||
.findById(sellAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_FOUND);
|
||||
} else if (!foundAccount.isRootType(ACCOUNT_ROOT_TYPE.INCOME)) {
|
||||
throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_INCOME);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates COGS account existance and type.
|
||||
* @param {number} costAccountId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async validateCostAccount(costAccountId: number) {
|
||||
const foundAccount = await this.accountModel
|
||||
.query()
|
||||
.findById(costAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_FOUMD);
|
||||
} else if (!foundAccount.isRootType(ACCOUNT_ROOT_TYPE.EXPENSE)) {
|
||||
throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_COGS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates inventory account existance and type.
|
||||
* @param {number} inventoryAccountId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async validateInventoryAccount(inventoryAccountId: number) {
|
||||
const foundAccount = await this.accountModel
|
||||
.query()
|
||||
.findById(inventoryAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_FOUND);
|
||||
} else if (!foundAccount.isAccountType(ACCOUNT_TYPE.INVENTORY)) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_INVENTORY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IItemCategoryOTD,
|
||||
IItemCategoryCreatedPayload,
|
||||
} from '../ItemCategory.interfaces';
|
||||
import { events } from '@/common/events/events';
|
||||
import { CommandItemCategoryValidatorService } from './CommandItemCategoryValidator.service';
|
||||
import { ItemCategory } from '../models/ItemCategory.model';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||
|
||||
@Injectable()
|
||||
export class CreateItemCategoryService {
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandItemCategoryValidatorService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
|
||||
@Inject(ItemCategory.name)
|
||||
private readonly itemCategoryModel: typeof ItemCategory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Transforms OTD to model object.
|
||||
* @param {IItemCategoryOTD} itemCategoryOTD
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
private transformOTDToObject(
|
||||
itemCategoryOTD: IItemCategoryOTD,
|
||||
authorizedUser: SystemUser,
|
||||
): ItemCategory {
|
||||
return { ...itemCategoryOTD, userId: authorizedUser.id };
|
||||
}
|
||||
/**
|
||||
* Inserts a new item category.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemCategoryOTD} itemCategoryOTD
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async newItemCategory(
|
||||
tenantId: number,
|
||||
itemCategoryOTD: IItemCategoryOTD,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<ItemCategory> {
|
||||
// Validate the category name uniquiness.
|
||||
await this.validator.validateCategoryNameUniquiness(itemCategoryOTD.name);
|
||||
|
||||
if (itemCategoryOTD.sellAccountId) {
|
||||
await this.validator.validateSellAccount(itemCategoryOTD.sellAccountId);
|
||||
}
|
||||
if (itemCategoryOTD.costAccountId) {
|
||||
await this.validator.validateCostAccount(itemCategoryOTD.costAccountId);
|
||||
}
|
||||
if (itemCategoryOTD.inventoryAccountId) {
|
||||
await this.validator.validateInventoryAccount(
|
||||
itemCategoryOTD.inventoryAccountId,
|
||||
);
|
||||
}
|
||||
const itemCategoryObj = this.transformOTDToObject(
|
||||
itemCategoryOTD,
|
||||
authorizedUser,
|
||||
);
|
||||
// Creates item category under unit-of-work evnirement.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Inserts the item category.
|
||||
const itemCategory = await this.itemCategoryModel.query(trx).insert({
|
||||
...itemCategoryObj,
|
||||
});
|
||||
// Triggers `onItemCategoryCreated` event.
|
||||
await this.eventEmitter.emitAsync(events.itemCategory.onCreated, {
|
||||
itemCategory,
|
||||
tenantId,
|
||||
trx,
|
||||
} as IItemCategoryCreatedPayload);
|
||||
|
||||
return itemCategory;
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { CommandItemCategoryValidatorService } from './CommandItemCategoryValidator.service';
|
||||
import { ItemCategory } from '../models/ItemCategory.model';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { IItemCategoryDeletedPayload } from '../ItemCategory.interfaces';
|
||||
import { Item } from '@/modules/Items/models/Item';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteItemCategoryService {
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandItemCategoryValidatorService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
@Inject(ItemCategory.name)
|
||||
private readonly itemCategoryModel: typeof ItemCategory,
|
||||
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: typeof Item,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Deletes the given item category.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} itemCategoryId - Item category id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteItemCategory(itemCategoryId: number) {
|
||||
// Retrieve item category or throw not found error.
|
||||
const oldItemCategory = await this.itemCategoryModel
|
||||
.query()
|
||||
.findById(itemCategoryId)
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Unassociate items with item category.
|
||||
await this.unassociateItemsWithCategories(itemCategoryId, trx);
|
||||
|
||||
// Delete item category.
|
||||
await ItemCategory.query(trx).findById(itemCategoryId).delete();
|
||||
|
||||
// Triggers `onItemCategoryDeleted` event.
|
||||
await this.eventEmitter.emitAsync(events.itemCategory.onDeleted, {
|
||||
itemCategoryId,
|
||||
oldItemCategory,
|
||||
} as IItemCategoryDeletedPayload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink items relations with item categories.
|
||||
* @param {number|number[]} itemCategoryId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
private async unassociateItemsWithCategories(
|
||||
itemCategoryId: number | number[],
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const ids = Array.isArray(itemCategoryId)
|
||||
? itemCategoryId
|
||||
: [itemCategoryId];
|
||||
|
||||
await this.itemModel.query(trx)
|
||||
.whereIn('category_id', ids)
|
||||
.patch({ categoryId: null });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { CommandItemCategoryValidatorService } from './CommandItemCategoryValidator.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import {
|
||||
IItemCategoryEditedPayload,
|
||||
IItemCategoryOTD,
|
||||
} from '../ItemCategory.interfaces';
|
||||
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||
import { Knex } from 'knex';
|
||||
import { ItemCategory } from '../models/ItemCategory.model';
|
||||
import { Inject } from '@nestjs/common';
|
||||
|
||||
export class EditItemCategoryService {
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandItemCategoryValidatorService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
@Inject(ItemCategory.name)
|
||||
private readonly itemCategoryModel: typeof ItemCategory,
|
||||
) {}
|
||||
/**
|
||||
* Edits item category.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemCategoryId
|
||||
* @param {IItemCategoryOTD} itemCategoryOTD
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async editItemCategory(
|
||||
itemCategoryId: number,
|
||||
itemCategoryOTD: IItemCategoryOTD,
|
||||
): Promise<IItemCategory> {
|
||||
// Retrieve the item category from the storage.
|
||||
const oldItemCategory = await this.itemCategoryModel
|
||||
.query()
|
||||
.findById(itemCategoryId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate the category name whether unique on the storage.
|
||||
await this.validator.validateCategoryNameUniquiness(
|
||||
itemCategoryOTD.name,
|
||||
itemCategoryId,
|
||||
);
|
||||
if (itemCategoryOTD.sellAccountId) {
|
||||
await this.validator.validateSellAccount(itemCategoryOTD.sellAccountId);
|
||||
}
|
||||
if (itemCategoryOTD.costAccountId) {
|
||||
await this.validator.validateCostAccount(itemCategoryOTD.costAccountId);
|
||||
}
|
||||
if (itemCategoryOTD.inventoryAccountId) {
|
||||
await this.validator.validateInventoryAccount(
|
||||
itemCategoryOTD.inventoryAccountId,
|
||||
);
|
||||
}
|
||||
const itemCategoryObj = this.transformOTDToObject(
|
||||
itemCategoryOTD,
|
||||
authorizedUser,
|
||||
);
|
||||
//
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
//
|
||||
const itemCategory = await ItemCategory.query().patchAndFetchById(
|
||||
itemCategoryId,
|
||||
{ ...itemCategoryObj },
|
||||
);
|
||||
// Triggers `onItemCategoryEdited` event.
|
||||
await this.eventEmitter.emitAsync(events.itemCategory.onEdited, {
|
||||
oldItemCategory,
|
||||
trx,
|
||||
} as IItemCategoryEditedPayload);
|
||||
|
||||
return itemCategory;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms OTD to model object.
|
||||
* @param {IItemCategoryOTD} itemCategoryOTD
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
private transformOTDToObject(
|
||||
itemCategoryOTD: IItemCategoryOTD,
|
||||
authorizedUser: SystemUser,
|
||||
) {
|
||||
return { ...itemCategoryOTD, userId: authorizedUser.id };
|
||||
}
|
||||
}
|
||||
35
packages/server-nest/src/modules/ItemCategories/constants.ts
Normal file
35
packages/server-nest/src/modules/ItemCategories/constants.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const ERRORS = {
|
||||
ITEM_CATEGORIES_NOT_FOUND: 'ITEM_CATEGORIES_NOT_FOUND',
|
||||
CATEGORY_NAME_EXISTS: 'CATEGORY_NAME_EXISTS',
|
||||
CATEGORY_NOT_FOUND: 'CATEGORY_NOT_FOUND',
|
||||
COST_ACCOUNT_NOT_FOUMD: 'COST_ACCOUNT_NOT_FOUMD',
|
||||
COST_ACCOUNT_NOT_COGS: 'COST_ACCOUNT_NOT_COGS',
|
||||
SELL_ACCOUNT_NOT_INCOME: 'SELL_ACCOUNT_NOT_INCOME',
|
||||
SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND',
|
||||
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
|
||||
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
|
||||
CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS',
|
||||
};
|
||||
|
||||
export const ItemCategoriesSampleData = [
|
||||
{
|
||||
Name: 'Kassulke Group',
|
||||
Description: 'Optio itaque eaque qui adipisci illo sed.',
|
||||
},
|
||||
{
|
||||
Name: 'Crist, Mraz and Lueilwitz',
|
||||
Description:
|
||||
'Dolores veniam deserunt sed commodi error quia veritatis non.',
|
||||
},
|
||||
{
|
||||
Name: 'Gutmann and Sons',
|
||||
Description:
|
||||
'Ratione aperiam voluptas rem adipisci assumenda eos neque veritatis tempora.',
|
||||
},
|
||||
{
|
||||
Name: 'Reichel - Raynor',
|
||||
Description:
|
||||
'Necessitatibus repellendus placeat possimus dolores excepturi ut.',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,63 @@
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import { Model, mixin } from 'objection';
|
||||
// import TenantModel from 'models/TenantModel';
|
||||
// import ModelSetting from './ModelSetting';
|
||||
// import ItemCategorySettings from './ItemCategory.Settings';
|
||||
|
||||
export class ItemCategory extends BaseModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'items_categories';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { Item } = require('../../Items/models/Item');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Item category may has many items.
|
||||
*/
|
||||
items: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: Item,
|
||||
join: {
|
||||
from: 'items_categories.id',
|
||||
to: 'items.categoryId',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
/**
|
||||
* Inactive/Active mode.
|
||||
*/
|
||||
sortByCount(query, order = 'asc') {
|
||||
query.orderBy('count', order);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Model meta.
|
||||
*/
|
||||
// static get meta() {
|
||||
// return ItemCategorySettings;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ItemCategory } from '../models/ItemCategory.model';
|
||||
|
||||
@Injectable()
|
||||
export class GetItemCategoryService {
|
||||
constructor(
|
||||
@Inject(ItemCategory.name)
|
||||
private readonly itemCategoryModel: typeof ItemCategory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves item category by id.
|
||||
* @param {number} itemCategoryId
|
||||
* @returns {Promise<IItemCategory>}
|
||||
*/
|
||||
public async getItemCategory(itemCategoryId: number) {
|
||||
const itemCategory = await this.itemCategoryModel
|
||||
.query()
|
||||
.findById(itemCategoryId)
|
||||
.throwIfNotFound();
|
||||
|
||||
return itemCategory;
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,13 @@ import { ServiceError } from './ServiceError';
|
||||
import { IItem, IItemDTO } from '@/interfaces/Item';
|
||||
import { ERRORS } from './Items.constants';
|
||||
import { Item } from './models/Item';
|
||||
import { AccountModel } from '../Accounts/models/Account.model';
|
||||
import { Account } from '../Accounts/models/Account.model';
|
||||
|
||||
@Injectable()
|
||||
export class ItemsValidators {
|
||||
constructor(
|
||||
@Inject(Item.name) private itemModel: typeof Item,
|
||||
@Inject(AccountModel.name) private accountModel: typeof AccountModel,
|
||||
@Inject(Account.name) private accountModel: typeof Account,
|
||||
@Inject(Item.name) private taxRateModel: typeof Item,
|
||||
@Inject(Item.name) private itemEntryModel: typeof Item,
|
||||
@Inject(Item.name) private itemCategoryModel: typeof Item,
|
||||
|
||||
@@ -3,11 +3,11 @@ import { Global, Module, Scope } from '@nestjs/common';
|
||||
import { TENANCY_DB_CONNECTION } from '../TenancyDB/TenancyDB.constants';
|
||||
|
||||
import { Item } from '../../../modules/Items/models/Item';
|
||||
import { AccountModel } from '@/modules/Accounts/models/Account.model';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { ItemEntry } from '@/modules/Items/models/ItemEntry';
|
||||
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
|
||||
|
||||
const models = [Item, AccountModel, ItemEntry, AccountTransaction];
|
||||
const models = [Item, Account, ItemEntry, AccountTransaction];
|
||||
|
||||
const modelProviders = models.map((model) => {
|
||||
return {
|
||||
|
||||
@@ -165,7 +165,7 @@ export class Transformer {
|
||||
* @param {string} format
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formatDate(date: string, format?: string) {
|
||||
protected formatDate(date: string | Date, format?: string) {
|
||||
// Use the export date format if the async operation is in exporting,
|
||||
// otherwise use the given or default format.
|
||||
const _format = this.context.exportAls.isExport
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { isNull, isUndefined } from 'lodash';
|
||||
|
||||
export function assocItemEntriesDefaultIndex<T>(
|
||||
entries: Array<T & { index?: number }>,
|
||||
): Array<T & { index: number }> {
|
||||
return entries.map((entry, index) => {
|
||||
return {
|
||||
index:
|
||||
isUndefined(entry.index) || isNull(entry.index)
|
||||
? index + 1
|
||||
: entry.index,
|
||||
...entry,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -6,10 +6,11 @@
|
||||
"dist",
|
||||
"**/*spec.ts",
|
||||
// "./src/modules/DynamicListing/**/*.ts",
|
||||
"./src/modules/Export",
|
||||
"./src/modules/Import",
|
||||
"./src/modules/Import/**/*.ts",
|
||||
"./src/modules/Export/**/*.ts",
|
||||
"./src/modules/DynamicListing",
|
||||
// "./src/modules/DynamicListing",
|
||||
"./src/modules/Views"
|
||||
"./src/modules/DynamicListing/**/*.ts",
|
||||
"./src/modules/Views",
|
||||
"./src/modules/Expenses/subscribers"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user