mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 07:10:33 +00:00
refactor: migrate item categories module to nestjs
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Transformer } from '../Transformer/Transformer';
|
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 { flatToNestedArray } from '@/utils/flat-to-nested-array';
|
||||||
import { assocDepthLevelToObjectTree } from '@/utils/assoc-depth-level-to-object-tree';
|
import { assocDepthLevelToObjectTree } from '@/utils/assoc-depth-level-to-object-tree';
|
||||||
import { nestedArrayToFlatten } from '@/utils/nested-array-to-flatten';
|
import { nestedArrayToFlatten } from '@/utils/nested-array-to-flatten';
|
||||||
@@ -35,7 +35,7 @@ export class AccountTransformer extends Transformer {
|
|||||||
* @param {IAccount} account -
|
* @param {IAccount} account -
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
public flattenName = (account: AccountModel): string => {
|
public flattenName = (account: Account): string => {
|
||||||
const parentDependantsIds = this.options.accountsGraph.dependantsOf(
|
const parentDependantsIds = this.options.accountsGraph.dependantsOf(
|
||||||
account.id,
|
account.id,
|
||||||
);
|
);
|
||||||
@@ -51,7 +51,7 @@ export class AccountTransformer extends Transformer {
|
|||||||
* @param {IAccount} invoice
|
* @param {IAccount} invoice
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected formattedAmount = (account: AccountModel): string => {
|
protected formattedAmount = (account: Account): string => {
|
||||||
return this.formatNumber(account.amount, {
|
return this.formatNumber(account.amount, {
|
||||||
currencyCode: account.currencyCode,
|
currencyCode: account.currencyCode,
|
||||||
});
|
});
|
||||||
@@ -59,10 +59,10 @@ export class AccountTransformer extends Transformer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the formatted bank balance.
|
* Retrieves the formatted bank balance.
|
||||||
* @param {AccountModel} account
|
* @param {Account} account
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected bankBalanceFormatted = (account: AccountModel): string => {
|
protected bankBalanceFormatted = (account: Account): string => {
|
||||||
return this.formatNumber(account.bankBalance, {
|
return this.formatNumber(account.bankBalance, {
|
||||||
currencyCode: account.currencyCode,
|
currencyCode: account.currencyCode,
|
||||||
});
|
});
|
||||||
@@ -73,7 +73,7 @@ export class AccountTransformer extends Transformer {
|
|||||||
* @param {IAccount} account
|
* @param {IAccount} account
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected lastFeedsUpdatedAtFormatted = (account: AccountModel): string => {
|
protected lastFeedsUpdatedAtFormatted = (account: Account): string => {
|
||||||
return account.lastFeedsUpdatedAt
|
return account.lastFeedsUpdatedAt
|
||||||
? this.formatDate(account.lastFeedsUpdatedAt)
|
? this.formatDate(account.lastFeedsUpdatedAt)
|
||||||
: '';
|
: '';
|
||||||
@@ -84,7 +84,7 @@ export class AccountTransformer extends Transformer {
|
|||||||
* @param account
|
* @param account
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
protected isFeedsPaused = (account: AccountModel): boolean => {
|
protected isFeedsPaused = (account: Account): boolean => {
|
||||||
// return account.plaidItem?.isPaused || false;
|
// return account.plaidItem?.isPaused || false;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -94,7 +94,7 @@ export class AccountTransformer extends Transformer {
|
|||||||
* Retrieves formatted account type label.
|
* Retrieves formatted account type label.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected accountTypeLabel = (account: AccountModel): string => {
|
protected accountTypeLabel = (account: Account): string => {
|
||||||
return this.context.i18n.t(account.accountTypeLabel);
|
return this.context.i18n.t(account.accountTypeLabel);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ export class AccountTransformer extends Transformer {
|
|||||||
* Retrieves formatted account normal.
|
* Retrieves formatted account normal.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected accountNormalFormatted = (account: AccountModel): string => {
|
protected accountNormalFormatted = (account: Account): string => {
|
||||||
return this.context.i18n.t(account.accountNormalFormatted);
|
return this.context.i18n.t(account.accountNormalFormatted);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ export class AccountTransformer extends Transformer {
|
|||||||
* @param {IAccount[]}
|
* @param {IAccount[]}
|
||||||
* @returns {IAccount[]}
|
* @returns {IAccount[]}
|
||||||
*/
|
*/
|
||||||
protected postCollectionTransform = (accounts: AccountModel[]) => {
|
protected postCollectionTransform = (accounts: Account[]) => {
|
||||||
// Transfom the flatten to accounts tree.
|
// Transfom the flatten to accounts tree.
|
||||||
const transformed = flatToNestedArray(accounts, {
|
const transformed = flatToNestedArray(accounts, {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { AccountModel } from './models/Account.model';
|
import { Account } from './models/Account.model';
|
||||||
// import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
|
// import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
|
||||||
|
|
||||||
export enum AccountNormal {
|
export enum AccountNormal {
|
||||||
@@ -42,26 +42,26 @@ export interface IAccountEventCreatingPayload {
|
|||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
export interface IAccountEventCreatedPayload {
|
export interface IAccountEventCreatedPayload {
|
||||||
account: AccountModel;
|
account: Account;
|
||||||
accountId: number;
|
accountId: number;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccountEventEditedPayload {
|
export interface IAccountEventEditedPayload {
|
||||||
account: AccountModel;
|
account: Account;
|
||||||
oldAccount: AccountModel;
|
oldAccount: Account;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccountEventDeletedPayload {
|
export interface IAccountEventDeletedPayload {
|
||||||
accountId: number;
|
accountId: number;
|
||||||
oldAccount: AccountModel;
|
oldAccount: Account;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccountEventDeletePayload {
|
export interface IAccountEventDeletePayload {
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
oldAccount: AccountModel;
|
oldAccount: Account;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccountEventActivatedPayload {
|
export interface IAccountEventActivatedPayload {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CreateAccountService } from './CreateAccount.service';
|
|||||||
import { DeleteAccount } from './DeleteAccount.service';
|
import { DeleteAccount } from './DeleteAccount.service';
|
||||||
import { EditAccount } from './EditAccount.service';
|
import { EditAccount } from './EditAccount.service';
|
||||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||||
import { AccountModel } from './models/Account.model';
|
import { Account } from './models/Account.model';
|
||||||
import { EditAccountDTO } from './EditAccount.dto';
|
import { EditAccountDTO } from './EditAccount.dto';
|
||||||
import { GetAccount } from './GetAccount.service';
|
import { GetAccount } from './GetAccount.service';
|
||||||
import { ActivateAccount } from './ActivateAccount.service';
|
import { ActivateAccount } from './ActivateAccount.service';
|
||||||
@@ -37,7 +37,7 @@ export class AccountsApplication {
|
|||||||
public createAccount = (
|
public createAccount = (
|
||||||
accountDTO: CreateAccountDTO,
|
accountDTO: CreateAccountDTO,
|
||||||
trx?: Knex.Transaction,
|
trx?: Knex.Transaction,
|
||||||
): Promise<AccountModel> => {
|
): Promise<Account> => {
|
||||||
return this.createAccountService.createAccount(accountDTO, trx);
|
return this.createAccountService.createAccount(accountDTO, trx);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { IAccountEventActivatedPayload } from './Accounts.types';
|
import { IAccountEventActivatedPayload } from './Accounts.types';
|
||||||
import { AccountModel } from './models/Account.model';
|
import { Account } from './models/Account.model';
|
||||||
import { AccountRepository } from './repositories/Account.repository';
|
import { AccountRepository } from './repositories/Account.repository';
|
||||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
@@ -13,8 +13,8 @@ export class ActivateAccount {
|
|||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly uow: UnitOfWork,
|
private readonly uow: UnitOfWork,
|
||||||
|
|
||||||
@Inject(AccountModel.name)
|
@Inject(Account.name)
|
||||||
private readonly accountModel: typeof AccountModel,
|
private readonly accountModel: typeof Account,
|
||||||
private readonly accountRepository: AccountRepository,
|
private readonly accountRepository: AccountRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
|
|||||||
// import AccountTypesUtils from '@/lib/AccountTypes';
|
// import AccountTypesUtils from '@/lib/AccountTypes';
|
||||||
import { ServiceError } from '../Items/ServiceError';
|
import { ServiceError } from '../Items/ServiceError';
|
||||||
import { ERRORS, MAX_ACCOUNTS_CHART_DEPTH } from './constants';
|
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 { AccountRepository } from './repositories/Account.repository';
|
||||||
import { AccountTypesUtils } from './utils/AccountType.utils';
|
import { AccountTypesUtils } from './utils/AccountType.utils';
|
||||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||||
@@ -13,16 +13,16 @@ import { EditAccountDTO } from './EditAccount.dto';
|
|||||||
@Injectable({ scope: Scope.REQUEST })
|
@Injectable({ scope: Scope.REQUEST })
|
||||||
export class CommandAccountValidators {
|
export class CommandAccountValidators {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(AccountModel.name)
|
@Inject(Account.name)
|
||||||
private readonly accountModel: typeof AccountModel,
|
private readonly accountModel: typeof Account,
|
||||||
private readonly accountRepository: AccountRepository,
|
private readonly accountRepository: AccountRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Throws error if the account was prefined.
|
* Throws error if the account was prefined.
|
||||||
* @param {AccountModel} account
|
* @param {Account} account
|
||||||
*/
|
*/
|
||||||
public throwErrorIfAccountPredefined(account: AccountModel) {
|
public throwErrorIfAccountPredefined(account: Account) {
|
||||||
if (account.predefined) {
|
if (account.predefined) {
|
||||||
throw new ServiceError(ERRORS.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
|
* Diff account type between new and old account, throw service error
|
||||||
* if they have different account type.
|
* if they have different account type.
|
||||||
* @param {AccountModel|CreateAccountDTO|EditAccountDTO} oldAccount
|
* @param {Account|CreateAccountDTO|EditAccountDTO} oldAccount
|
||||||
* @param {AccountModel|CreateAccountDTO|EditAccountDTO} newAccount
|
* @param {Account|CreateAccountDTO|EditAccountDTO} newAccount
|
||||||
*/
|
*/
|
||||||
public async isAccountTypeChangedOrThrowError(
|
public async isAccountTypeChangedOrThrowError(
|
||||||
oldAccount: AccountModel | CreateAccountDTO | EditAccountDTO,
|
oldAccount: Account | CreateAccountDTO | EditAccountDTO,
|
||||||
newAccount: AccountModel | CreateAccountDTO | EditAccountDTO,
|
newAccount: Account | CreateAccountDTO | EditAccountDTO,
|
||||||
) {
|
) {
|
||||||
if (oldAccount.accountType !== newAccount.accountType) {
|
if (oldAccount.accountType !== newAccount.accountType) {
|
||||||
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE);
|
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
|
* Validates the account DTO currency code whether equals the currency code of
|
||||||
* parent account.
|
* parent account.
|
||||||
* @param {CreateAccountDTO | EditAccountDTO} accountDTO
|
* @param {CreateAccountDTO | EditAccountDTO} accountDTO
|
||||||
* @param {AccountModel} parentAccount
|
* @param {Account} parentAccount
|
||||||
* @param {string} baseCurrency -
|
* @param {string} baseCurrency -
|
||||||
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
|
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
|
||||||
*/
|
*/
|
||||||
public validateCurrentSameParentAccount = (
|
public validateCurrentSameParentAccount = (
|
||||||
accountDTO: CreateAccountDTO | EditAccountDTO,
|
accountDTO: CreateAccountDTO | EditAccountDTO,
|
||||||
parentAccount: AccountModel,
|
parentAccount: Account,
|
||||||
baseCurrency: string,
|
baseCurrency: string,
|
||||||
) => {
|
) => {
|
||||||
// If the account DTO currency not assigned and the parent account has no base currency.
|
// If the account DTO currency not assigned and the parent account has no base currency.
|
||||||
@@ -187,7 +187,7 @@ export class CommandAccountValidators {
|
|||||||
*/
|
*/
|
||||||
public throwErrorIfParentHasDiffType(
|
public throwErrorIfParentHasDiffType(
|
||||||
accountDTO: CreateAccountDTO | EditAccountDTO,
|
accountDTO: CreateAccountDTO | EditAccountDTO,
|
||||||
parentAccount: AccountModel,
|
parentAccount: Account,
|
||||||
) {
|
) {
|
||||||
if (accountDTO.accountType !== parentAccount.accountType) {
|
if (accountDTO.accountType !== parentAccount.accountType) {
|
||||||
throw new ServiceError(ERRORS.PARENT_ACCOUNT_HAS_DIFFERENT_TYPE);
|
throw new ServiceError(ERRORS.PARENT_ACCOUNT_HAS_DIFFERENT_TYPE);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
CreateAccountParams,
|
CreateAccountParams,
|
||||||
} from './Accounts.types';
|
} from './Accounts.types';
|
||||||
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
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 { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
@@ -20,8 +20,8 @@ import { CreateAccountDTO } from './CreateAccount.dto';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateAccountService {
|
export class CreateAccountService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(AccountModel.name)
|
@Inject(Account.name)
|
||||||
private readonly accountModel: typeof AccountModel,
|
private readonly accountModel: typeof Account,
|
||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly uow: UnitOfWork,
|
private readonly uow: UnitOfWork,
|
||||||
private readonly validator: CommandAccountValidators,
|
private readonly validator: CommandAccountValidators,
|
||||||
@@ -102,7 +102,7 @@ export class CreateAccountService {
|
|||||||
accountDTO: CreateAccountDTO,
|
accountDTO: CreateAccountDTO,
|
||||||
trx?: Knex.Transaction,
|
trx?: Knex.Transaction,
|
||||||
params: CreateAccountParams = { ignoreUniqueName: false },
|
params: CreateAccountParams = { ignoreUniqueName: false },
|
||||||
): Promise<AccountModel> => {
|
): Promise<Account> => {
|
||||||
// Retrieves the given tenant metadata.
|
// Retrieves the given tenant metadata.
|
||||||
const tenant = await this.tenancyContext.getTenant(true);
|
const tenant = await this.tenancyContext.getTenant(true);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Knex } from 'knex';
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
// import { IAccountEventDeletedPayload } from '@/interfaces';
|
// import { IAccountEventDeletedPayload } from '@/interfaces';
|
||||||
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
||||||
import { AccountModel } from './models/Account.model';
|
import { Account } from './models/Account.model';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
@@ -11,7 +11,7 @@ import { IAccountEventDeletedPayload } from './Accounts.types';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeleteAccount {
|
export class DeleteAccount {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(AccountModel.name) private accountModel: typeof AccountModel,
|
@Inject(Account.name) private accountModel: typeof Account,
|
||||||
private eventEmitter: EventEmitter2,
|
private eventEmitter: EventEmitter2,
|
||||||
private uow: UnitOfWork,
|
private uow: UnitOfWork,
|
||||||
private validator: CommandAccountValidators,
|
private validator: CommandAccountValidators,
|
||||||
@@ -21,7 +21,7 @@ export class DeleteAccount {
|
|||||||
* Authorize account delete.
|
* Authorize account delete.
|
||||||
* @param {number} accountId - Account id.
|
* @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.
|
// Throw error if the account was predefined.
|
||||||
this.validator.throwErrorIfAccountPredefined(oldAccount);
|
this.validator.throwErrorIfAccountPredefined(oldAccount);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
||||||
import { AccountModel } from './models/Account.model';
|
import { Account } from './models/Account.model';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
@@ -14,8 +14,8 @@ export class EditAccount {
|
|||||||
private readonly uow: UnitOfWork,
|
private readonly uow: UnitOfWork,
|
||||||
private readonly validator: CommandAccountValidators,
|
private readonly validator: CommandAccountValidators,
|
||||||
|
|
||||||
@Inject(AccountModel.name)
|
@Inject(Account.name)
|
||||||
private readonly accountModel: typeof AccountModel,
|
private readonly accountModel: typeof Account,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +27,7 @@ export class EditAccount {
|
|||||||
private authorize = async (
|
private authorize = async (
|
||||||
accountId: number,
|
accountId: number,
|
||||||
accountDTO: EditAccountDTO,
|
accountDTO: EditAccountDTO,
|
||||||
oldAccount: AccountModel,
|
oldAccount: Account,
|
||||||
) => {
|
) => {
|
||||||
// Validate account name uniquiness.
|
// Validate account name uniquiness.
|
||||||
await this.validator.validateAccountNameUniquiness(
|
await this.validator.validateAccountNameUniquiness(
|
||||||
@@ -64,7 +64,7 @@ export class EditAccount {
|
|||||||
public async editAccount(
|
public async editAccount(
|
||||||
accountId: number,
|
accountId: number,
|
||||||
accountDTO: EditAccountDTO,
|
accountDTO: EditAccountDTO,
|
||||||
): Promise<AccountModel> {
|
): Promise<Account> {
|
||||||
// Retrieve the old account or throw not found service error.
|
// Retrieve the old account or throw not found service error.
|
||||||
const oldAccount = await this.accountModel
|
const oldAccount = await this.accountModel
|
||||||
.query()
|
.query()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { AccountTransformer } from './Account.transformer';
|
import { AccountTransformer } from './Account.transformer';
|
||||||
import { AccountModel } from './models/Account.model';
|
import { Account } from './models/Account.model';
|
||||||
import { AccountRepository } from './repositories/Account.repository';
|
import { AccountRepository } from './repositories/Account.repository';
|
||||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
@@ -9,8 +9,8 @@ import { events } from '@/common/events/events';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetAccount {
|
export class GetAccount {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(AccountModel.name)
|
@Inject(Account.name)
|
||||||
private readonly accountModel: typeof AccountModel,
|
private readonly accountModel: typeof Account,
|
||||||
private readonly accountRepository: AccountRepository,
|
private readonly accountRepository: AccountRepository,
|
||||||
private readonly transformer: TransformerInjectable,
|
private readonly transformer: TransformerInjectable,
|
||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
} from './Accounts.types';
|
} from './Accounts.types';
|
||||||
import { AccountTransactionTransformer } from './AccountTransaction.transformer';
|
import { AccountTransactionTransformer } from './AccountTransaction.transformer';
|
||||||
import { AccountTransaction } from './models/AccountTransaction.model';
|
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 { Inject, Injectable } from '@nestjs/common';
|
||||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@ export class GetAccountTransactionsService {
|
|||||||
@Inject(AccountTransaction.name)
|
@Inject(AccountTransaction.name)
|
||||||
private readonly accountTransaction: typeof AccountTransaction,
|
private readonly accountTransaction: typeof AccountTransaction,
|
||||||
|
|
||||||
@Inject(AccountModel.name)
|
@Inject(Account.name)
|
||||||
private readonly account: typeof AccountModel,
|
private readonly account: typeof Account,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
// import { DynamicListService } from '../DynamicListing/DynamicListService';
|
// import { DynamicListService } from '../DynamicListing/DynamicListService';
|
||||||
// import { AccountTransformer } from './Account.transformer';
|
// import { AccountTransformer } from './Account.transformer';
|
||||||
// import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
// import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||||
// import { AccountModel } from './models/Account.model';
|
// import { Account } from './models/Account.model';
|
||||||
// import { AccountRepository } from './repositories/Account.repository';
|
// import { AccountRepository } from './repositories/Account.repository';
|
||||||
|
|
||||||
// @Injectable()
|
// @Injectable()
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
// constructor(
|
// constructor(
|
||||||
// private readonly dynamicListService: DynamicListService,
|
// private readonly dynamicListService: DynamicListService,
|
||||||
// private readonly transformerService: TransformerInjectable,
|
// private readonly transformerService: TransformerInjectable,
|
||||||
// private readonly accountModel: typeof AccountModel,
|
// private readonly accountModel: typeof Account,
|
||||||
// private readonly accountRepository: AccountRepository,
|
// private readonly accountRepository: AccountRepository,
|
||||||
// ) {}
|
// ) {}
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import { Model } from 'objection';
|
|||||||
// import { flatToNestedArray } from 'utils';
|
// import { flatToNestedArray } from 'utils';
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
// export class AccountModel extends mixin(TenantModel, [
|
// export class Account extends mixin(TenantModel, [
|
||||||
// ModelSettings,
|
// ModelSettings,
|
||||||
// CustomViewBaseModel,
|
// CustomViewBaseModel,
|
||||||
// SearchableModel,
|
// SearchableModel,
|
||||||
// ]) {
|
// ]) {
|
||||||
|
|
||||||
export class AccountModel extends TenantModel {
|
export class Account extends TenantModel {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -126,7 +126,7 @@ export class AccountModel extends TenantModel {
|
|||||||
* Model modifiers.
|
* Model modifiers.
|
||||||
*/
|
*/
|
||||||
static get modifiers() {
|
static get modifiers() {
|
||||||
const TABLE_NAME = AccountModel.tableName;
|
const TABLE_NAME = Account.tableName;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Knex } from 'knex';
|
|||||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import { TenantRepository } from '@/common/repository/TenantRepository';
|
import { TenantRepository } from '@/common/repository/TenantRepository';
|
||||||
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
|
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 { TenantMetadata } from '@/modules/System/models/TenantMetadataModel';
|
||||||
// import { IAccount } from '../Accounts.types';
|
// import { IAccount } from '../Accounts.types';
|
||||||
// import {
|
// import {
|
||||||
@@ -20,8 +20,8 @@ export class AccountRepository extends TenantRepository {
|
|||||||
/**
|
/**
|
||||||
* Gets the repository's model.
|
* Gets the repository's model.
|
||||||
*/
|
*/
|
||||||
get model(): typeof AccountModel {
|
get model(): typeof Account {
|
||||||
return AccountModel.bindKnex(this.tenantDBKnex);
|
return Account.bindKnex(this.tenantDBKnex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { TenancyGlobalMiddleware } from '../Tenancy/TenancyGlobal.middleware';
|
|||||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||||
import { TransformerModule } from '../Transformer/Transformer.module';
|
import { TransformerModule } from '../Transformer/Transformer.module';
|
||||||
import { AccountsModule } from '../Accounts/Accounts.module';
|
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||||
|
import { ExpensesModule } from '../Expenses/Expenses.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -86,7 +87,8 @@ import { AccountsModule } from '../Accounts/Accounts.module';
|
|||||||
TenancyDatabaseModule,
|
TenancyDatabaseModule,
|
||||||
TenancyModelsModule,
|
TenancyModelsModule,
|
||||||
ItemsModule,
|
ItemsModule,
|
||||||
AccountsModule
|
AccountsModule,
|
||||||
|
ExpensesModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
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 { IItem, IItemDTO } from '@/interfaces/Item';
|
||||||
import { ERRORS } from './Items.constants';
|
import { ERRORS } from './Items.constants';
|
||||||
import { Item } from './models/Item';
|
import { Item } from './models/Item';
|
||||||
import { AccountModel } from '../Accounts/models/Account.model';
|
import { Account } from '../Accounts/models/Account.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemsValidators {
|
export class ItemsValidators {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(Item.name) private itemModel: typeof Item,
|
@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 taxRateModel: typeof Item,
|
||||||
@Inject(Item.name) private itemEntryModel: typeof Item,
|
@Inject(Item.name) private itemEntryModel: typeof Item,
|
||||||
@Inject(Item.name) private itemCategoryModel: 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 { TENANCY_DB_CONNECTION } from '../TenancyDB/TenancyDB.constants';
|
||||||
|
|
||||||
import { Item } from '../../../modules/Items/models/Item';
|
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 { ItemEntry } from '@/modules/Items/models/ItemEntry';
|
||||||
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
|
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) => {
|
const modelProviders = models.map((model) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export class Transformer {
|
|||||||
* @param {string} format
|
* @param {string} format
|
||||||
* @returns {string}
|
* @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,
|
// Use the export date format if the async operation is in exporting,
|
||||||
// otherwise use the given or default format.
|
// otherwise use the given or default format.
|
||||||
const _format = this.context.exportAls.isExport
|
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",
|
"dist",
|
||||||
"**/*spec.ts",
|
"**/*spec.ts",
|
||||||
// "./src/modules/DynamicListing/**/*.ts",
|
// "./src/modules/DynamicListing/**/*.ts",
|
||||||
"./src/modules/Export",
|
"./src/modules/Import/**/*.ts",
|
||||||
"./src/modules/Import",
|
"./src/modules/Export/**/*.ts",
|
||||||
"./src/modules/DynamicListing",
|
"./src/modules/DynamicListing",
|
||||||
// "./src/modules/DynamicListing",
|
"./src/modules/DynamicListing/**/*.ts",
|
||||||
"./src/modules/Views"
|
"./src/modules/Views",
|
||||||
|
"./src/modules/Expenses/subscribers"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user